diff --git a/DibariBot/Core/BotConfig.cs b/DibariBot/Core/BotConfig.cs index 535db07..fa7728a 100644 --- a/DibariBot/Core/BotConfig.cs +++ b/DibariBot/Core/BotConfig.cs @@ -38,9 +38,6 @@ public class BotConfig [YamlMember(Description = "An optional API key for Seq. Empty string is interpreted as no API key.")] public string SeqApiKey { get; set; } = ""; - [YamlMember(Description = "The default config for the bot.")] - public string DefaultPrefix { get; set; } = "m."; - [YamlMember(Description = "The logging level to use.")] public LogEventLevel LogEventLevel { get; set; } = LogEventLevel.Information; @@ -116,6 +113,14 @@ public class BotConfig } ]; + [YamlMember(Description = "The color to use for all embeds (if not set by the Guild).\n" + + "Ideally this should be formatted as a hex number. Default for example is 0x5E69A3")] + public int DefaultEmbedColor { get; set; } = 0x5E69A3; + + [YamlMember(Description = "The color to use for all embeds (if not set by the Guild). \n" + + "Ideally this should be formatted as a hex number. Default for example is 0xE74C3C")] + public int ErrorEmbedColor { get; set; } = 0xE74C3C; + public struct AboutField { public string Name { get; set; } diff --git a/DibariBot/Core/BotService.cs b/DibariBot/Core/BotService.cs index 68a39cf..7211008 100644 --- a/DibariBot/Core/BotService.cs +++ b/DibariBot/Core/BotService.cs @@ -1,5 +1,7 @@ using System.Reflection; using DibariBot.Database; +using Discord.Commands; +using Discord.Interactions; using Discord.WebSocket; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; @@ -11,7 +13,9 @@ public class BotService( BotConfig config, DbService dbService, ILogger logger, - CommandHandler commandHandler + CommandHandler commandHandler, + InteractionService interactionService, + CommandService commandService ) : BackgroundService { protected override async Task ExecuteAsync(CancellationToken stoppingToken) @@ -35,6 +39,9 @@ protected override async Task ExecuteAsync(CancellationToken stoppingToken) client.Ready += Client_Ready; + interactionService.Log += Client_Log; + commandService.Log += Client_Log; + await client.LoginAsync(TokenType.Bot, config.BotToken); await client.StartAsync(); } diff --git a/DibariBot/Core/CommandHandler.cs b/DibariBot/Core/CommandHandler.cs index dd433ba..98d24bc 100644 --- a/DibariBot/Core/CommandHandler.cs +++ b/DibariBot/Core/CommandHandler.cs @@ -123,10 +123,10 @@ protected async Task CommandExecuted(Optional cmdInfoOpt, ICommandC #region Interaction Handling - protected static Task InteractionExecuted(ICommandInfo cmdInfo, IInteractionContext ctx, Discord.Interactions.IResult res) + protected async Task InteractionExecuted(ICommandInfo cmdInfo, IInteractionContext ctx, Discord.Interactions.IResult res) { if (res.IsSuccess) - return Task.CompletedTask; + return; var messageBody = $"{res.Error}, {res.ErrorReason}"; @@ -137,14 +137,12 @@ protected static Task InteractionExecuted(ICommandInfo cmdInfo, IInteractionCont if (ctx.Interaction.HasResponded) { - ctx.Interaction.ModifyOriginalResponseAsync(new MessageContents(messageBody, embed: null, null)); + await ctx.Interaction.ModifyOriginalResponseAsync(new MessageContents(messageBody, embed: null, null)); } else { - ctx.Interaction.RespondAsync(messageBody, ephemeral: true); + await ctx.Interaction.RespondAsync(messageBody, ephemeral: true); } - - return Task.CompletedTask; } diff --git a/DibariBot/Core/Config/BotConfigFactory.cs b/DibariBot/Core/Config/BotConfigFactory.cs index ee15a03..50c9ae0 100644 --- a/DibariBot/Core/Config/BotConfigFactory.cs +++ b/DibariBot/Core/Config/BotConfigFactory.cs @@ -22,6 +22,7 @@ public bool GetConfig([NotNullWhen(true)] out BotConfig? botConfig) var deserializer = new DeserializerBuilder() .WithNamingConvention(UnderscoredNamingConvention.Instance) + .IgnoreUnmatchedProperties() .Build(); Directory.CreateDirectory(DataDir); diff --git a/DibariBot/Core/Database/Models/GuildConfig.cs b/DibariBot/Core/Database/Models/GuildConfig.cs index fdec274..95526e2 100644 --- a/DibariBot/Core/Database/Models/GuildConfig.cs +++ b/DibariBot/Core/Database/Models/GuildConfig.cs @@ -2,15 +2,16 @@ namespace DibariBot.Database; -#nullable disable public class GuildConfig { public const int MaxPrefixLength = 8; - public const string DefaultPrefix = "m?"; + public const string DefaultPrefix = "m."; [Key] public ulong GuildId { get; set; } [MaxLength(MaxPrefixLength)] public string Prefix { get; set; } = DefaultPrefix; + + public int? EmbedColor { get; set; } } diff --git a/DibariBot/Core/Extensions/EmbedBuilderExtensions.cs b/DibariBot/Core/Extensions/EmbedBuilderExtensions.cs deleted file mode 100644 index b301125..0000000 --- a/DibariBot/Core/Extensions/EmbedBuilderExtensions.cs +++ /dev/null @@ -1,23 +0,0 @@ -namespace DibariBot; - -public enum CommandResult -{ - Default, - Failure -} - -public static class EmbedBuilderExtensions -{ - public static uint SuccessColor = 0xFBEED9; - public static uint FailureColor = 0xDA373C; - - public static EmbedBuilder WithColor(this EmbedBuilder builder, CommandResult result) - { - return result switch - { - CommandResult.Default => builder.WithColor(SuccessColor), - CommandResult.Failure => builder.WithColor(FailureColor), - _ => throw new ArgumentOutOfRangeException(nameof(result), result, null) - }; - } -} diff --git a/DibariBot/DibariBot.csproj.DotSettings b/DibariBot/DibariBot.csproj.DotSettings index 9922706..d2f55b5 100644 --- a/DibariBot/DibariBot.csproj.DotSettings +++ b/DibariBot/DibariBot.csproj.DotSettings @@ -2,6 +2,7 @@ True True True + True True True True diff --git a/DibariBot/Migrations/PostgresMigrations/20240830053851_EmbedColorConfig.Designer.cs b/DibariBot/Migrations/PostgresMigrations/20240830053851_EmbedColorConfig.Designer.cs new file mode 100644 index 0000000..2e34b2d --- /dev/null +++ b/DibariBot/Migrations/PostgresMigrations/20240830053851_EmbedColorConfig.Designer.cs @@ -0,0 +1,145 @@ +// +using System; +using DibariBot.Database; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace DibariBot.Migrations.PostgresMigrations +{ + [DbContext(typeof(PostgresContext))] + [Migration("20240830053851_EmbedColorConfig")] + partial class EmbedColorConfig + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "8.0.8") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("DibariBot.Database.GuildConfig", b => + { + b.Property("GuildId") + .ValueGeneratedOnAdd() + .HasColumnType("numeric(20,0)"); + + b.Property("EmbedColor") + .HasColumnType("integer"); + + b.Property("Prefix") + .IsRequired() + .HasMaxLength(8) + .HasColumnType("character varying(8)"); + + b.HasKey("GuildId"); + + b.ToTable("GuildConfigs"); + }); + + modelBuilder.Entity("DibariBot.Database.Models.DefaultManga", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ChannelId") + .HasColumnType("numeric(20,0)"); + + b.Property("GuildId") + .HasColumnType("numeric(20,0)"); + + b.Property("Manga") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("GuildId", "ChannelId") + .IsUnique(); + + b.ToTable("DefaultMangas"); + }); + + modelBuilder.Entity("DibariBot.Database.Models.RegexChannelEntry", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ChannelId") + .HasColumnType("numeric(20,0)"); + + b.Property("RegexFilterId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("RegexFilterId"); + + b.HasIndex("ChannelId", "RegexFilterId"); + + b.ToTable("RegexChannelEntries"); + }); + + modelBuilder.Entity("DibariBot.Database.Models.RegexFilter", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ChannelFilterScope") + .HasColumnType("integer"); + + b.Property("Filter") + .IsRequired() + .HasColumnType("text"); + + b.Property("FilterType") + .HasColumnType("integer"); + + b.Property("GuildId") + .HasColumnType("numeric(20,0)"); + + b.Property("Template") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("GuildId"); + + b.ToTable("RegexFilters"); + }); + + modelBuilder.Entity("DibariBot.Database.Models.RegexChannelEntry", b => + { + b.HasOne("DibariBot.Database.Models.RegexFilter", "RegexFilter") + .WithMany("RegexChannelEntries") + .HasForeignKey("RegexFilterId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("RegexFilter"); + }); + + modelBuilder.Entity("DibariBot.Database.Models.RegexFilter", b => + { + b.Navigation("RegexChannelEntries"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/DibariBot/Migrations/PostgresMigrations/20240830053851_EmbedColorConfig.cs b/DibariBot/Migrations/PostgresMigrations/20240830053851_EmbedColorConfig.cs new file mode 100644 index 0000000..2f2cbb3 --- /dev/null +++ b/DibariBot/Migrations/PostgresMigrations/20240830053851_EmbedColorConfig.cs @@ -0,0 +1,51 @@ +using DibariBot.Database; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace DibariBot.Migrations.PostgresMigrations +{ + /// + public partial class EmbedColorConfig : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AlterColumn( + name: "Prefix", + table: "GuildConfigs", + type: "character varying(8)", + maxLength: 8, + nullable: false, + defaultValue: GuildConfig.DefaultPrefix, + oldClrType: typeof(string), + oldType: "character varying(8)", + oldMaxLength: 8, + oldNullable: true); + + migrationBuilder.AddColumn( + name: "EmbedColor", + table: "GuildConfigs", + type: "integer", + nullable: true); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "EmbedColor", + table: "GuildConfigs"); + + migrationBuilder.AlterColumn( + name: "Prefix", + table: "GuildConfigs", + type: "character varying(8)", + maxLength: 8, + nullable: true, + oldClrType: typeof(string), + oldType: "character varying(8)", + oldMaxLength: 8); + } + } +} diff --git a/DibariBot/Migrations/PostgresMigrations/PostgresqlContextModelSnapshot.cs b/DibariBot/Migrations/PostgresMigrations/PostgresqlContextModelSnapshot.cs index 692826a..f331394 100644 --- a/DibariBot/Migrations/PostgresMigrations/PostgresqlContextModelSnapshot.cs +++ b/DibariBot/Migrations/PostgresMigrations/PostgresqlContextModelSnapshot.cs @@ -1,4 +1,5 @@ // +using System; using DibariBot.Database; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; @@ -27,7 +28,11 @@ protected override void BuildModel(ModelBuilder modelBuilder) .ValueGeneratedOnAdd() .HasColumnType("numeric(20,0)"); + b.Property("EmbedColor") + .HasColumnType("integer"); + b.Property("Prefix") + .IsRequired() .HasMaxLength(8) .HasColumnType("character varying(8)"); diff --git a/DibariBot/Migrations/SqliteMigrations/20240830053805_EmbedColorConfig.Designer.cs b/DibariBot/Migrations/SqliteMigrations/20240830053805_EmbedColorConfig.Designer.cs new file mode 100644 index 0000000..1f90150 --- /dev/null +++ b/DibariBot/Migrations/SqliteMigrations/20240830053805_EmbedColorConfig.Designer.cs @@ -0,0 +1,134 @@ +// +using System; +using DibariBot.Database; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace DibariBot.Migrations.SqliteMigrations +{ + [DbContext(typeof(SqliteContext))] + [Migration("20240830053805_EmbedColorConfig")] + partial class EmbedColorConfig + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "8.0.8"); + + modelBuilder.Entity("DibariBot.Database.GuildConfig", b => + { + b.Property("GuildId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("EmbedColor") + .HasColumnType("INTEGER"); + + b.Property("Prefix") + .IsRequired() + .HasMaxLength(8) + .HasColumnType("TEXT"); + + b.HasKey("GuildId"); + + b.ToTable("GuildConfigs"); + }); + + modelBuilder.Entity("DibariBot.Database.Models.DefaultManga", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ChannelId") + .HasColumnType("INTEGER"); + + b.Property("GuildId") + .HasColumnType("INTEGER"); + + b.Property("Manga") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("GuildId", "ChannelId") + .IsUnique(); + + b.ToTable("DefaultMangas"); + }); + + modelBuilder.Entity("DibariBot.Database.Models.RegexChannelEntry", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ChannelId") + .HasColumnType("INTEGER"); + + b.Property("RegexFilterId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("RegexFilterId"); + + b.HasIndex("ChannelId", "RegexFilterId"); + + b.ToTable("RegexChannelEntries"); + }); + + modelBuilder.Entity("DibariBot.Database.Models.RegexFilter", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ChannelFilterScope") + .HasColumnType("INTEGER"); + + b.Property("Filter") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("FilterType") + .HasColumnType("INTEGER"); + + b.Property("GuildId") + .HasColumnType("INTEGER"); + + b.Property("Template") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("GuildId"); + + b.ToTable("RegexFilters"); + }); + + modelBuilder.Entity("DibariBot.Database.Models.RegexChannelEntry", b => + { + b.HasOne("DibariBot.Database.Models.RegexFilter", "RegexFilter") + .WithMany("RegexChannelEntries") + .HasForeignKey("RegexFilterId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("RegexFilter"); + }); + + modelBuilder.Entity("DibariBot.Database.Models.RegexFilter", b => + { + b.Navigation("RegexChannelEntries"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/DibariBot/Migrations/SqliteMigrations/20240830053805_EmbedColorConfig.cs b/DibariBot/Migrations/SqliteMigrations/20240830053805_EmbedColorConfig.cs new file mode 100644 index 0000000..8959e96 --- /dev/null +++ b/DibariBot/Migrations/SqliteMigrations/20240830053805_EmbedColorConfig.cs @@ -0,0 +1,51 @@ +using DibariBot.Database; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace DibariBot.Migrations.SqliteMigrations +{ + /// + public partial class EmbedColorConfig : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AlterColumn( + name: "Prefix", + table: "GuildConfigs", + type: "TEXT", + maxLength: 8, + nullable: false, + defaultValue: GuildConfig.DefaultPrefix, + oldClrType: typeof(string), + oldType: "TEXT", + oldMaxLength: 8, + oldNullable: true); + + migrationBuilder.AddColumn( + name: "EmbedColor", + table: "GuildConfigs", + type: "INTEGER", + nullable: true); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "EmbedColor", + table: "GuildConfigs"); + + migrationBuilder.AlterColumn( + name: "Prefix", + table: "GuildConfigs", + type: "TEXT", + maxLength: 8, + nullable: true, + oldClrType: typeof(string), + oldType: "TEXT", + oldMaxLength: 8); + } + } +} diff --git a/DibariBot/Migrations/SqliteMigrations/SqliteContextModelSnapshot.cs b/DibariBot/Migrations/SqliteMigrations/SqliteContextModelSnapshot.cs index 36af9b5..1dfe95c 100644 --- a/DibariBot/Migrations/SqliteMigrations/SqliteContextModelSnapshot.cs +++ b/DibariBot/Migrations/SqliteMigrations/SqliteContextModelSnapshot.cs @@ -1,4 +1,5 @@ // +using System; using DibariBot.Database; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; @@ -22,7 +23,11 @@ protected override void BuildModel(ModelBuilder modelBuilder) .ValueGeneratedOnAdd() .HasColumnType("INTEGER"); + b.Property("EmbedColor") + .HasColumnType("INTEGER"); + b.Property("Prefix") + .IsRequired() .HasMaxLength(8) .HasColumnType("TEXT"); diff --git a/DibariBot/Modules/About/AboutModule.cs b/DibariBot/Modules/About/AboutModule.cs index 1824c5b..d1179fd 100644 --- a/DibariBot/Modules/About/AboutModule.cs +++ b/DibariBot/Modules/About/AboutModule.cs @@ -12,7 +12,7 @@ public async Task AboutSlash() { await DeferAsync(); - var contents = aboutService.GetMessageContents(await AboutService.GetPlaceholders(Context.Client), Context.User.Id); + var contents = await aboutService.GetMessageContents(await AboutService.GetPlaceholders(Context.Client), Context.User.Id, Context.Guild); await FollowupAsync(contents); } diff --git a/DibariBot/Modules/About/AboutPrefixModule.cs b/DibariBot/Modules/About/AboutPrefixModule.cs index b5bae6f..f5f61c1 100644 --- a/DibariBot/Modules/About/AboutPrefixModule.cs +++ b/DibariBot/Modules/About/AboutPrefixModule.cs @@ -2,15 +2,8 @@ namespace DibariBot.Modules.About; -public class AboutPrefixModule : PrefixModule +public class AboutPrefixModule(AboutService aboutService) : PrefixModule { - private readonly AboutService aboutService; - - public AboutPrefixModule(AboutService aboutService) - { - this.aboutService = aboutService; - } - [Command("about")] [ParentModulePrefix(typeof(AboutModule))] public async Task AboutCommand() @@ -20,7 +13,7 @@ public async Task AboutCommand() await DeferAsync(); - var contents = aboutService.GetMessageContents(await AboutService.GetPlaceholders(Context.Client), Context.User.Id); + var contents = await aboutService.GetMessageContents(await AboutService.GetPlaceholders(Context.Client), Context.User.Id, Context.Guild); await ReplyAsync(contents); } diff --git a/DibariBot/Modules/About/AboutService.cs b/DibariBot/Modules/About/AboutService.cs index 0216ce7..48244a6 100644 --- a/DibariBot/Modules/About/AboutService.cs +++ b/DibariBot/Modules/About/AboutService.cs @@ -1,13 +1,15 @@ -namespace DibariBot.Modules.About; +using Discord; + +namespace DibariBot.Modules.About; [Inject(Microsoft.Extensions.DependencyInjection.ServiceLifetime.Singleton)] -public class AboutService(BotConfig botConfig) +public class AboutService(BotConfig botConfig, ColorProvider colorProvider) { /// Array of KVPs, with key as the phrase to replace and value as what to replace with. /// Recommended to use with . /// The caller's User ID. /// The user ID is used to determine if they should see the manager controls. - public MessageContents GetMessageContents(KeyValuePair[] placeholders, ulong userId) + public async Task GetMessageContents(KeyValuePair[] placeholders, ulong userId, IGuild? guild) { var fields = new EmbedFieldBuilder[botConfig.AboutPageFields.Length]; for (int i = 0; i < fields.Length; i++) @@ -29,7 +31,7 @@ public MessageContents GetMessageContents(KeyValuePair[] placeho var embed = new EmbedBuilder() .WithAuthor(ReplacePlaceholders(botConfig.AboutPageTitle, placeholders)) .WithDescription(ReplacePlaceholders(botConfig.AboutPageDescription, placeholders)) - .WithColor(CommandResult.Default) + .WithColor(await colorProvider.GetEmbedColor(guild)) .WithFields(fields); return new MessageContents(string.Empty, embed.Build(), components); diff --git a/DibariBot/Modules/ConfigCommand/ConfigCommandModule.cs b/DibariBot/Modules/ConfigCommand/ConfigCommandModule.cs index 8a87dd7..12502cb 100644 --- a/DibariBot/Modules/ConfigCommand/ConfigCommandModule.cs +++ b/DibariBot/Modules/ConfigCommand/ConfigCommandModule.cs @@ -13,9 +13,9 @@ public async Task SelectInteraction(string id) { await DeferAsync(); - var page = StateSerializer.DeserializeObject(id)!; + var page = StateSerializer.DeserializeObject(id); - await ModifyOriginalResponseAsync(await configService.GetMessageContents(new(page: page, data: ""), Context)); + await ModifyOriginalResponseAsync(await configService.GetMessageContents(new ConfigCommandService.State(page: page, data: ""), Context)); } [ComponentInteraction(ModulePrefixes.CONFIG_PAGE_SELECT_PAGE_BUTTON + "*")] @@ -29,6 +29,6 @@ public async Task ConfigSlash() { await DeferAsync(); - await FollowupAsync(await configService.GetMessageContents(new(), Context)); + await FollowupAsync(await configService.GetMessageContents(new ConfigCommandService.State(), Context)); } } diff --git a/DibariBot/Modules/ConfigCommand/ConfigCommandService.cs b/DibariBot/Modules/ConfigCommand/ConfigCommandService.cs index 227d173..a6c1831 100644 --- a/DibariBot/Modules/ConfigCommand/ConfigCommandService.cs +++ b/DibariBot/Modules/ConfigCommand/ConfigCommandService.cs @@ -1,55 +1,106 @@ -using DibariBot.Modules.ConfigCommand.Pages; -using System.Reflection; +using System.Reflection; namespace DibariBot.Modules.ConfigCommand; public class ConfigCommandService(IServiceProvider services) { - public struct State + public struct State(Page page, string data) { - public ConfigPage.Page page; - public string data; + public Page page = page; + public string data = data; public State() - { - page = default; - data = string.Empty; - } + : this(default, string.Empty) { } - public State(ConfigPage.Page page) : this() + public State(Page page) + : this() { this.page = page; data = string.Empty; } - - public State(ConfigPage.Page page, string data) - { - this.page = page; - this.data = data; - } } - public Dictionary ConfigPages + public ConfigPageDefinition[] ConfigPages { - get - { - return configPages ??= GetConfigPages(services); - } + get { return configPages ??= GetConfigPageDefinitions(); } } - private Dictionary? configPages; + private ConfigPageDefinition[]? configPages; public async Task GetMessageContents(State state, IInteractionContext context) { - var page = ConfigPages[state.page]; + var page = ConfigPages.First(x => x.ConfigPageAttribute.Id == state.page); + + var instancedPage = (IConfigPage)ActivatorUtilities.CreateInstance(services, page.PageType); - var method = page.GetType().GetMethod("SetContext", BindingFlags.Instance | BindingFlags.NonPublic) ?? throw new NullReferenceException("SetContext doesnt exist!"); - method.Invoke(page, new object[] { context }); + var method = + instancedPage + .GetType() + .GetMethod("SetContext", BindingFlags.Instance | BindingFlags.NonPublic) + ?? throw new NullReferenceException("SetContext doesn't exist!"); + method.Invoke(instancedPage, [context]); - return await page.GetMessageContents(state); + return await instancedPage.GetMessageContents(state); } - public static Dictionary GetConfigPages(IServiceProvider services) + public static ConfigPageDefinition[] GetConfigPageDefinitions() { - return services.GetServices().ToDictionary(type => type.Id); + var assembly = Assembly.GetExecutingAssembly(); + + var types = assembly + .GetTypes() + .Select(t => new + { + PageType = t, + ConfigPageAttribute = t.GetCustomAttributes(true) + .FirstOrDefault(), + }) + .Where(t1 => t1.ConfigPageAttribute != null) + // avoiding nullable issues + .Select(x => new ConfigPageDefinition + { + ConfigPageAttribute = x.ConfigPageAttribute!, + PageType = x.PageType, + }) + .OrderBy(x => x.ConfigPageAttribute.Id) + .ToArray(); + + return types; } -} \ No newline at end of file + + public SelectMenuBuilder GetPageSelectDropdown(Page id, bool isDm) + { + var dropdown = new SelectMenuBuilder().WithCustomId(ModulePrefixes.CONFIG_PAGE_SELECT_PAGE); + + foreach (var page in FilteredConfigPages(isDm)) + { + dropdown.AddOption( + new SelectMenuOptionBuilder() + .WithLabel(page.ConfigPageAttribute.Label) + .WithValue(StateSerializer.SerializeObject(page.ConfigPageAttribute.Id)) + .WithDefault(page.ConfigPageAttribute.Id.Equals(id)) + .WithDescription(page.ConfigPageAttribute.Description) + ); + } + + return dropdown; + } + + public IEnumerable FilteredConfigPages(bool isDm) => + ConfigPages.Where(page => + { + var passes = true; + + if (passes && page.ConfigPageAttribute.Conditions.HasFlag(Conditions.NotInDm)) + { + passes = !isDm; + } + + return passes; + }); +} + +public class ConfigPageDefinition +{ + public required Type PageType { get; set; } + public required ConfigPageAttribute ConfigPageAttribute { get; set; } +} diff --git a/DibariBot/Modules/ConfigCommand/ConfigPageAttribute.cs b/DibariBot/Modules/ConfigCommand/ConfigPageAttribute.cs new file mode 100644 index 0000000..c66de66 --- /dev/null +++ b/DibariBot/Modules/ConfigCommand/ConfigPageAttribute.cs @@ -0,0 +1,26 @@ +namespace DibariBot.Modules.ConfigCommand; + +[AttributeUsage(AttributeTargets.Class, Inherited = false)] +public sealed class ConfigPageAttribute(Page id, string label, string description, Conditions conditions) : Attribute +{ + public Page Id => id; + public string Label => label; + public string Description => description; + public Conditions Conditions => conditions; +} + +public enum Page +{ + Help, + DefaultManga, + RegexFilters, + Prefix, + Appearance +} + +[Flags] +public enum Conditions +{ + None = 0, + NotInDm = 1, +} \ No newline at end of file diff --git a/DibariBot/Modules/ConfigCommand/IConfigPage.cs b/DibariBot/Modules/ConfigCommand/IConfigPage.cs new file mode 100644 index 0000000..ad1b863 --- /dev/null +++ b/DibariBot/Modules/ConfigCommand/IConfigPage.cs @@ -0,0 +1,6 @@ +namespace DibariBot.Modules.ConfigCommand; + +public interface IConfigPage +{ + public Task GetMessageContents(ConfigCommandService.State state); +} \ No newline at end of file diff --git a/DibariBot/Modules/ConfigCommand/Pages/AppearancePage.cs b/DibariBot/Modules/ConfigCommand/Pages/AppearancePage.cs new file mode 100644 index 0000000..7939caf --- /dev/null +++ b/DibariBot/Modules/ConfigCommand/Pages/AppearancePage.cs @@ -0,0 +1,130 @@ +using System.Diagnostics.Contracts; +using System.Text.RegularExpressions; +using DibariBot.Database; +using Discord; +using Discord.Interactions; + +namespace DibariBot.Modules.ConfigCommand.Pages; + +[ConfigPage(Id, "Appearance", "How Bot messages will look.", Conditions.NotInDm)] +public partial class AppearancePage(ConfigCommandService configCommandService, DbService dbService, ColorProvider colorProvider, BotConfig config) + : BotModule, + IConfigPage +{ + public class SetColorRegex : IModal + { + public string Title => "Set Embed Color"; + + [ModalTextInput( + customId: ModulePrefixes.CONFIG_APPEARANCE_CHANGE_COLOR_MODAL_COLOR_TEXTBOX, + minLength: 0, + maxLength: 20, + placeholder: "#FFFFFF" + )] + public string Color { get; set; } = ""; + } + + public const Page Id = Page.Appearance; + + public Task GetMessageContents(ConfigCommandService.State state) => + GetMessageContents(); + + public async Task GetMessageContents() + { + await using var context = dbService.GetDbContext(); + + var guildConfig = await context.GetGuildConfig(Context.Guild.Id); + + var eb = new EmbedBuilder() + .WithFields( + new EmbedFieldBuilder() + .WithName("Embed Color") + .WithValue(guildConfig.EmbedColor.HasValue ? $"`#{guildConfig.EmbedColor:X6}`" : $"Default (`#{config.DefaultEmbedColor:X6}`)") + ) + .WithColor(colorProvider.GetEmbedColor(guildConfig)); + + var components = new ComponentBuilder() + .WithSelectMenu(configCommandService.GetPageSelectDropdown(Id, IsDm())) + .WithButton( + "Embed Color", + ModulePrefixes.CONFIG_APPEARANCE_CHANGE_COLOR_BUTTON, + ButtonStyle.Secondary + ) + .WithRedButton(); + + return new MessageContents(eb, components); + } + + [ComponentInteraction(ModulePrefixes.CONFIG_APPEARANCE_CHANGE_COLOR_BUTTON)] + public async Task OpenModalButton() + { + await using var context = dbService.GetDbContext(); + + var guildConfig = await context.GetGuildConfig(Context.Guild.Id); + + await RespondWithModalAsync( + ModulePrefixes.CONFIG_APPEARANCE_CHANGE_COLOR_MODAL, + modifyModal: x => + { + x.UpdateTextInput( + ModulePrefixes.CONFIG_APPEARANCE_CHANGE_COLOR_MODAL_COLOR_TEXTBOX, + i => + { + i.Value = guildConfig.EmbedColor == null ? "" : $"#{guildConfig.EmbedColor:X6}"; + i.Placeholder = $"#{config.DefaultEmbedColor}"; + i.Required = false; + } + ); + } + ); + } + + [ModalInteraction(ModulePrefixes.CONFIG_APPEARANCE_CHANGE_COLOR_MODAL)] + public async Task ModalResponse(SetColorRegex modal) + { + await DeferAsync(); + + await using var context = dbService.GetDbContext(); + + var guildConfig = await context.GetGuildConfig(Context.Guild.Id); + + // nullable structs are a PITA that i CBA to deal with in small stuff like this + var color = -1; + if(modal.Color != "") + { + try + { + var value = modal.Color; + + color = (int)ParseColorInput(value).RawValue; + } + catch + { + // we don't care why it failed to parse the color we just know it did, and in that case color will stay -1 + } + } + + if (color == -1) + { + guildConfig.EmbedColor = null; + + await context.SaveChangesAsync(); + } + else + { + guildConfig.EmbedColor = color; + + await context.SaveChangesAsync(); + } + + await ModifyOriginalResponseAsync(await GetMessageContents()); + } + + [Pure] + private static Color ParseColorInput(string input) + { + var color = System.Drawing.ColorTranslator.FromHtml(input); + + return new Color(color.R, color.G, color.B); + } +} diff --git a/DibariBot/Modules/ConfigCommand/Pages/ConfigPage.cs b/DibariBot/Modules/ConfigCommand/Pages/ConfigPage.cs deleted file mode 100644 index fe61147..0000000 --- a/DibariBot/Modules/ConfigCommand/Pages/ConfigPage.cs +++ /dev/null @@ -1,44 +0,0 @@ -namespace DibariBot.Modules.ConfigCommand.Pages; - -public abstract class ConfigPage : BotModule -{ - public enum Page - { - Help, - DefaultManga, - RegexFilters, - Prefix - } - - public abstract Page Id { get; } - public abstract string Label { get; } - public abstract string Description { get; } - public abstract bool EnabledInDMs { get; } - - // isDm is here because context isn't injected for some of these. - // TODO: move ID, label, etc. to attributes instead of abstract properties. - public bool ShouldShow(bool isDm) - { - return !isDm || EnabledInDMs; - } - - public abstract Task GetMessageContents(ConfigCommandService.State state); - - public SelectMenuBuilder GetPageSelectDropdown(Dictionary pages, Page id, bool isDm) - { - var dropdown = new SelectMenuBuilder() - .WithCustomId(ModulePrefixes.CONFIG_PAGE_SELECT_PAGE); - - foreach (var page in pages.Values.Where(page => page.ShouldShow(IsDm()))) - { - dropdown - .AddOption(new SelectMenuOptionBuilder() - .WithLabel(page.Label) - .WithValue(StateSerializer.SerializeObject(page.Id)) - .WithDefault(page.Id.Equals(id)) - .WithDescription(page.Description)); - } - - return dropdown; - } -} diff --git a/DibariBot/Modules/ConfigCommand/Pages/DefaultMangaPage.cs b/DibariBot/Modules/ConfigCommand/Pages/DefaultMangaPage.cs index 4d86af6..33344f1 100644 --- a/DibariBot/Modules/ConfigCommand/Pages/DefaultMangaPage.cs +++ b/DibariBot/Modules/ConfigCommand/Pages/DefaultMangaPage.cs @@ -2,13 +2,17 @@ using Microsoft.EntityFrameworkCore; using DibariBot.Database.Models; using Discord.Interactions; +using Discord; namespace DibariBot.Modules.ConfigCommand.Pages; [DefaultMemberPermissions(GuildPermission.ManageGuild)] [CommandContextType(InteractionContextType.Guild, InteractionContextType.BotDm, InteractionContextType.PrivateChannel)] -public class DefaultMangaPage : ConfigPage +[ConfigPage(Id, "Default manga", "Change the manga that opens when no URL is specified. Can be per-server and per-channel.", Conditions.None)] +public class DefaultMangaPage(DbService db, ConfigCommandService configCommandService, ColorProvider colorProvider) : BotModule, IConfigPage { + public const Page Id = Page.DefaultManga; + public class DefaultMangaSetModal : IModal { public string Title => "Set Default Manga - Step 1"; @@ -37,30 +41,13 @@ public ConfirmState(SeriesIdentifier series, ulong channelId) } } - public override Page Id => Page.DefaultManga; - - public override string Label => "Default manga"; - - public override string Description => "Change the manga that opens when no URL is specified. Can be per-server and per-channel."; - - public override bool EnabledInDMs => true; - - private readonly DbService dbService; - private readonly ConfigCommandService configCommandService; - - public DefaultMangaPage(DbService db, ConfigCommandService configCommandService) - { - dbService = db; - this.configCommandService = configCommandService; - } - // step 1 - help page/modal open - public override async Task GetMessageContents(ConfigCommandService.State state) + public async Task GetMessageContents(ConfigCommandService.State state) { - var embed = GetCurrentDefaultsEmbed(await GetMangaDefaultsList()); + var embed = await GetCurrentDefaultsEmbed(await GetMangaDefaultsList()); var components = new ComponentBuilder() - .WithSelectMenu(GetPageSelectDropdown(configCommandService.ConfigPages, Id, IsDm())) + .WithSelectMenu(configCommandService.GetPageSelectDropdown(Page.DefaultManga, IsDm())) .WithButton(new ButtonBuilder() .WithLabel("Set") .WithCustomId($"{ModulePrefixes.CONFIG_DEFAULT_MANGA_SET}") @@ -76,7 +63,7 @@ public override async Task GetMessageContents(ConfigCommandServ private async Task GetMangaDefaultsList() { - await using var dbContext = dbService.GetDbContext(); + await using var dbContext = db.GetDbContext(); var guildId = (Context.Guild?.Id) ?? 0ul; @@ -93,9 +80,9 @@ private async Task GetMangaDefaultsList() return defaults; } - private static EmbedBuilder GetCurrentDefaultsEmbed(DefaultManga[] defaults) + private async Task GetCurrentDefaultsEmbed(DefaultManga[] defaults) { - var embed = new EmbedBuilder().WithColor(CommandResult.Default); + var embed = new EmbedBuilder().WithColor(await colorProvider.GetEmbedColor(Context.Guild)); if (defaults.Length > 0) { @@ -119,7 +106,7 @@ public async Task RemoveMangaDropdown(string id) ulong channelId = ulong.Parse(id); - await using var context = dbService.GetDbContext(); + await using var context = db.GetDbContext(); var guildId = Context.Guild?.Id ?? 0ul; @@ -145,7 +132,7 @@ public async Task RemoveMangaButton() var defaults = await GetMangaDefaultsList(); - var embed = GetCurrentDefaultsEmbed(defaults); + var embed = await GetCurrentDefaultsEmbed(defaults); var cancelButton = new ButtonBuilder() .WithLabel("Cancel") .WithStyle(ButtonStyle.Danger) @@ -202,7 +189,7 @@ public async Task OnModalResponse(DefaultMangaSetModal modal) { var errorEmbed = new EmbedBuilder() .WithDescription("Unsupported/invalid URL. Please make sure you're using a link that is supported by the bot.") - .WithColor(CommandResult.Failure); + .WithColor(await colorProvider.GetEmbedColor(Context.Guild)); await ModifyOriginalResponseAsync(new MessageContents(string.Empty, errorEmbed.Build(), null)); return; @@ -214,7 +201,7 @@ public async Task OnModalResponse(DefaultMangaSetModal modal) channelId = Context.Channel.Id; } - await ModifyOriginalResponseAsync(ConfirmPromptContents(new ConfirmState(parsedUrl.Value, channelId))); + await ModifyOriginalResponseAsync(await ConfirmPromptContents(new ConfirmState(parsedUrl.Value, channelId))); } [ComponentInteraction(ModulePrefixes.CONFIG_DEFAULT_MANGA_SET_CHANNEL_INPUT + "*")] @@ -223,16 +210,16 @@ public async Task OnChannelSet(string id, IChannel[] channel) // should be doing UpdateAsync but i have no clue how to get that kekw await DeferAsync(); await ModifyOriginalResponseAsync( - ConfirmPromptContents( + await ConfirmPromptContents( new ConfirmState(StateSerializer.DeserializeObject(id), channel.Length > 0 ? channel[0].Id : 0ul))); } - private MessageContents ConfirmPromptContents(ConfirmState confirmState) + private async Task ConfirmPromptContents(ConfirmState confirmState) { var embed = new EmbedBuilder() .WithDescription($"Set the default manga for **{(confirmState.channelId == 0ul ? "the server" : $"<#{confirmState.channelId}>")}** as **{confirmState.series}**?") - .WithColor(CommandResult.Default); + .WithColor(await colorProvider.GetEmbedColor(Context.Guild)); var components = new ComponentBuilder(); @@ -280,7 +267,7 @@ public async Task OnConfirmed(string id) // TODO: keep an eye on this to see if they implement it // Why is this not a thing yet: https://github.com/dotnet/efcore/issues/4526 - await using (var context = dbService.GetDbContext()) + await using (var context = db.GetDbContext()) { var exists = await context.DefaultMangas.FirstOrDefaultAsync(x => x.GuildId == toAdd.GuildId && x.ChannelId == toAdd.ChannelId); diff --git a/DibariBot/Modules/ConfigCommand/Pages/HomePage.cs b/DibariBot/Modules/ConfigCommand/Pages/HomePage.cs index 4662629..fa2ab37 100644 --- a/DibariBot/Modules/ConfigCommand/Pages/HomePage.cs +++ b/DibariBot/Modules/ConfigCommand/Pages/HomePage.cs @@ -1,29 +1,24 @@ -namespace DibariBot.Modules.ConfigCommand.Pages; +using Discord; -public class HomePage(ConfigCommandService configCommandService) : ConfigPage -{ - public override Page Id => Page.Help; - - public override string Label => "Help"; - - public override string Description => "Brings up information about each config page."; +namespace DibariBot.Modules.ConfigCommand.Pages; - public override bool EnabledInDMs => true; - - public override Task GetMessageContents(ConfigCommandService.State state) +[ConfigPage(Page.Help, "Help", "Brings up information about each config page.", Conditions.None)] +public class HomePage(ConfigCommandService configCommandService, ColorProvider colorProvider) : BotModule, IConfigPage +{ + public async Task GetMessageContents(ConfigCommandService.State state) { var embed = new EmbedBuilder() - .WithColor(CommandResult.Default); + .WithColor(await colorProvider.GetEmbedColor(Context.Guild)); - foreach (var page in configCommandService.ConfigPages.Values.Where(page => page.ShouldShow(IsDm()))) + foreach (var page in configCommandService.FilteredConfigPages(IsDm())) { - embed.AddField(page.Label, page.Description); + embed.AddField(page.ConfigPageAttribute.Label, page.ConfigPageAttribute.Description); } var components = new ComponentBuilder() - .WithSelectMenu(GetPageSelectDropdown(configCommandService.ConfigPages, Id, IsDm())) + .WithSelectMenu(configCommandService.GetPageSelectDropdown(Page.Help, IsDm())) .WithRedButton(); - return Task.FromResult(new MessageContents("", embed.Build(), components)); + return await Task.FromResult(new MessageContents("", embed.Build(), components)); } } diff --git a/DibariBot/Modules/ConfigCommand/Pages/PrefixPage.cs b/DibariBot/Modules/ConfigCommand/Pages/PrefixPage.cs index 72e8b47..9762fd4 100644 --- a/DibariBot/Modules/ConfigCommand/Pages/PrefixPage.cs +++ b/DibariBot/Modules/ConfigCommand/Pages/PrefixPage.cs @@ -1,4 +1,5 @@ using DibariBot.Database; +using Discord; using Discord.Interactions; namespace DibariBot.Modules.ConfigCommand.Pages; @@ -11,17 +12,12 @@ public class SetPrefixModal : IModal public string Prefix { get; set; } = ""; } -public class PrefixPage(ConfigCommandService configCommandService, DbService dbService) : ConfigPage +[ConfigPage(Id, "Prefix", "What prefix to use for prefix commands.", Conditions.NotInDm)] +public class PrefixPage(ConfigCommandService configCommandService, DbService dbService, ColorProvider colorProvider) : BotModule, IConfigPage { - public override Page Id => Page.Prefix; - - public override string Label => "Prefix"; + public const Page Id = Page.Prefix; - public override string Description => "What prefix to use for prefix commands."; - - public override bool EnabledInDMs => false; - - public override async Task GetMessageContents(ConfigCommandService.State state) + public async Task GetMessageContents(ConfigCommandService.State state) { await using var dbContext = dbService.GetDbContext(); @@ -32,10 +28,10 @@ public override async Task GetMessageContents(ConfigCommandServ .WithFields(new EmbedFieldBuilder() .WithName("Prefix") .WithValue($"`{prefix}`")) - .WithColor(CommandResult.Default); + .WithColor(colorProvider.GetEmbedColor(config)); var components = new ComponentBuilder() - .WithSelectMenu(GetPageSelectDropdown(configCommandService.ConfigPages, Id, IsDm())) + .WithSelectMenu(configCommandService.GetPageSelectDropdown(Id, IsDm())) .WithButton("Change Prefix", ModulePrefixes.CONFIG_PREFIX_MODAL_BUTTON, ButtonStyle.Secondary) .WithRedButton(); diff --git a/DibariBot/Modules/ConfigCommand/Pages/RegexFiltersPage.cs b/DibariBot/Modules/ConfigCommand/Pages/RegexFiltersPage.cs index c67a30a..ba02689 100644 --- a/DibariBot/Modules/ConfigCommand/Pages/RegexFiltersPage.cs +++ b/DibariBot/Modules/ConfigCommand/Pages/RegexFiltersPage.cs @@ -8,10 +8,12 @@ namespace DibariBot.Modules.ConfigCommand.Pages; [DefaultMemberPermissions(GuildPermission.ManageGuild)] +[ConfigPage(Id, "Regex filters", "Specify filters to limit what mangas can be pulled up.", Conditions.NotInDm)] public partial class RegexFiltersPage( MangaService mangaService, - ConfigCommandService configCommandService) - : ConfigPage + ConfigCommandService configCommandService, + ColorProvider colorProvider) + : BotModule, IConfigPage { public class SetRegexModal : IModal { @@ -24,13 +26,7 @@ public class SetRegexModal : IModal public string Filter { get; set; } = ""; } - public override Page Id => Page.RegexFilters; - - public override string Label => "Regex filters"; - - public override string Description => "Specify filters to limit what mangas can be pulled up."; - - public override bool EnabledInDMs => false; + public const Page Id = Page.RegexFilters; private const string EMBED_NAME_TEMPLATE = "Template"; private const string EMBED_NAME_FILTER = "Filter"; @@ -38,9 +34,9 @@ public class SetRegexModal : IModal private const string EMBED_NAME_FILTER_TYPE = "Filter Type"; private const string EMBED_NAME_SCOPE = "Channel Scope"; - public override async Task GetMessageContents(ConfigCommandService.State state) + public async Task GetMessageContents(ConfigCommandService.State state) { - var embed = new EmbedBuilder().WithColor(CommandResult.Default); + var embed = new EmbedBuilder().WithColor(await colorProvider.GetEmbedColor(Context.Guild)); var filters = await mangaService.GetFilters(Context.Guild.Id); @@ -86,7 +82,7 @@ public override async Task GetMessageContents(ConfigCommandServ } var components = new ComponentBuilder() - .WithSelectMenu(GetPageSelectDropdown(configCommandService.ConfigPages, Id, IsDm())) + .WithSelectMenu(configCommandService.GetPageSelectDropdown(Id, IsDm())) .WithButton(new ButtonBuilder() .WithLabel("Add") .WithCustomId($"{ModulePrefixes.CONFIG_FILTERS_OPEN_MODAL_BUTTON}{0ul}") @@ -178,7 +174,7 @@ private async Task EditFilterSelectChanged(string id) return; } - await ModifyOriginalResponseAsync(UpsertConfirmation(filter)); + await ModifyOriginalResponseAsync(await UpsertConfirmation(filter)); } [ComponentInteraction(ModulePrefixes.CONFIG_FILTERS_OPEN_MODAL_BUTTON + "*")] @@ -232,7 +228,7 @@ public async Task ModalResponse(bool existing, SetRegexModal modal) filterType: FilterType.Block, channelFilterScope: ChannelFilterScope.Exclude ); - var contents = UpsertConfirmation(filter); + var contents = await UpsertConfirmation(filter); await ModifyOriginalResponseAsync(contents); } @@ -248,17 +244,20 @@ private async Task ConfirmationAddButton() if (newId == 0ul) { - await FollowupAsync("Just noting nothing was actually added or changed as the filter no longer exists.", - ephemeral: true); + var errorEmbed = new EmbedBuilder() + .WithDescription("Just noting nothing was actually added or changed as the filter no longer exists.") + .WithColor(colorProvider.GetErrorEmbedColor()); + + await FollowupAsync(new MessageContents(errorEmbed), ephemeral: true); } await ModifyOriginalResponseAsync(await configCommandService.GetMessageContents( new ConfigCommandService.State(page: Id), Context)); } - private MessageContents UpsertConfirmation(RegexFilter filter) + private async Task UpsertConfirmation(RegexFilter filter) { - var embed = new EmbedBuilder().WithColor(CommandResult.Default); + var embed = new EmbedBuilder().WithColor(await colorProvider.GetEmbedColor(Context.Guild)); var channels = filter.RegexChannelEntries; @@ -364,7 +363,7 @@ private async Task UpsertConfirmationChannelSelectChanged(IChannel[] channels) var filter = await GetRegexFilterFromContext(channels.Select(x => new RegexChannelEntry { ChannelId = x.Id}).ToList()); - await ModifyOriginalResponseAsync(UpsertConfirmation(filter)); + await ModifyOriginalResponseAsync(await UpsertConfirmation(filter)); } [ComponentInteraction(ModulePrefixes.CONFIG_FILTERS_CONFIRMATION_FILTER_TYPE)] @@ -376,7 +375,7 @@ private async Task UpsertConfirmationFilterTypeChanged(string id) filter.FilterType = StateSerializer.DeserializeObject(id); - await ModifyOriginalResponseAsync(UpsertConfirmation(filter)); + await ModifyOriginalResponseAsync(await UpsertConfirmation(filter)); } [ComponentInteraction(ModulePrefixes.CONFIG_FILTERS_CONFIRMATION_CHANNEL_SCOPE)] @@ -388,7 +387,7 @@ private async Task UpsertConfirmationChannelScopeChanged(string id) filter.ChannelFilterScope = StateSerializer.DeserializeObject(id); - await ModifyOriginalResponseAsync(UpsertConfirmation(filter)); + await ModifyOriginalResponseAsync(await UpsertConfirmation(filter)); } private async Task GetRegexFilterFromContext(List? channels = null) diff --git a/DibariBot/Modules/Core/BotModule.cs b/DibariBot/Modules/Core/BotModule.cs index d1f5ada..0437def 100644 --- a/DibariBot/Modules/Core/BotModule.cs +++ b/DibariBot/Modules/Core/BotModule.cs @@ -1,4 +1,5 @@ -using Discord.Interactions; +using DibariBot.Database; +using Discord.Interactions; using Discord.WebSocket; namespace DibariBot; diff --git a/DibariBot/Modules/Core/ColorProvider.cs b/DibariBot/Modules/Core/ColorProvider.cs new file mode 100644 index 0000000..4ec2963 --- /dev/null +++ b/DibariBot/Modules/Core/ColorProvider.cs @@ -0,0 +1,32 @@ +using DibariBot.Database; + +namespace DibariBot.Modules; + +[Inject(ServiceLifetime.Singleton)] +public class ColorProvider(BotConfig config, DbService dbService) +{ + public async Task GetEmbedColor(IGuild? guild) + { + await using var dbContext = dbService.GetDbContext(); + + return await GetEmbedColor(dbContext, guild); + } + + public async Task GetEmbedColor(BotDbContext dbContext, IGuild? guild) + { + var guildConfig = guild == null ? null : await dbContext.GetGuildConfig(guild.Id); + + return GetEmbedColor(guildConfig); + } + + public Color GetEmbedColor(GuildConfig? guildConfig) + { + // the compiler doesn't complain with the const here for some reason? + return guildConfig?.EmbedColor == null ? new Color((uint)config.DefaultEmbedColor) : new Color((uint)guildConfig.EmbedColor); + } + + public Color GetErrorEmbedColor() + { + return (uint)config.ErrorEmbedColor; + } +} \ No newline at end of file diff --git a/DibariBot/Modules/Core/MessageContents.cs b/DibariBot/Modules/Core/MessageContents.cs index 3d9f35f..f5a3642 100644 --- a/DibariBot/Modules/Core/MessageContents.cs +++ b/DibariBot/Modules/Core/MessageContents.cs @@ -51,4 +51,4 @@ public MessageContents SetEmbed(Embed embed) return this; } -} +} \ No newline at end of file diff --git a/DibariBot/Modules/Core/ModulePrefixes.cs b/DibariBot/Modules/Core/ModulePrefixes.cs index b87fbed..4a433f6 100644 --- a/DibariBot/Modules/Core/ModulePrefixes.cs +++ b/DibariBot/Modules/Core/ModulePrefixes.cs @@ -75,10 +75,22 @@ public static class ModulePrefixes public const string CONFIG_FILTERS_REMOVE_BASE = $"{CONFIG_FILTERS}r"; public const string CONFIG_FILTERS_REMOVE_BUTTON = $"{CONFIG_FILTERS_REMOVE_BASE}:"; public const string CONFIG_FILTERS_REMOVE_FILTER_SELECT = $"{CONFIG_FILTERS_REMOVE_BASE}-s:"; - + public const string CONFIG_FILTERS_EDIT_BASE = $"{CONFIG_FILTERS}e"; public const string CONFIG_FILTERS_EDIT_BUTTON = $"{CONFIG_FILTERS_EDIT_BASE}:"; public const string CONFIG_FILTERS_EDIT_FILTER_SELECT = $"{CONFIG_FILTERS_EDIT_BASE}-s:"; #endregion + + #region Config - Appearance + + public const string CONFIG_APPEARANCE = "c-a"; + + public const string CONFIG_APPEARANCE_CHANGE_COLOR_BASE = $"{CONFIG_APPEARANCE}-cc"; + public const string CONFIG_APPEARANCE_CHANGE_COLOR_BUTTON = $"{CONFIG_APPEARANCE_CHANGE_COLOR_BASE}-b:"; + + public const string CONFIG_APPEARANCE_CHANGE_COLOR_MODAL = $"{CONFIG_APPEARANCE_CHANGE_COLOR_BASE}m"; + public const string CONFIG_APPEARANCE_CHANGE_COLOR_MODAL_COLOR_TEXTBOX = $"{CONFIG_APPEARANCE_CHANGE_COLOR_MODAL}ct:"; + + #endregion } diff --git a/DibariBot/Modules/Core/StateSerializer.cs b/DibariBot/Modules/Core/StateSerializer.cs index 8909797..e2fee7d 100644 --- a/DibariBot/Modules/Core/StateSerializer.cs +++ b/DibariBot/Modules/Core/StateSerializer.cs @@ -8,6 +8,7 @@ namespace DibariBot; // > because that order varies. However, starting with .NET 7, the ordering is deterministic based upon the metadata ordering in the assembly. // https://learn.microsoft.com/en-us/dotnet/api/system.type.getfields?view=net-7.0 // cant make this up, this code is only deterministic in net 7 lol. prefer this new system to alphabetically sorting it or whatever though +// TODO: Move this to a CSV-based implementation public static class StateSerializer { const char SEPARATOR = '|'; diff --git a/DibariBot/Modules/Help/HelpModule.cs b/DibariBot/Modules/Help/HelpModule.cs index c16cb0e..107a32e 100644 --- a/DibariBot/Modules/Help/HelpModule.cs +++ b/DibariBot/Modules/Help/HelpModule.cs @@ -3,10 +3,13 @@ namespace DibariBot.Modules.Help; -[CommandContextType(InteractionContextType.Guild, InteractionContextType.BotDm, InteractionContextType.PrivateChannel)] +[CommandContextType( + InteractionContextType.Guild, + InteractionContextType.BotDm, + InteractionContextType.PrivateChannel +)] [IntegrationType(ApplicationIntegrationType.GuildInstall, ApplicationIntegrationType.UserInstall)] -public class HelpModule(DbService dbService, LazyHelpService helpService) - : BotModule +public class HelpModule(DbService dbService, LazyHelpService helpService) : BotModule { [SlashCommand("help", "Help! What are all the commands?")] [HelpPageDescription("Pulls up this page!")] @@ -14,12 +17,17 @@ public async Task HelpSlash() { await DeferAsync(); - await using var dbContext = dbService.GetDbContext(); + string prefix = GuildConfig.DefaultPrefix; + GuildConfig? config = null; + if (Context.Guild != null) + { + await using var dbContext = dbService.GetDbContext(); + config = await dbContext.GetGuildConfig(Context.Guild.Id); - var config = await dbContext.GetGuildConfig(Context.Guild.Id); + prefix = config.Prefix; + } - var prefix = Context.Guild != null ? config.Prefix : GuildConfig.DefaultPrefix; - var contents = helpService.GetMessageContents(prefix); + MessageContents contents = helpService.GetMessageContents(prefix, config); await FollowupAsync(contents); } diff --git a/DibariBot/Modules/Help/HelpPrefixModule.cs b/DibariBot/Modules/Help/HelpPrefixModule.cs index 7f728f2..73f7792 100644 --- a/DibariBot/Modules/Help/HelpPrefixModule.cs +++ b/DibariBot/Modules/Help/HelpPrefixModule.cs @@ -16,13 +16,17 @@ public async Task HelpCommand() await DeferAsync(); - await using var dbContext = dbService.GetDbContext(); + string prefix = GuildConfig.DefaultPrefix; + GuildConfig? config = null; + if (Context.Guild != null) + { + await using var dbContext = dbService.GetDbContext(); + config = await dbContext.GetGuildConfig(Context.Guild.Id); + prefix = config.Prefix; + } - var config = Context.Guild != null ? await dbContext.GetGuildConfig(Context.Guild.Id) : null; - - var prefix = Context.Guild != null ? config!.Prefix : GuildConfig.DefaultPrefix; - var contents = helpService.GetMessageContents(prefix); + MessageContents contents = helpService.GetMessageContents(prefix, config); await ReplyAsync(contents); } diff --git a/DibariBot/Modules/Help/LazyHelpService.cs b/DibariBot/Modules/Help/LazyHelpService.cs index 46a163d..c475528 100644 --- a/DibariBot/Modules/Help/LazyHelpService.cs +++ b/DibariBot/Modules/Help/LazyHelpService.cs @@ -1,9 +1,12 @@ -namespace DibariBot.Modules.Help; +using DibariBot.Database; +using Discord; + +namespace DibariBot.Modules.Help; [Inject(ServiceLifetime.Singleton)] -public class LazyHelpService +public class LazyHelpService(ColorProvider colorProvider) { - public MessageContents GetMessageContents(string prefix) + public MessageContents GetMessageContents(string prefix, GuildConfig? guild) { var eb = new EmbedBuilder() .WithDescription("Noting that any prefix command parameter wrapped in square brackets is an optional named parameter." + @@ -36,7 +39,7 @@ public MessageContents GetMessageContents(string prefix) .WithValue($"Pulls up into about the bot\n**Prefix versions**\n`{prefix}about`"), ]) - .WithColor(CommandResult.Default); + .WithColor(colorProvider.GetEmbedColor(guild)); return new MessageContents(eb); } diff --git a/DibariBot/Modules/MDSearch/SearchModule.cs b/DibariBot/Modules/MDSearch/SearchModule.cs index 4b5a16a..ae7b9bb 100644 --- a/DibariBot/Modules/MDSearch/SearchModule.cs +++ b/DibariBot/Modules/MDSearch/SearchModule.cs @@ -14,7 +14,7 @@ public async Task SearchSlash([Summary(description: "The manga to search for.")] { await DeferAsync(ephemeral); - await FollowupAsync(await search.GetMessageContents(new SearchService.State() { query = query, isSpoiler = spoiler})); + await FollowupAsync(await search.GetMessageContents(new SearchService.State() { query = query, isSpoiler = spoiler}, Context.Guild)); } [ComponentInteraction(ModulePrefixes.MANGADEX_SEARCH_BUTTON_PREFIX + "*")] @@ -24,7 +24,7 @@ public async Task SearchButtonInteraction(string id) var state = StateSerializer.DeserializeObject(id); - await ModifyOriginalResponseAsync(await search.GetMessageContents(state)); + await ModifyOriginalResponseAsync(await search.GetMessageContents(state, Context.Guild)); } [ComponentInteraction(ModulePrefixes.MANGADEX_SEARCH_DROPDOWN_PREFIX + "*")] diff --git a/DibariBot/Modules/MDSearch/SearchPrefixModule.cs b/DibariBot/Modules/MDSearch/SearchPrefixModule.cs index 804b6c0..ba95358 100644 --- a/DibariBot/Modules/MDSearch/SearchPrefixModule.cs +++ b/DibariBot/Modules/MDSearch/SearchPrefixModule.cs @@ -21,7 +21,7 @@ public async Task SearchCommand(string query, NameableArguments args) await DeferAsync(); - await ReplyAsync(await search.GetMessageContents(new SearchService.State { query = query, isSpoiler = args.Spoiler })); + await ReplyAsync(await search.GetMessageContents(new SearchService.State { query = query, isSpoiler = args.Spoiler }, Context.Guild)); } [Command("search")] @@ -34,6 +34,6 @@ public async Task SearchCommand([Remainder] string query) await DeferAsync(); - await ReplyAsync(await search.GetMessageContents(new SearchService.State { query = query, isSpoiler = false })); + await ReplyAsync(await search.GetMessageContents(new SearchService.State { query = query, isSpoiler = false }, Context.Guild)); } } \ No newline at end of file diff --git a/DibariBot/Modules/MDSearch/SearchService.cs b/DibariBot/Modules/MDSearch/SearchService.cs index 169eac1..a3db3b3 100644 --- a/DibariBot/Modules/MDSearch/SearchService.cs +++ b/DibariBot/Modules/MDSearch/SearchService.cs @@ -1,10 +1,11 @@ using System.ComponentModel; using DibariBot.Apis; +using Discord; namespace DibariBot.Modules.MDSearch; [DibariBot.Inject(Microsoft.Extensions.DependencyInjection.ServiceLifetime.Singleton)] -public class SearchService(MangaDexApi mdapi, BotConfig config) +public class SearchService(MangaDexApi mdapi, BotConfig config, ColorProvider colorProvider) { public struct State { @@ -16,7 +17,7 @@ public struct State public bool isSpoiler; } - public async Task GetMessageContents(State state) + public async Task GetMessageContents(State state, IGuild? guild) { var res = await mdapi.GetMangas(new Apis.MangaListQueryParams { @@ -35,12 +36,12 @@ public async Task GetMessageContents(State state) { var errorEmbed = new EmbedBuilder() .WithDescription("No results found!") - .WithColor(CommandResult.Default); + .WithColor(colorProvider.GetErrorEmbedColor()); return new MessageContents(string.Empty, errorEmbed.Build(), null); } - var embed = new EmbedBuilder().WithColor(CommandResult.Default); + var embed = new EmbedBuilder().WithColor(await colorProvider.GetEmbedColor(guild)); foreach (var mangaSchema in res.data) { diff --git a/DibariBot/Modules/Manga/MangaService.cs b/DibariBot/Modules/Manga/MangaService.cs index b7c00ae..57d379b 100644 --- a/DibariBot/Modules/Manga/MangaService.cs +++ b/DibariBot/Modules/Manga/MangaService.cs @@ -20,7 +20,7 @@ public enum MangaAction } [Inject(ServiceLifetime.Singleton)] -public partial class MangaService(MangaFactory mangaFactory, BotConfig botConfig, DbService dbService, ILogger logger) +public partial class MangaService(MangaFactory mangaFactory, BotConfig botConfig, DbService dbService, ILogger logger, ColorProvider colorProvider) { public struct State { @@ -55,6 +55,7 @@ public async Task MangaCommand(ulong guildId, ulong channelId, { if (string.IsNullOrWhiteSpace(url)) { + // this could probably be re-used await using var contextDefaults = dbService.GetDbContext(); var exists = @@ -67,7 +68,7 @@ public async Task MangaCommand(ulong guildId, ulong channelId, new EmbedBuilder() .WithDescription( "This server/channel hasn't got a default manga set! Please manually specify the URL.") - .WithColor(CommandResult.Failure) + .WithColor(colorProvider.GetErrorEmbedColor()) .Build(), null); } @@ -82,7 +83,7 @@ public async Task MangaCommand(ulong guildId, ulong channelId, new EmbedBuilder() .WithDescription( "Unsupported/invalid URL. Please make sure you're using a link that is supported by the bot.") - .WithColor(CommandResult.Failure) + .WithColor(colorProvider.GetErrorEmbedColor()) .Build(), null); } @@ -106,7 +107,7 @@ public async Task GetMangaMessage(ulong guildId, ulong channelI var errorEmbed = new EmbedBuilder() .WithDescription( $"Failed to get manga. `{ex.Message}`\n`{state.identifier.platform}/{state.identifier.series}`") - .WithColor(CommandResult.Failure) + .WithColor(colorProvider.GetErrorEmbedColor()) .Build(); logger.LogWarning(ex, "Failed to get manga."); @@ -116,13 +117,15 @@ public async Task GetMangaMessage(ulong guildId, ulong channelI var metadata = await manga.GetMetadata(); + await using var context = dbService.GetDbContext(); + try { - if (guildId != 0ul && !await IsMangaAllowed(guildId, channelId, metadata, manga.GetIdentifier())) + if (guildId != 0ul && !await IsMangaAllowed(guildId, channelId, metadata, manga.GetIdentifier(), context)) { var errorEmbed = new EmbedBuilder() .WithDescription("Manga disallowed by server rules.") - .WithColor(CommandResult.Failure) + .WithColor(colorProvider.GetErrorEmbedColor()) .Build(); return new MessageContents(string.Empty, errorEmbed); @@ -136,7 +139,7 @@ public async Task GetMangaMessage(ulong guildId, ulong channelI var errorEmbed = new EmbedBuilder() .WithDescription("Filter took too long to process.") - .WithColor(CommandResult.Failure) + .WithColor(colorProvider.GetErrorEmbedColor()) .Build(); return new MessageContents(string.Empty, errorEmbed, null); @@ -148,7 +151,7 @@ public async Task GetMangaMessage(ulong guildId, ulong channelI { var errorEmbed = new EmbedBuilder() .WithDescription("No chapters found.") - .WithColor(CommandResult.Failure) + .WithColor(colorProvider.GetErrorEmbedColor()) .Build(); return new MessageContents(string.Empty, errorEmbed, null); @@ -162,7 +165,7 @@ public async Task GetMangaMessage(ulong guildId, ulong channelI { var errorEmbed = new EmbedBuilder() .WithDescription("Chapter not found.") - .WithColor(CommandResult.Failure) + .WithColor(colorProvider.GetErrorEmbedColor()) .Build(); return new MessageContents(string.Empty, errorEmbed, null); @@ -203,11 +206,11 @@ public async Task GetMangaMessage(ulong guildId, ulong channelI { pages = await manga.GetImageSrcs(bookmark.chapter); } - catch(Exception ex) + catch (Exception ex) { var errorEmbed = new EmbedBuilder() .WithDescription($"Failed to get chapter {bookmark.chapter}: `{ex.Message}`") - .WithColor(CommandResult.Failure) + .WithColor(colorProvider.GetErrorEmbedColor()) .Build(); return new MessageContents(string.Empty, errorEmbed, null); @@ -217,7 +220,7 @@ public async Task GetMangaMessage(ulong guildId, ulong channelI { var errorEmbed = new EmbedBuilder() .WithDescription($"page {bookmark.page + 1} doesn't exist in chapter {bookmark.chapter}!") - .WithColor(CommandResult.Failure) + .WithColor(colorProvider.GetErrorEmbedColor()) .Build(); return new MessageContents(string.Empty, errorEmbed, null); @@ -233,6 +236,7 @@ public async Task GetMangaMessage(ulong guildId, ulong channelI bool disableLeftPage = disableLeftChapter && bookmark.page <= 0; bool disableRightPage = disableRightChapter && bookmark.page >= pages.srcs.Length; + var guildConfig = guildId == 0ul ? null : await context.GetGuildConfig(guildId); var embed = new EmbedBuilder() .WithTitle(string.IsNullOrWhiteSpace(chapterData.title) ? $"Chapter {bookmark.chapter}" : chapterData.title) @@ -244,7 +248,7 @@ public async Task GetMangaMessage(ulong guildId, ulong channelI .WithText($"{metadata.title.Truncate(50)}, by {author}.\n" + $"Group: {pages.group}") ) - .WithColor(CommandResult.Default) + .WithColor(colorProvider.GetEmbedColor(guildConfig)) .Build(); var newState = new State(MangaAction.Open, state.identifier, bookmark, state.isSpoiler); @@ -402,10 +406,8 @@ public async Task RemoveFilter(RegexFilter filter) /// ID of the guild. /// Channel ID. will only grab the filters that apply to the current channel. - public async Task GetFilters(ulong guildId, ulong channelId) + public async Task GetFilters(ulong guildId, ulong channelId, BotDbContext context) { - await using var context = dbService.GetDbContext(); - var filterQuery = context.RegexFilters .Where(rf => // The filter is for the current guild @@ -437,9 +439,9 @@ public async Task GetFilters(ulong guildId) } public async Task IsMangaAllowed(ulong guildId, ulong channelId, MangaMetadata metadata, - SeriesIdentifier identifier) + SeriesIdentifier identifier, BotDbContext context) { - var filters = await GetFilters(guildId, channelId); + var filters = await GetFilters(guildId, channelId, context); var values = new Dictionary { diff --git a/DibariBot/Startup.cs b/DibariBot/Startup.cs index 25c334f..c86b2f4 100644 --- a/DibariBot/Startup.cs +++ b/DibariBot/Startup.cs @@ -119,11 +119,6 @@ private static IServiceCollection AddBotServices(this IServiceCollection service .WithTransientLifetime() ); - serviceCollection.Scan(scan => scan.FromAssemblyOf() - .AddClasses(classes => classes.AssignableTo()) - .As() - .WithTransientLifetime()); - serviceCollection.AddHostedService(); return serviceCollection;