diff --git a/src/EllieBot/Migrations/PostgreSql/20250319010757_livechannels.sql b/src/EllieBot/Migrations/PostgreSql/20250319010757_livechannels.sql new file mode 100644 index 0000000..2751114 --- /dev/null +++ b/src/EllieBot/Migrations/PostgreSql/20250319010757_livechannels.sql @@ -0,0 +1,18 @@ +START TRANSACTION; +CREATE TABLE livechannelconfig ( + id integer GENERATED BY DEFAULT AS IDENTITY, + guildid numeric(20,0) NOT NULL, + channelid numeric(20,0) NOT NULL, + template text NOT NULL, + CONSTRAINT pk_livechannelconfig PRIMARY KEY (id) +); + +CREATE INDEX ix_livechannelconfig_guildid ON livechannelconfig (guildid); + +CREATE UNIQUE INDEX ix_livechannelconfig_guildid_channelid ON livechannelconfig (guildid, channelid); + +INSERT INTO "__EFMigrationsHistory" (migrationid, productversion) +VALUES ('20250319010757_livechannels', '9.0.1'); + +COMMIT; + diff --git a/src/EllieBot/Migrations/PostgreSql/20250318222207_init.Designer.cs b/src/EllieBot/Migrations/PostgreSql/20250319010930_init.Designer.cs similarity index 99% rename from src/EllieBot/Migrations/PostgreSql/20250318222207_init.Designer.cs rename to src/EllieBot/Migrations/PostgreSql/20250319010930_init.Designer.cs index db697cd..6b6214f 100644 --- a/src/EllieBot/Migrations/PostgreSql/20250318222207_init.Designer.cs +++ b/src/EllieBot/Migrations/PostgreSql/20250319010930_init.Designer.cs @@ -12,7 +12,7 @@ using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; namespace EllieBot.Migrations.PostgreSql { [DbContext(typeof(PostgreSqlContext))] - [Migration("20250318222207_init")] + [Migration("20250319010930_init")] partial class init { /// <inheritdoc /> @@ -1541,6 +1541,41 @@ namespace EllieBot.Migrations.PostgreSql b.ToTable("imageonlychannels", (string)null); }); + modelBuilder.Entity("EllieBot.Db.Models.LiveChannelConfig", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id")); + + b.Property<decimal>("ChannelId") + .HasColumnType("numeric(20,0)") + .HasColumnName("channelid"); + + b.Property<decimal>("GuildId") + .HasColumnType("numeric(20,0)") + .HasColumnName("guildid"); + + b.Property<string>("Template") + .IsRequired() + .HasColumnType("text") + .HasColumnName("template"); + + b.HasKey("Id") + .HasName("pk_livechannelconfig"); + + b.HasIndex("GuildId") + .HasDatabaseName("ix_livechannelconfig_guildid"); + + b.HasIndex("GuildId", "ChannelId") + .IsUnique() + .HasDatabaseName("ix_livechannelconfig_guildid_channelid"); + + b.ToTable("livechannelconfig", (string)null); + }); + modelBuilder.Entity("EllieBot.Db.Models.LogSetting", b => { b.Property<int>("Id") diff --git a/src/EllieBot/Migrations/PostgreSql/20250318222207_init.cs b/src/EllieBot/Migrations/PostgreSql/20250319010930_init.cs similarity index 99% rename from src/EllieBot/Migrations/PostgreSql/20250318222207_init.cs rename to src/EllieBot/Migrations/PostgreSql/20250319010930_init.cs index 5278da3..6cfd21e 100644 --- a/src/EllieBot/Migrations/PostgreSql/20250318222207_init.cs +++ b/src/EllieBot/Migrations/PostgreSql/20250319010930_init.cs @@ -550,6 +550,21 @@ namespace EllieBot.Migrations.PostgreSql table.PrimaryKey("pk_imageonlychannels", x => x.id); }); + migrationBuilder.CreateTable( + name: "livechannelconfig", + columns: table => new + { + id = table.Column<int>(type: "integer", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + guildid = table.Column<decimal>(type: "numeric(20,0)", nullable: false), + channelid = table.Column<decimal>(type: "numeric(20,0)", nullable: false), + template = table.Column<string>(type: "text", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("pk_livechannelconfig", x => x.id); + }); + migrationBuilder.CreateTable( name: "logsettings", columns: table => new @@ -1984,6 +1999,17 @@ namespace EllieBot.Migrations.PostgreSql column: "channelid", unique: true); + migrationBuilder.CreateIndex( + name: "ix_livechannelconfig_guildid", + table: "livechannelconfig", + column: "guildid"); + + migrationBuilder.CreateIndex( + name: "ix_livechannelconfig_guildid_channelid", + table: "livechannelconfig", + columns: new[] { "guildid", "channelid" }, + unique: true); + migrationBuilder.CreateIndex( name: "ix_logsettings_guildid", table: "logsettings", @@ -2475,6 +2501,9 @@ namespace EllieBot.Migrations.PostgreSql migrationBuilder.DropTable( name: "imageonlychannels"); + migrationBuilder.DropTable( + name: "livechannelconfig"); + migrationBuilder.DropTable( name: "musicplayersettings"); diff --git a/src/EllieBot/Migrations/PostgreSql/PostgreSqlContextModelSnapshot.cs b/src/EllieBot/Migrations/PostgreSql/PostgreSqlContextModelSnapshot.cs index e9a47ca..0c236ba 100644 --- a/src/EllieBot/Migrations/PostgreSql/PostgreSqlContextModelSnapshot.cs +++ b/src/EllieBot/Migrations/PostgreSql/PostgreSqlContextModelSnapshot.cs @@ -1538,6 +1538,41 @@ namespace EllieBot.Migrations.PostgreSql b.ToTable("imageonlychannels", (string)null); }); + modelBuilder.Entity("EllieBot.Db.Models.LiveChannelConfig", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id")); + + b.Property<decimal>("ChannelId") + .HasColumnType("numeric(20,0)") + .HasColumnName("channelid"); + + b.Property<decimal>("GuildId") + .HasColumnType("numeric(20,0)") + .HasColumnName("guildid"); + + b.Property<string>("Template") + .IsRequired() + .HasColumnType("text") + .HasColumnName("template"); + + b.HasKey("Id") + .HasName("pk_livechannelconfig"); + + b.HasIndex("GuildId") + .HasDatabaseName("ix_livechannelconfig_guildid"); + + b.HasIndex("GuildId", "ChannelId") + .IsUnique() + .HasDatabaseName("ix_livechannelconfig_guildid_channelid"); + + b.ToTable("livechannelconfig", (string)null); + }); + modelBuilder.Entity("EllieBot.Db.Models.LogSetting", b => { b.Property<int>("Id") diff --git a/src/EllieBot/Migrations/Sqlite/20250319010745_livechannels.sql b/src/EllieBot/Migrations/Sqlite/20250319010745_livechannels.sql new file mode 100644 index 0000000..29ac746 --- /dev/null +++ b/src/EllieBot/Migrations/Sqlite/20250319010745_livechannels.sql @@ -0,0 +1,17 @@ +BEGIN TRANSACTION; +CREATE TABLE "LiveChannelConfig" ( + "Id" INTEGER NOT NULL CONSTRAINT "PK_LiveChannelConfig" PRIMARY KEY AUTOINCREMENT, + "GuildId" INTEGER NOT NULL, + "ChannelId" INTEGER NOT NULL, + "Template" TEXT NOT NULL +); + +CREATE INDEX "IX_LiveChannelConfig_GuildId" ON "LiveChannelConfig" ("GuildId"); + +CREATE UNIQUE INDEX "IX_LiveChannelConfig_GuildId_ChannelId" ON "LiveChannelConfig" ("GuildId", "ChannelId"); + +INSERT INTO "__EFMigrationsHistory" ("MigrationId", "ProductVersion") +VALUES ('20250319010745_livechannels', '9.0.1'); + +COMMIT; + diff --git a/src/EllieBot/Migrations/Sqlite/20250318222152_init.Designer.cs b/src/EllieBot/Migrations/Sqlite/20250319010920_init.Designer.cs similarity index 99% rename from src/EllieBot/Migrations/Sqlite/20250318222152_init.Designer.cs rename to src/EllieBot/Migrations/Sqlite/20250319010920_init.Designer.cs index 969d466..fc2c559 100644 --- a/src/EllieBot/Migrations/Sqlite/20250318222152_init.Designer.cs +++ b/src/EllieBot/Migrations/Sqlite/20250319010920_init.Designer.cs @@ -11,7 +11,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion; namespace EllieBot.Migrations.Sqlite { [DbContext(typeof(SqliteContext))] - [Migration("20250318222152_init")] + [Migration("20250319010920_init")] partial class init { /// <inheritdoc /> @@ -1151,6 +1151,32 @@ namespace EllieBot.Migrations.Sqlite b.ToTable("ImageOnlyChannels"); }); + modelBuilder.Entity("EllieBot.Db.Models.LiveChannelConfig", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property<ulong>("ChannelId") + .HasColumnType("INTEGER"); + + b.Property<ulong>("GuildId") + .HasColumnType("INTEGER"); + + b.Property<string>("Template") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("GuildId"); + + b.HasIndex("GuildId", "ChannelId") + .IsUnique(); + + b.ToTable("LiveChannelConfig"); + }); + modelBuilder.Entity("EllieBot.Db.Models.LogSetting", b => { b.Property<int>("Id") diff --git a/src/EllieBot/Migrations/Sqlite/20250318222152_init.cs b/src/EllieBot/Migrations/Sqlite/20250319010920_init.cs similarity index 99% rename from src/EllieBot/Migrations/Sqlite/20250318222152_init.cs rename to src/EllieBot/Migrations/Sqlite/20250319010920_init.cs index 429b5fb..7089818 100644 --- a/src/EllieBot/Migrations/Sqlite/20250318222152_init.cs +++ b/src/EllieBot/Migrations/Sqlite/20250319010920_init.cs @@ -550,6 +550,21 @@ namespace EllieBot.Migrations.Sqlite table.PrimaryKey("PK_ImageOnlyChannels", x => x.Id); }); + migrationBuilder.CreateTable( + name: "LiveChannelConfig", + columns: table => new + { + Id = table.Column<int>(type: "INTEGER", nullable: false) + .Annotation("Sqlite:Autoincrement", true), + GuildId = table.Column<ulong>(type: "INTEGER", nullable: false), + ChannelId = table.Column<ulong>(type: "INTEGER", nullable: false), + Template = table.Column<string>(type: "TEXT", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_LiveChannelConfig", x => x.Id); + }); + migrationBuilder.CreateTable( name: "LogSettings", columns: table => new @@ -1986,6 +2001,17 @@ namespace EllieBot.Migrations.Sqlite column: "ChannelId", unique: true); + migrationBuilder.CreateIndex( + name: "IX_LiveChannelConfig_GuildId", + table: "LiveChannelConfig", + column: "GuildId"); + + migrationBuilder.CreateIndex( + name: "IX_LiveChannelConfig_GuildId_ChannelId", + table: "LiveChannelConfig", + columns: new[] { "GuildId", "ChannelId" }, + unique: true); + migrationBuilder.CreateIndex( name: "IX_LogSettings_GuildId", table: "LogSettings", @@ -2477,6 +2503,9 @@ namespace EllieBot.Migrations.Sqlite migrationBuilder.DropTable( name: "ImageOnlyChannels"); + migrationBuilder.DropTable( + name: "LiveChannelConfig"); + migrationBuilder.DropTable( name: "MusicPlayerSettings"); diff --git a/src/EllieBot/Migrations/Sqlite/SqliteContextModelSnapshot.cs b/src/EllieBot/Migrations/Sqlite/SqliteContextModelSnapshot.cs index 6ef86fe..2c146f0 100644 --- a/src/EllieBot/Migrations/Sqlite/SqliteContextModelSnapshot.cs +++ b/src/EllieBot/Migrations/Sqlite/SqliteContextModelSnapshot.cs @@ -1148,6 +1148,32 @@ namespace EllieBot.Migrations.Sqlite b.ToTable("ImageOnlyChannels"); }); + modelBuilder.Entity("EllieBot.Db.Models.LiveChannelConfig", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property<ulong>("ChannelId") + .HasColumnType("INTEGER"); + + b.Property<ulong>("GuildId") + .HasColumnType("INTEGER"); + + b.Property<string>("Template") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("GuildId"); + + b.HasIndex("GuildId", "ChannelId") + .IsUnique(); + + b.ToTable("LiveChannelConfig"); + }); + modelBuilder.Entity("EllieBot.Db.Models.LogSetting", b => { b.Property<int>("Id") diff --git a/src/EllieBot/Modules/Administration/Timezone/GuildTimezoneService.cs b/src/EllieBot/Modules/Administration/Timezone/GuildTimezoneService.cs index 9776c07..68453f3 100644 --- a/src/EllieBot/Modules/Administration/Timezone/GuildTimezoneService.cs +++ b/src/EllieBot/Modules/Administration/Timezone/GuildTimezoneService.cs @@ -85,8 +85,7 @@ public sealed class GuildTimezoneService : ITimezoneService, IReadyExecutor, IES to = GetTimeZoneOrDefault(g.Id) ?? TimeZoneInfo.Local; } - return TimeZoneInfo.ConvertTime(DateTime.UtcNow, TimeZoneInfo.Utc, to).ToString("HH:mm ") - + to.StandardName.GetInitials(); + return TimeZoneInfo.ConvertTime(DateTime.UtcNow, TimeZoneInfo.Utc, to).ToShortTimeString(); }); } } \ No newline at end of file diff --git a/src/EllieBot/Modules/Utility/LiveChannel/LiveChannelCommands.cs b/src/EllieBot/Modules/Utility/LiveChannel/LiveChannelCommands.cs new file mode 100644 index 0000000..a4b0cd5 --- /dev/null +++ b/src/EllieBot/Modules/Utility/LiveChannel/LiveChannelCommands.cs @@ -0,0 +1,91 @@ +namespace EllieBot.Modules.Utility.LiveChannel; + +public partial class Utility +{ + [Group] + public class LiveChannelCommands(LiveChannelService svc) : EllieModule + { + [Cmd] + [RequireContext(ContextType.Guild)] + [UserPerm(GuildPerm.ManageChannels)] + [BotPerm(GuildPerm.ManageChannels)] + public async Task LiveChAdd(IChannel channel, [Leftover] string template) + { + if (!await svc.AddLiveChannelAsync(ctx.Guild.Id, channel, template)) + { + await Response() + .Error(strs.livechannel_limit(LiveChannelService.MAX_LIVECHANNELS)) + .SendAsync(); + return; + } + + var eb = CreateEmbed() + .WithOkColor() + .WithDescription(GetText(strs.livechannel_added(channel.Name))) + .AddField(GetText(strs.template), template) + .AddField(GetText(strs.preview), + await repSvc.ReplaceAsync(template, + new( + client: ctx.Client as DiscordSocketClient, + guild: ctx.Guild + ))); + await Response() + .Confirm(strs.livechannel_added(channel.Name)) + .SendAsync(); + return; + } + + [Cmd] + [RequireContext(ContextType.Guild)] + [UserPerm(GuildPerm.ManageChannels)] + [BotPerm(GuildPerm.ManageChannels)] + public async Task LiveChList() + { + var liveChannels = await svc.GetLiveChannelsAsync(ctx.Guild.Id); + + if (liveChannels.Count == 0) + { + await Response().Pending(strs.livechannel_list_empty).SendAsync(); + return; + } + + var embed = CreateEmbed() + .WithTitle(GetText(strs.livechannel_list_title(ctx.Guild.Name))); + + foreach (var config in liveChannels) + { + var channelName = await ctx.Guild.GetChannelAsync(config.ChannelId) + .Fmap(x => x?.Name ?? config.ChannelId.ToString()); + + embed.AddField(channelName, config.Template); + } + + await Response().Embed(embed).SendAsync(); + } + + [Cmd] + [RequireContext(ContextType.Guild)] + [UserPerm(GuildPerm.ManageChannels)] + [BotPerm(GuildPerm.ManageChannels)] + public Task LiveChRemove(IChannel channel) + => LiveChRemove(channel.Id); + + [Cmd] + [RequireContext(ContextType.Guild)] + [UserPerm(GuildPerm.ManageChannels)] + [BotPerm(GuildPerm.ManageChannels)] + public async Task LiveChRemove(ulong channelId) + { + if (await svc.RemoveLiveChannelAsync(ctx.Guild.Id, channelId)) + { + await Response() + .Confirm(strs.livechannel_removed(((SocketGuild)ctx.Guild).GetChannel(channelId)?.Name ?? + channelId.ToString())).SendAsync(); + } + else + { + await Response().Error(strs.livechannel_not_found).SendAsync(); + } + } + } +} \ No newline at end of file diff --git a/src/EllieBot/Modules/Utility/LiveChannel/LiveChannelService.cs b/src/EllieBot/Modules/Utility/LiveChannel/LiveChannelService.cs new file mode 100644 index 0000000..c2cd9d9 --- /dev/null +++ b/src/EllieBot/Modules/Utility/LiveChannel/LiveChannelService.cs @@ -0,0 +1,195 @@ +using System.Net; +using LinqToDB; +using LinqToDB.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore; +using EllieBot.Common.ModuleBehaviors; +using EllieBot.Db.Models; +using Newtonsoft.Json.Linq; + +namespace EllieBot.Modules.Utility.LiveChannel; + +/// <summary> +/// Service for managing live channels. +/// </summary> +public class LiveChannelService( + DbService db, + DiscordSocketClient client, + IReplacementService repSvc, + ShardData shardData) : IReadyExecutor, IEService +{ + public const int MAX_LIVECHANNELS = 5; + + private readonly ConcurrentDictionary<ulong, ConcurrentDictionary<ulong, LiveChannelConfig>> _liveChannels = new(); + + /// <summary> + /// Initializes data when bot is ready + /// </summary> + public async Task OnReadyAsync() + { + // Load all existing live channels into memory + await using var uow = db.GetDbContext(); + var configs = await uow.GetTable<LiveChannelConfig>() + .AsNoTracking() + .Where(x => Queries.GuildOnShard(x.GuildId, shardData.TotalShards, shardData.ShardId)) + .ToListAsyncLinqToDB(); + + foreach (var config in configs) + { + var guildDict = _liveChannels.GetOrAdd( + config.GuildId, + _ => new()); + + guildDict[config.ChannelId] = config; + } + + using var timer = new PeriodicTimer(TimeSpan.FromMinutes(10)); + while (await timer.WaitForNextTickAsync()) + { + try + { + // get all live channels from cache + var channels = new List<LiveChannelConfig>(_liveChannels.Count * 2); + + foreach (var (_, vals) in _liveChannels) + { + foreach (var (_, config) in vals) + { + channels.Add(config); + } + } + + foreach (var config in channels) + { + var guild = client.GetGuild(config.GuildId); + var channel = guild?.GetChannel(config.ChannelId); + + if (channel is null) + { + await RemoveLiveChannelAsync(config.GuildId, config.ChannelId); + continue; + } + + var repCtx = new ReplacementContext( + user: null, + guild: guild, + client: client + ); + + try + { + var text = await repSvc.ReplaceAsync(config.Template, repCtx); + + // only update if needed + if (channel.Name != text) + await channel.ModifyAsync(x => x.Name = text); + } + catch (HttpException ex) when (ex.HttpCode == HttpStatusCode.Forbidden) + { + await RemoveLiveChannelAsync(config.GuildId, config.ChannelId); + Log.Warning( + "Channel {ChannelId} in guild {GuildId} is not accessible. Live channel will be removed", + config.ChannelId, + config.GuildId); + } + catch (HttpException ex) when (ex.HttpCode == HttpStatusCode.TooManyRequests || + ex.DiscordCode == DiscordErrorCode.ChannelWriteRatelimit) + { + Log.Warning(ex, "LiveChannel hit a ratelimit. Sleeping for 2 minutes: {Message}", ex.Message); + await Task.Delay(2.Minutes()); + } + catch (Exception e) + { + Log.Error(e, "Error in live channel service: {ErrorMessage}", e.Message); + } + + // wait for half a second to reduce the chance of global ratelimits + await Task.Delay(500); + } + } + catch (Exception ex) + { + Log.Error(ex, "Error in live channel service: {ErrorMessage}", ex.Message); + } + } + } + + /// <summary> + /// Adds a new live channel configuration to the specified guild. + /// </summary> + /// <param name="guildId">ID of the guild</param> + /// <param name="channel">Channel to set as live</param> + /// <param name="template">Template text to use for the channel</param> + /// <returns>True if successfully added, false otherwise</returns> + public async Task<bool> AddLiveChannelAsync(ulong guildId, IChannel channel, string template) + { + var guildDict = _liveChannels.GetOrAdd( + guildId, + _ => new()); + + if (guildDict.Count >= MAX_LIVECHANNELS) + return false; + + await using var uow = db.GetDbContext(); + await uow.GetTable<LiveChannelConfig>() + .InsertOrUpdateAsync(() => new() + { + GuildId = guildId, + ChannelId = channel.Id, + Template = template + }, + (_) => new() + { + Template = template + }, + () => new() + { + GuildId = guildId, + ChannelId = channel.Id + }); + + // Add to in-memory cache + var newConfig = new LiveChannelConfig + { + GuildId = guildId, + ChannelId = channel.Id, + Template = template + }; + + guildDict[channel.Id] = newConfig; + return true; + } + + /// <summary> + /// Removes a live channel configuration from the specified guild. + /// </summary> + /// <param name="guildId">ID of the guild</param> + /// <param name="channelId">ID of the channel to remove as live</param> + /// <returns>True if successfully removed, false otherwise</returns> + public async Task<bool> RemoveLiveChannelAsync(ulong guildId, ulong channelId) + { + if (!_liveChannels.TryGetValue(guildId, out var guildDict) || + !guildDict.TryRemove(channelId, out _)) + return false; + + await using var uow = db.GetDbContext(); + await uow.GetTable<LiveChannelConfig>() + .Where(x => x.GuildId == guildId && x.ChannelId == channelId) + .DeleteAsync(); + + return true; + } + + /// <summary> + /// Gets all live channels for a guild. + /// </summary> + /// <param name="guildId">ID of the guild</param> + /// <returns>List of live channel configurations</returns> + public async Task<List<LiveChannelConfig>> GetLiveChannelsAsync(ulong guildId) + { + await using var uow = db.GetDbContext(); + return await uow.GetTable<LiveChannelConfig>() + .AsNoTracking() + .Where(x => x.GuildId == guildId) + .ToListAsyncLinqToDB(); + } +} \ No newline at end of file diff --git a/src/EllieBot/Modules/Utility/LiveChannel/db/LiveChannelConfig.cs b/src/EllieBot/Modules/Utility/LiveChannel/db/LiveChannelConfig.cs new file mode 100644 index 0000000..b88e7ee --- /dev/null +++ b/src/EllieBot/Modules/Utility/LiveChannel/db/LiveChannelConfig.cs @@ -0,0 +1,42 @@ +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; +using EllieBot.Common.TypeReaders.Models; +using EllieBot.Db; +using EllieBot.Db.Models; + +namespace EllieBot.Db.Models; + +/// <summary> +/// Configuration for a live channel. +/// </summary> +public class LiveChannelConfig +{ + [Key] + public int Id { get; set; } + + /// <summary> + /// ID of the server this live channel belongs to. + /// </summary> + public ulong GuildId { get; set; } + + /// <summary> + /// ID of the channel that is configured as a live channel. + /// </summary> + public ulong ChannelId { get; set; } + + /// <summary> + /// Text template to be used for the live channel. + /// </summary> + public string Template { get; set; } = ""; +} + +public class LiveChannelConfigDbEntityTypeConfiguration : IEntityTypeConfiguration<LiveChannelConfig> +{ + public void Configure(EntityTypeBuilder<LiveChannelConfig> builder) + { + builder.HasIndex(x => x.GuildId); + builder.HasIndex(x => new { x.GuildId, x.ChannelId }).IsUnique(); + } +} \ No newline at end of file diff --git a/src/EllieBot/Modules/Xp/XpRate/GuildConfigXpService.cs b/src/EllieBot/Modules/Xp/XpRate/XpRateService.cs similarity index 100% rename from src/EllieBot/Modules/Xp/XpRate/GuildConfigXpService.cs rename to src/EllieBot/Modules/Xp/XpRate/XpRateService.cs diff --git a/src/EllieBot/data/commandlist.json b/src/EllieBot/data/commandlist.json index f587f65..01eada3 100644 --- a/src/EllieBot/data/commandlist.json +++ b/src/EllieBot/data/commandlist.json @@ -1731,7 +1731,7 @@ ".sar excl", ".sar tesar" ], - "Description": "Toggles whether self-assigned roles are exclusive.\nWhile enabled, users can only have one self-assignable role per group.", + "Description": "Toggles the sar group as exclusive.\nWhile enabled, users can only have one self-assignable role from that group.", "Usage": [ ".sar exclusive 1" ], @@ -2617,6 +2617,21 @@ "ModerateMembers Server Permission" ] }, + { + "Aliases": [ + ".masskick" + ], + "Description": "Kicks multiple users at once. Specify a space separated list of IDs of users who you wish to kick.", + "Usage": [ + ".masskick 123123123 3333333333 444444444" + ], + "Submodule": "UserPunishCommands", + "Module": "Administration", + "Options": null, + "Requirements": [ + "KickMembers Server Permission" + ] + }, { "Aliases": [ ".massban" @@ -2717,7 +2732,7 @@ ".exas", ".expraddserver" ], - "Description": "Add an expression with a trigger and a response in this server. Bot will post a response whenever someone types the trigger word. This command is useful if you want to lower the permission requirement for managing expressions by using `.dpo`. Guide [here](<https://docs.elliebot.net/ellie/features/expressions/>).", + "Description": "Add an expression with a trigger and a response in this server. Bot will post a response whenever someone types the trigger word. This command is useful if you want to lower the permission requirement for managing expressions by using `.dpo`.", "Usage": [ ".expradds \"hello\" Hi there %user.mention%" ], @@ -2735,7 +2750,7 @@ ".exa", ".acr" ], - "Description": "Add an expression with a trigger and a response. Bot will post a response whenever someone types the trigger word. Running this command in a server requires the Administrator permission. Running this command in DM is Bot Owner only and adds a new global expression. Guide [here](<https://docs.elliebot.net/ellie/features/expressions/>)", + "Description": "Add an expression with a trigger and a response.\nBot will post a response whenever someone types the trigger word.\nRunning this command in a server requires the Administrator permission.\nRunning this command in DM is Bot Owner only and adds a new global expression.", "Usage": [ ".expradd \"hello\" Hi there %user.mention%" ], @@ -4513,6 +4528,61 @@ ] } ], + "LiveChannelCommands": [ + { + "Aliases": [ + ".livechadd", + ".lcha", + ".lchadd" + ], + "Description": "Adds a channel as a live channel with the specified template.\nYou can see a full list of placeholders with `.phs` command.", + "Usage": [ + ".livechadd #general Time: %server.time%", + ".livechadd #general -- %server.members% --" + ], + "Submodule": "LiveChannelCommands", + "Module": "LiveChannelCommands", + "Options": null, + "Requirements": [ + "ManageChannels Server Permission" + ] + }, + { + "Aliases": [ + ".livechlist", + ".lchl", + ".lchli", + ".lchlist" + ], + "Description": "Lists all live channels in the server.", + "Usage": [ + ".livechlist" + ], + "Submodule": "LiveChannelCommands", + "Module": "LiveChannelCommands", + "Options": null, + "Requirements": [ + "ManageChannels Server Permission" + ] + }, + { + "Aliases": [ + ".livechremove", + ".lchd", + ".lchrm" + ], + "Description": "Removes a live channel.", + "Usage": [ + ".livechremove #general" + ], + "Submodule": "LiveChannelCommands", + "Module": "LiveChannelCommands", + "Options": null, + "Requirements": [ + "ManageChannels Server Permission" + ] + } + ], "Marmalade": [ { "Aliases": [ @@ -4537,7 +4607,7 @@ ".marmaladeunload", ".maunload" ], - "Description": "Unloads the previously loaded marmalade.\nProvide no name to see the list of unloadable marmalades. \nRead about the marmalade system [here](https://docs.elliebot.net/ellie/marmalade/creating-a-marmalade/)", + "Description": "Unloads the previously loaded marmalade.\nProvide no name to see the list of unloadable marmalades.\nRead about the marmalade system [here](https://docs.elliebot.net/ellie/marmalade/creating-a-marmalade/)", "Usage": [ ".marmaladeunload mycoolmarmalade", ".marmaladeunload" @@ -4572,7 +4642,7 @@ ".marmaladeinfo", ".mainfo" ], - "Description": "Shows information about the specified marmalade such as the author, name, description, list of canaries, number of commands etc.\nProvide no name to see the basic information about all loaded marmalades. \nRead about the marmalade system [here](https://docs.elliebot.net/ellie/marmalade/creating-a-marmalade/)", + "Description": "Shows information about the specified marmalade such as the author, name, description, list of canaries, number of commands etc.\nProvide no name to see the basic information about all loaded marmalades.\nRead about the marmalade system [here](https://docs.elliebot.net/ellie/marmalade/creating-a-marmalade/)", "Usage": [ ".marmaladeinfo mycoolmarmalade", ".marmaladeinfo" @@ -5766,6 +5836,54 @@ ] } ], + "ScheduledCommands": [ + { + "Aliases": [ + ".schedulelist", + ".schl", + ".schli" + ], + "Description": "Lists your scheduled commands in the current server.", + "Usage": [ + ".schedulelist" + ], + "Submodule": "ScheduledCommands", + "Module": "ScheduledCommands", + "Options": null, + "Requirements": [] + }, + { + "Aliases": [ + ".scheduledelete", + ".schd", + ".schdel" + ], + "Description": "Deletes one of your scheduled commands by its ID.", + "Usage": [ + ".scheduledelete 5" + ], + "Submodule": "ScheduledCommands", + "Module": "ScheduledCommands", + "Options": null, + "Requirements": [] + }, + { + "Aliases": [ + ".scheduleadd", + ".scha", + ".schadd" + ], + "Description": "Schedules a command to be executed after the specified amount of time.\nYou can schedule up to 5 commands at a time.", + "Usage": [ + ".scheduleadd 1h5m .say Hello after 1 hour and 5 minutes", + ".scheduleadd 3h .br all" + ], + "Submodule": "ScheduledCommands", + "Module": "ScheduledCommands", + "Options": null, + "Requirements": [] + } + ], "Searches": [ { "Aliases": [ @@ -6898,7 +7016,7 @@ "Aliases": [ ".savechat" ], - "Description": "Saves a number of messages to a text file and sends it to you.", + "Description": "Saves a number of messages to a text file and sends it to you.\nMax is 1000, unless you're the bot owner. ", "Usage": [ ".savechat 150" ], @@ -6906,7 +7024,7 @@ "Module": "Utility", "Options": null, "Requirements": [ - "Bot Owner Only" + "Administrator Server Permission" ] }, { @@ -8513,6 +8631,41 @@ "Options": null, "Requirements": [] }, + { + "Aliases": [ + ".xpexclusion", + ".xpexl" + ], + "Description": "Shows a list of all XP exclusions in the server.", + "Usage": [ + ".xpexclusion" + ], + "Submodule": "XpExclusionCommands", + "Module": "Xp", + "Options": null, + "Requirements": [ + "Administrator Server Permission" + ] + }, + { + "Aliases": [ + ".xpexclude", + ".xpex" + ], + "Description": "Toggles XP gain exclusion for a specified item.\nItem types can be Role or User.", + "Usage": [ + ".xpexclude @CoolRole", + ".xpexclude @User", + ".xpexclude role 123123123", + ".xpexclude user 123123123" + ], + "Submodule": "XpExclusionCommands", + "Module": "Xp", + "Options": null, + "Requirements": [ + "Administrator Server Permission" + ] + }, { "Aliases": [ ".xprate" diff --git a/src/EllieBot/strings/aliases.yml b/src/EllieBot/strings/aliases.yml index c44c944..41808c0 100644 --- a/src/EllieBot/strings/aliases.yml +++ b/src/EllieBot/strings/aliases.yml @@ -1634,4 +1634,17 @@ xpexclusion: - xpexl xpexclude: - xpexclude - - xpex \ No newline at end of file + - xpex +livechadd: + - livechadd + - lcha + - lchadd +livechlist: + - livechlist + - lchl + - lchli + - lchlist +livechremove: + - livechremove + - lchd + - lchrm \ No newline at end of file diff --git a/src/EllieBot/strings/commands/commands.en-US.yml b/src/EllieBot/strings/commands/commands.en-US.yml index 4bb6345..bb18f7a 100644 --- a/src/EllieBot/strings/commands/commands.en-US.yml +++ b/src/EllieBot/strings/commands/commands.en-US.yml @@ -5128,4 +5128,31 @@ xpexclude: - type: desc: "Type of the item to exclude: role or user" itemId: - desc: "ID or mention of the item to exclude." \ No newline at end of file + desc: "ID or mention of the item to exclude." +livechadd: + desc: |- + Adds a channel as a live channel with the specified template. + You can see a full list of placeholders with `{0}phs` command. + ex: + - '#general Time: %server.time%' + - '#general -- %server.members% --' + params: + - channel: + desc: "The channel to configure as a live channel." + - template: + desc: "The text template to use as a name for the live channel." +livechlist: + desc: |- + Lists all live channels in the server. + ex: + - '' + params: + - { } +livechremove: + desc: |- + Removes a live channel. + ex: + - '#general' + params: + - channel: + desc: "The channel to remove from live channels." \ No newline at end of file diff --git a/src/EllieBot/strings/responses/responses.en-US.json b/src/EllieBot/strings/responses/responses.en-US.json index 1427c2a..1021251 100644 --- a/src/EllieBot/strings/responses/responses.en-US.json +++ b/src/EllieBot/strings/responses/responses.en-US.json @@ -1221,5 +1221,15 @@ "xp_exclusion_none": "There are no exclusions set for this server.", "xp_exclusion_title": "XP Exclusions", "xp_exclude_added": "{0}: {1} has been excluded from the XP system.", - "xp_exclude_removed": "{0}: {1} is no longer excluded from the XP system." + "xp_exclude_removed": "{0}: {1} is no longer excluded from the XP system.", + "livechannel_added": "Successfully added {0} as a live channel.", + "livechannel_template": "Template: `{0}`", + "livechannel_limit": "You can have at most {0} live channels per server.", + "livechannel_exists": "This channel is already configured as a live channel.", + "livechannel_removed": "Successfully removed {0} from live channels.", + "livechannel_not_found": "Channel was not found in the live channels list.", + "livechannel_list_title": "Live Channels in {0}", + "livechannel_list_empty": "No live channels configured for this server.", + "template": "Template", + "preview": "Preview" } \ No newline at end of file