From f78c0d77d0532e37136a7de004282c9cb74b2e3f Mon Sep 17 00:00:00 2001 From: HEJOK254 <90698026+HEJOK254@users.noreply.github.com> Date: Tue, 17 Sep 2024 16:56:49 +0200 Subject: [PATCH 01/13] Add Microsoft.Extensions.Hosting NuGet package --- Discord QuickEdit.csproj | 1 + 1 file changed, 1 insertion(+) diff --git a/Discord QuickEdit.csproj b/Discord QuickEdit.csproj index 1eef98f..c55bda8 100644 --- a/Discord QuickEdit.csproj +++ b/Discord QuickEdit.csproj @@ -13,6 +13,7 @@ + From ce2c02258f8f5d725e0dd0eecad5899fac5a5453 Mon Sep 17 00:00:00 2001 From: HEJOK254 <90698026+HEJOK254@users.noreply.github.com> Date: Tue, 17 Sep 2024 16:57:50 +0200 Subject: [PATCH 02/13] Add Serliog.Extensions.Hosting NuGet package --- Discord QuickEdit.csproj | 1 + 1 file changed, 1 insertion(+) diff --git a/Discord QuickEdit.csproj b/Discord QuickEdit.csproj index c55bda8..e5f4f37 100644 --- a/Discord QuickEdit.csproj +++ b/Discord QuickEdit.csproj @@ -15,6 +15,7 @@ + From 7d5bf58b862a54a31a40da7df986bfdd6248196d Mon Sep 17 00:00:00 2001 From: HEJOK254 <90698026+HEJOK254@users.noreply.github.com> Date: Tue, 17 Sep 2024 23:34:40 +0200 Subject: [PATCH 03/13] Implement Generic Host Features Big commit, didn't really have the opportunity to break it up. It introduces various fixes, graceful shutdown, DI etc. Probably going to get a few fixes/refactors Implemented: - Dependency Injection - Generic Host - Configuration (Microsoft.Extensions.Configuration, switched from custom config) Changed: - Split `Program.cs` into 2 files, `Program.cs` manages the program startup, while `Bot.cs` manages the bot's lifetime. - Removed the old configuration in favour of the Microsoft.Extensions.Configuration package - And more I think Thanks to @Filip55561 for helping with the Host setup --- Bot.cs | 58 +++++++++++ Commands/InteractionServiceHandler.cs | 41 ++++---- Config/Config.cs | 13 +++ ConfigManager.cs | 35 ------- Program.cs | 137 +++++++++++++------------- 5 files changed, 163 insertions(+), 121 deletions(-) create mode 100644 Bot.cs create mode 100644 Config/Config.cs delete mode 100644 ConfigManager.cs diff --git a/Bot.cs b/Bot.cs new file mode 100644 index 0000000..c346853 --- /dev/null +++ b/Bot.cs @@ -0,0 +1,58 @@ +using System; +using System.Diagnostics; +using Discord; +using Discord.WebSocket; +using Microsoft.Extensions.Hosting; +using QuickEdit.Commands; +using QuickEdit.Logger; +using Serilog; + +namespace QuickEdit; + +internal sealed class Bot(DiscordSocketClient client, Config.DiscordConfig discordConfig, InteractionServiceHandler interactionServiceHandler, IHostApplicationLifetime appLifetime) : IHostedService +{ + private readonly DiscordSocketClient _client = client; + private readonly Config.DiscordConfig _discordConfig = discordConfig; + private readonly InteractionServiceHandler _interactionServiceHandler = interactionServiceHandler; + private readonly IHostApplicationLifetime _appLifetime = appLifetime; + + public async Task StartAsync(CancellationToken cancellationToken) + { + _client.Log += AutoLog.LogMessage; + _client.Ready += OnReadyAsync; + + await _client.LoginAsync(TokenType.Bot, _discordConfig.Token); + await _client.StartAsync(); + + // Custom activities use a different method + if (_discordConfig.StatusType == ActivityType.CustomStatus) + { + await _client.SetCustomStatusAsync(_discordConfig.Status); + } + else + { + await _client.SetGameAsync(_discordConfig.Status, null, _discordConfig.StatusType); + } + } + + public async Task StopAsync(CancellationToken cancellationToken) + { + await _client.LogoutAsync(); + await _client.StopAsync(); + } + + private async Task OnReadyAsync() + { + try + { + await _interactionServiceHandler.InitAsync(); + } + catch + { + Log.Fatal("Program is exiting due to an error in InteractionServiceHandler."); + // The program cannot continue without the InteractionService, so terminate it. Nothing important should be running at this point. + Environment.ExitCode = 1; + _appLifetime.StopApplication(); + } + } +} diff --git a/Commands/InteractionServiceHandler.cs b/Commands/InteractionServiceHandler.cs index d9863ae..adf7d1d 100644 --- a/Commands/InteractionServiceHandler.cs +++ b/Commands/InteractionServiceHandler.cs @@ -3,19 +3,30 @@ using Discord.WebSocket; using Serilog; +using QuickEdit.Config; + namespace QuickEdit.Commands; -public class InteractionServiceHandler +internal sealed class InteractionServiceHandler { - private static readonly DiscordSocketClient? _client = Program.client; - private static InteractionService? _interactionService; - private static readonly InteractionServiceConfig _interactionServiceConfig = new() { UseCompiledLambda = true, DefaultRunMode = RunMode.Async }; + private readonly DiscordSocketClient _client; + private InteractionService _interactionService; + private readonly InteractionServiceConfig _interactionServiceConfig; + private readonly Config.DiscordConfig _discordConfig; private static readonly SemaphoreSlim _initSemaphore = new(1); private static bool isReady = false; + public InteractionServiceHandler(DiscordSocketClient client, InteractionService interactionService, Config.DiscordConfig discordConfig, InteractionServiceConfig interactionServiceConfig) + { + _client = client; + _interactionService = interactionService; + _discordConfig = discordConfig; + _interactionServiceConfig = interactionServiceConfig; + } + /// /// Initialize the InteractionService /// - public static async Task InitAsync() + public async Task InitAsync() { await _initSemaphore.WaitAsync(); @@ -25,12 +36,6 @@ public static async Task InitAsync() try { - if (_interactionService != null) - { - Log.Warning("Tried to Initialize the InteractionService after it has already been initialized"); - return; - } - _interactionService = new InteractionService(_client!.Rest, _interactionServiceConfig); await RegisterModulesAsync(); @@ -41,7 +46,7 @@ public static async Task InitAsync() } catch (Exception e) { - Log.Fatal($"Error initializing InteractionService: {e.Message}"); + Log.Fatal("Error initializing InteractionService: {e}", e); throw; } finally @@ -53,7 +58,7 @@ public static async Task InitAsync() /// /// Register modules / commands /// - public static async Task RegisterModulesAsync() + public async Task RegisterModulesAsync() { // The service might not have been initialized yet if (_interactionService == null) @@ -76,12 +81,12 @@ public static async Task RegisterModulesAsync() } catch (Exception e) { - Log.Fatal($"Error registering modules: {(Program.config != null && Program.config.debug ? e : e.Message)}"); + Log.Fatal("Error registering modules:\n{}", e); throw; } } - public static async Task OnInteractionCreatedAsync(SocketInteraction interaction) + public async Task OnInteractionCreatedAsync(SocketInteraction interaction) { // The service might not have been initialized yet if (_interactionService == null) @@ -99,7 +104,7 @@ public static async Task OnInteractionCreatedAsync(SocketInteraction interaction } catch (Exception e) { - Log.Error($"Error handling interaction: {e.Message}"); + Log.Error("Error handling interaction:\n{e}", e); if (interaction.Type is InteractionType.ApplicationCommand) { @@ -118,12 +123,12 @@ public static async Task OnSlashCommandExecutedAsync(SlashCommandInfo commandInf try { - Log.Error($"Error handling interaction: {result.Error}"); + Log.Error("Error handling interaction: {result.Error}", result); await interactionContext.Interaction.FollowupAsync("An error occurred while executing the command.", ephemeral: true); } catch (Exception e) { - Log.Error($"Error handling interaction exception bruh: {e.ToString()}"); + Log.Error("Error handling interaction exception bruh:\n{e}", e); throw; } } diff --git a/Config/Config.cs b/Config/Config.cs new file mode 100644 index 0000000..dc1cdb9 --- /dev/null +++ b/Config/Config.cs @@ -0,0 +1,13 @@ +using Discord; +using Newtonsoft.Json; +using Serilog; + +namespace QuickEdit.Config; + +public sealed class DiscordConfig +{ + public string? Token { get; set; } + public ActivityType StatusType { get; set; } + public string? Status { get; set; } + public bool Debug { get; set; } +} diff --git a/ConfigManager.cs b/ConfigManager.cs deleted file mode 100644 index a4a2f53..0000000 --- a/ConfigManager.cs +++ /dev/null @@ -1,35 +0,0 @@ -using Discord; -using Newtonsoft.Json; -using Serilog; - -namespace QuickEdit; -public class Config -{ - public required string token; - public ActivityType statusType; - public string status = string.Empty; - public bool debug = false; - - public static Config GetConfig() - { - string path = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "config.json"); - if (!File.Exists(path)) - { - Log.Fatal($"Config file not found at: {Path.GetFullPath(path)}"); - Log.Information($"Check if you have a valid config.json file present in the directory of the executable ({AppDomain.CurrentDomain.BaseDirectory})"); - throw new FileNotFoundException(); - } - - try - { - var config = JsonConvert.DeserializeObject(File.ReadAllText(path))!; - Log.Debug("Loaded config file"); - return config; - } - catch (Exception e) - { - Log.Fatal($"Failed to parse config file: {e.Message}"); - throw; - } - } -} diff --git a/Program.cs b/Program.cs index 0bccba6..ae17eaa 100644 --- a/Program.cs +++ b/Program.cs @@ -1,5 +1,6 @@ -using Discord; +using Discord; using Discord.WebSocket; +using Discord.Interactions; using FFMpegCore; using FFMpegCore.Exceptions; using FFMpegCore.Helpers; @@ -7,65 +8,99 @@ using QuickEdit.Logger; using Serilog; +using Serilog.Extensions.Hosting; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; + namespace QuickEdit; internal class Program { - public static DiscordSocketClient? client; - public static Config? config; - public static readonly DiscordSocketConfig socketConfig = new() { GatewayIntents = GatewayIntents.None }; - public static Task Main(string[] args) => new Program().MainAsync(); + private static readonly Config.DiscordConfig? discordConfig = new(); + + public static Task Main(string[] args) => new Program().MainAsync(args); - public async Task MainAsync() + public async Task MainAsync(string[] args) { + // Configure Serilog first SerilogConfiguration.ConfigureLogger(); + ShowStartMessage(); - // Handle exit - AppDomain.CurrentDomain.ProcessExit += (s, e) => - { - Log.Debug("Stopping program gracefully"); - try - { - if (client == null) return; - Log.Debug("Stopping bot"); - client.LogoutAsync().Wait(); - client.StopAsync().Wait(); - Log.Debug("Stopped bot"); - } - catch (Exception ex) - { - Log.Error("Error while trying to stop bot\n{e}", ex); - } - finally - { - Log.CloseAndFlush(); - } - }; + // Generic Host setup + HostApplicationBuilderSettings hostSettings = new() + { + Args = args, + Configuration = new ConfigurationManager(), + ContentRootPath = AppDomain.CurrentDomain.BaseDirectory + }; - ShowStartMessage(); + hostSettings.Configuration.AddJsonFile("config.json"); + hostSettings.Configuration.AddCommandLine(args); + HostApplicationBuilder hostBuilder = Host.CreateApplicationBuilder(hostSettings); + ConfigureServices(hostBuilder.Services); + hostBuilder.Configuration.GetRequiredSection(nameof(DiscordConfig)) + .Bind(discordConfig); + + using var host = hostBuilder.Build(); + + // Change log level after getting Config try { - config = Config.GetConfig(); - SerilogConfiguration.LoggingLevel.MinimumLevel = config.debug ? Serilog.Events.LogEventLevel.Debug : Serilog.Events.LogEventLevel.Information; + SerilogConfiguration.LoggingLevel.MinimumLevel = + discordConfig!.Debug + ? Serilog.Events.LogEventLevel.Debug + : Serilog.Events.LogEventLevel.Information; } - catch + catch (Exception e) { - Log.Fatal("Failed to GetConfig()"); + Log.Fatal("Failed to set debugging level:\n{e}", e); Environment.ExitCode = 1; // Exit without triggering DeepSource lol return; } + if (!CheckFFMpegExists()) return; + + await host.RunAsync(); + } + + private static void ConfigureServices(IServiceCollection services) + { + var socketConfig = new DiscordSocketConfig() + { + GatewayIntents = GatewayIntents.None + }; + + var interactionServiceConfig = new InteractionServiceConfig() + { + UseCompiledLambda = true, + DefaultRunMode = RunMode.Async + }; + + services.AddSerilog(); + services.AddSingleton(discordConfig!); + services.AddSingleton(socketConfig); + services.AddSingleton(interactionServiceConfig); + services.AddSingleton(); + services.AddSingleton(x => new InteractionService(x.GetRequiredService(), interactionServiceConfig)); + services.AddSingleton(); + services.AddHostedService(); + } + + private static bool CheckFFMpegExists() + { try { FFMpegHelper.VerifyFFMpegExists(GlobalFFOptions.Current); Log.Debug("Found FFMpeg"); + return true; } catch (FFMpegException) { Log.Fatal("FFMpeg not found."); Environment.ExitCode = 1; - return; + return false; } catch (Exception e) { @@ -73,28 +108,8 @@ public async Task MainAsync() // fail before it can throw the correct exception, which causes a different exception. Log.Fatal("FFMpeg verification resulted in a failure:\n{Message}", e); Environment.ExitCode = 1; - return; - } - - client = new DiscordSocketClient(socketConfig); - - client.Log += AutoLog.LogMessage; - client.Ready += OnReadyAsync; - - await client.LoginAsync(TokenType.Bot, config.token); - await client.StartAsync(); - - // Custom activities use a different method - if (config.statusType == ActivityType.CustomStatus) - { - await client.SetCustomStatusAsync(config.status); - } - else - { - await client.SetGameAsync(config.status, null, config.statusType); + return false; } - - await Task.Delay(-1); } private static void ShowStartMessage() @@ -105,18 +120,4 @@ private static void ShowStartMessage() Console.WriteLine($"\u001b[36m ---- QuickEdit ver. {buildVer} - Build Date: {compileTime.ToUniversalTime()} UTC - By HEJOK254 ---- \u001b[0m"); } - - private async Task OnReadyAsync() - { - try - { - await InteractionServiceHandler.InitAsync(); - } - catch - { - Log.Fatal("Program is exiting due to an error in InteractionServiceHandler."); - // The program cannot continue without the InteractionService, so terminate it. Nothing important should be running at this point. - Environment.Exit(1); // skipcq: CS-W1005 - } - } } From 3c54763166b8dade02d74433f4ccc6f5c42d7201 Mon Sep 17 00:00:00 2001 From: HEJOK254 <90698026+HEJOK254@users.noreply.github.com> Date: Sun, 22 Sep 2024 00:38:19 +0200 Subject: [PATCH 04/13] Rework Configuration --- Commands/InteractionServiceHandler.cs | 4 +- Config/Config.cs | 57 ++++++++++++++++- Discord QuickEdit.csproj | 1 + Logger/SerilogConfiguration.cs | 24 ++++++- Program.cs | 92 +++++++++++++++------------ 5 files changed, 132 insertions(+), 46 deletions(-) diff --git a/Commands/InteractionServiceHandler.cs b/Commands/InteractionServiceHandler.cs index adf7d1d..3e7caa4 100644 --- a/Commands/InteractionServiceHandler.cs +++ b/Commands/InteractionServiceHandler.cs @@ -3,8 +3,6 @@ using Discord.WebSocket; using Serilog; -using QuickEdit.Config; - namespace QuickEdit.Commands; internal sealed class InteractionServiceHandler { @@ -36,7 +34,7 @@ public async Task InitAsync() try { - _interactionService = new InteractionService(_client!.Rest, _interactionServiceConfig); + _interactionService = new InteractionService(_client.Rest, _interactionServiceConfig); await RegisterModulesAsync(); // Can't simply get the result of the ExecuteCommandAsync, because of RunMode.Async diff --git a/Config/Config.cs b/Config/Config.cs index dc1cdb9..afb23e1 100644 --- a/Config/Config.cs +++ b/Config/Config.cs @@ -1,12 +1,65 @@ using Discord; -using Newtonsoft.Json; using Serilog; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Configuration; +using System.ComponentModel.DataAnnotations; + namespace QuickEdit.Config; +internal sealed class ConfigManager +{ + /// + /// Set up configuration services + /// + /// The HostApplicationBuilder used for the program + /// True is success, False if failure + internal static bool LoadConfiguration(HostApplicationBuilder builder) + { + try + { + // Binding + var discordConfig = builder.Configuration.GetRequiredSection(DiscordConfig.ConfigurationSectionName) + .Get()!; + + // Validation + ValidateConfig(discordConfig); + + // Service registration (DI) + builder.Services.AddSingleton(discordConfig); + return true; + } + catch (ValidationException e) + { + Log.Fatal("Config parse error: {e}", e.Message); + Environment.ExitCode = 1; + return false; + } + catch (Exception e) + { + Log.Fatal("Failed to get config or create config service: {e}", e); + Environment.ExitCode = 1; + return false; + } + } + + private static void ValidateConfig(object config) + { + var validationContext = new ValidationContext(config); + Validator.ValidateObject(config, validationContext, validateAllProperties: true); + } +} + public sealed class DiscordConfig { - public string? Token { get; set; } + public const string ConfigurationSectionName = "DiscordConfig"; + + // TODO: Move the Token to user secrets at some point + + [Required] + [RegularExpression(@"^([MN][\w-]{23,25})\.([\w-]{6})\.([\w-]{27,39})$", ErrorMessage = "Invalid token format")] + public required string Token { get; set; } public ActivityType StatusType { get; set; } public string? Status { get; set; } public bool Debug { get; set; } diff --git a/Discord QuickEdit.csproj b/Discord QuickEdit.csproj index e5f4f37..c63e0ce 100644 --- a/Discord QuickEdit.csproj +++ b/Discord QuickEdit.csproj @@ -14,6 +14,7 @@ + diff --git a/Logger/SerilogConfiguration.cs b/Logger/SerilogConfiguration.cs index 58a11cd..6ccf81e 100644 --- a/Logger/SerilogConfiguration.cs +++ b/Logger/SerilogConfiguration.cs @@ -1,13 +1,15 @@ -using System.Diagnostics; +using QuickEdit.Config; using Serilog; using Serilog.Core; namespace QuickEdit.Logger; -public class SerilogConfiguration +public class SerilogConfiguration(DiscordConfig discordConfig) { // Use Debug by default, as it should get overwritten after the config is parsed and can help with Config issues public static LoggingLevelSwitch LoggingLevel { get; set; } = new LoggingLevelSwitch(Serilog.Events.LogEventLevel.Debug); + private readonly DiscordConfig _discordConfig = discordConfig; + public static void ConfigureLogger() { var logDirectory = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "logs"); @@ -23,4 +25,22 @@ public static void ConfigureLogger() Log.Logger = loggerConfig.CreateLogger(); } + + internal bool SetLoggingLevelFromConfig() + { + try + { + LoggingLevel.MinimumLevel = + _discordConfig.Debug + ? Serilog.Events.LogEventLevel.Debug + : Serilog.Events.LogEventLevel.Information; + return true; + } + catch (Exception e) + { + Log.Fatal("Failed to set minimum log level: {e}", e); + Environment.ExitCode = 1; + return false; + } + } } \ No newline at end of file diff --git a/Program.cs b/Program.cs index ae17eaa..2992983 100644 --- a/Program.cs +++ b/Program.cs @@ -6,9 +6,9 @@ using FFMpegCore.Helpers; using QuickEdit.Commands; using QuickEdit.Logger; +using QuickEdit.Config; using Serilog; -using Serilog.Extensions.Hosting; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; @@ -17,8 +17,6 @@ namespace QuickEdit; internal class Program { - private static readonly Config.DiscordConfig? discordConfig = new(); - public static Task Main(string[] args) => new Program().MainAsync(args); public async Task MainAsync(string[] args) @@ -35,57 +33,73 @@ public async Task MainAsync(string[] args) ContentRootPath = AppDomain.CurrentDomain.BaseDirectory }; - hostSettings.Configuration.AddJsonFile("config.json"); - hostSettings.Configuration.AddCommandLine(args); - - HostApplicationBuilder hostBuilder = Host.CreateApplicationBuilder(hostSettings); - ConfigureServices(hostBuilder.Services); - hostBuilder.Configuration.GetRequiredSection(nameof(DiscordConfig)) - .Bind(discordConfig); - - using var host = hostBuilder.Build(); - - // Change log level after getting Config try { - SerilogConfiguration.LoggingLevel.MinimumLevel = - discordConfig!.Debug - ? Serilog.Events.LogEventLevel.Debug - : Serilog.Events.LogEventLevel.Information; + hostSettings.Configuration.AddJsonFile("config.json"); + hostSettings.Configuration.AddCommandLine(args); + } + catch (FileNotFoundException) + { + // Can't log the file name using FileNotFoundException.FileName as it's just null + Log.Fatal("Couldn't find file 'config.json' in path: {path}", AppDomain.CurrentDomain.BaseDirectory); + return; } catch (Exception e) { - Log.Fatal("Failed to set debugging level:\n{e}", e); - Environment.ExitCode = 1; // Exit without triggering DeepSource lol + Log.Fatal("Failed to add config providers:{e}", e); return; } + HostApplicationBuilder hostBuilder = Host.CreateApplicationBuilder(hostSettings); + if (!ConfigManager.LoadConfiguration(hostBuilder)) return; + if (!ConfigureServices(hostBuilder.Services)) return; + + using var host = hostBuilder.Build(); + + // Change log level after getting Config + host.Services.GetRequiredService().SetLoggingLevelFromConfig(); + if (!CheckFFMpegExists()) return; await host.RunAsync(); } - private static void ConfigureServices(IServiceCollection services) + /// + /// Configures Dependency Injection Services + /// + /// The service collection from the builder + /// True is success, False if failure + private static bool ConfigureServices(IServiceCollection services) { - var socketConfig = new DiscordSocketConfig() + try { - GatewayIntents = GatewayIntents.None - }; - - var interactionServiceConfig = new InteractionServiceConfig() + var socketConfig = new DiscordSocketConfig() + { + GatewayIntents = GatewayIntents.None + }; + + var interactionServiceConfig = new InteractionServiceConfig() + { + UseCompiledLambda = true, + DefaultRunMode = RunMode.Async + }; + + services.AddSerilog(); + services.AddTransient(); + services.AddSingleton(socketConfig); + services.AddSingleton(interactionServiceConfig); + services.AddSingleton(); + services.AddSingleton(x => new InteractionService(x.GetRequiredService(), interactionServiceConfig)); + services.AddSingleton(); + services.AddHostedService(); + return true; + } + catch (Exception e) { - UseCompiledLambda = true, - DefaultRunMode = RunMode.Async - }; - - services.AddSerilog(); - services.AddSingleton(discordConfig!); - services.AddSingleton(socketConfig); - services.AddSingleton(interactionServiceConfig); - services.AddSingleton(); - services.AddSingleton(x => new InteractionService(x.GetRequiredService(), interactionServiceConfig)); - services.AddSingleton(); - services.AddHostedService(); + Log.Fatal("Failed to configure services: {e}", e); + Environment.ExitCode = 1; + return false; + } } private static bool CheckFFMpegExists() @@ -106,7 +120,7 @@ private static bool CheckFFMpegExists() { // It seems that there might be a bug in FFMpegCore, causing VerifyFFMpegExists() to // fail before it can throw the correct exception, which causes a different exception. - Log.Fatal("FFMpeg verification resulted in a failure:\n{Message}", e); + Log.Fatal("FFMpeg verification resulted in a failure: {Message}", e); Environment.ExitCode = 1; return false; } From 82aef8af7b334ec5c8578983f4036e61083f649d Mon Sep 17 00:00:00 2001 From: HEJOK254 <90698026+HEJOK254@users.noreply.github.com> Date: Sun, 22 Sep 2024 02:46:16 +0200 Subject: [PATCH 05/13] Add Token Validation The program will now report an invalid token with a single log line and stop the application --- Bot.cs | 34 ++++++++++++++++++++++++++++++---- 1 file changed, 30 insertions(+), 4 deletions(-) diff --git a/Bot.cs b/Bot.cs index c346853..e19e5ba 100644 --- a/Bot.cs +++ b/Bot.cs @@ -1,5 +1,3 @@ -using System; -using System.Diagnostics; using Discord; using Discord.WebSocket; using Microsoft.Extensions.Hosting; @@ -21,8 +19,36 @@ public async Task StartAsync(CancellationToken cancellationToken) _client.Log += AutoLog.LogMessage; _client.Ready += OnReadyAsync; - await _client.LoginAsync(TokenType.Bot, _discordConfig.Token); - await _client.StartAsync(); + // Try-catch ValidateToken since LoginAsync doesn't throw exceptions, they just catch them + // So there's no way to know if the token is invalid without checking it first (or is there?) + // Also this is separate from the other try-catch to make sure it's the token that's invalid + try + { + TokenUtils.ValidateToken(TokenType.Bot, _discordConfig.Token); + } + catch (ArgumentException e) + { + Log.Fatal("{e}", e.Message); + Environment.ExitCode = 1; + _appLifetime.StopApplication(); + return; // The app would normally continue for a short amount of time before stopping + } + + // Most of the exceptions are caught by the library :( [maybe not idk i'm writing this at 2am] + // This means that the program will log stuff in an ugly way and NOT stop the program on fatal errors + try + { + // The token is already validated, so there's no need to validate it again + await _client.LoginAsync(TokenType.Bot, _discordConfig.Token, validateToken: false); + await _client.StartAsync(); + } + catch (Exception e) + { + Log.Fatal("Failed to start the bot: {e}", e); + Environment.ExitCode = 1; + _appLifetime.StopApplication(); + return; + } // Custom activities use a different method if (_discordConfig.StatusType == ActivityType.CustomStatus) From eb7e1851e5423b4ca8787c5b686d6e0afb0afa89 Mon Sep 17 00:00:00 2001 From: HEJOK254 <90698026+HEJOK254@users.noreply.github.com> Date: Sun, 22 Sep 2024 03:38:06 +0200 Subject: [PATCH 06/13] Refactor interaction service handling to use interface and default implementation (DIP) --- Bot.cs | 4 ++-- Commands/InteractionServiceHandler.cs | 16 +++++++++++----- Program.cs | 2 +- 3 files changed, 14 insertions(+), 8 deletions(-) diff --git a/Bot.cs b/Bot.cs index e19e5ba..2c5e1bd 100644 --- a/Bot.cs +++ b/Bot.cs @@ -7,11 +7,11 @@ namespace QuickEdit; -internal sealed class Bot(DiscordSocketClient client, Config.DiscordConfig discordConfig, InteractionServiceHandler interactionServiceHandler, IHostApplicationLifetime appLifetime) : IHostedService +internal sealed class Bot(DiscordSocketClient client, Config.DiscordConfig discordConfig, IInteractionServiceHandler interactionServiceHandler, IHostApplicationLifetime appLifetime) : IHostedService { private readonly DiscordSocketClient _client = client; private readonly Config.DiscordConfig _discordConfig = discordConfig; - private readonly InteractionServiceHandler _interactionServiceHandler = interactionServiceHandler; + private readonly IInteractionServiceHandler _interactionServiceHandler = interactionServiceHandler; private readonly IHostApplicationLifetime _appLifetime = appLifetime; public async Task StartAsync(CancellationToken cancellationToken) diff --git a/Commands/InteractionServiceHandler.cs b/Commands/InteractionServiceHandler.cs index 3e7caa4..414dd44 100644 --- a/Commands/InteractionServiceHandler.cs +++ b/Commands/InteractionServiceHandler.cs @@ -4,7 +4,13 @@ using Serilog; namespace QuickEdit.Commands; -internal sealed class InteractionServiceHandler + +internal interface IInteractionServiceHandler +{ + Task InitAsync(); +} + +internal sealed class DefaultInteractionServiceHandler : IInteractionServiceHandler { private readonly DiscordSocketClient _client; private InteractionService _interactionService; @@ -13,7 +19,7 @@ internal sealed class InteractionServiceHandler private static readonly SemaphoreSlim _initSemaphore = new(1); private static bool isReady = false; - public InteractionServiceHandler(DiscordSocketClient client, InteractionService interactionService, Config.DiscordConfig discordConfig, InteractionServiceConfig interactionServiceConfig) + public DefaultInteractionServiceHandler(DiscordSocketClient client, InteractionService interactionService, Config.DiscordConfig discordConfig, InteractionServiceConfig interactionServiceConfig) { _client = client; _interactionService = interactionService; @@ -56,7 +62,7 @@ public async Task InitAsync() /// /// Register modules / commands /// - public async Task RegisterModulesAsync() + private async Task RegisterModulesAsync() { // The service might not have been initialized yet if (_interactionService == null) @@ -84,7 +90,7 @@ public async Task RegisterModulesAsync() } } - public async Task OnInteractionCreatedAsync(SocketInteraction interaction) + private async Task OnInteractionCreatedAsync(SocketInteraction interaction) { // The service might not have been initialized yet if (_interactionService == null) @@ -113,7 +119,7 @@ public async Task OnInteractionCreatedAsync(SocketInteraction interaction) } } - public static async Task OnSlashCommandExecutedAsync(SlashCommandInfo commandInfo, IInteractionContext interactionContext, IResult result) + private static async Task OnSlashCommandExecutedAsync(SlashCommandInfo commandInfo, IInteractionContext interactionContext, IResult result) { // Only trying to handle errors lol if (result.IsSuccess) diff --git a/Program.cs b/Program.cs index 2992983..a9fd455 100644 --- a/Program.cs +++ b/Program.cs @@ -90,7 +90,7 @@ private static bool ConfigureServices(IServiceCollection services) services.AddSingleton(interactionServiceConfig); services.AddSingleton(); services.AddSingleton(x => new InteractionService(x.GetRequiredService(), interactionServiceConfig)); - services.AddSingleton(); + services.AddSingleton(); services.AddHostedService(); return true; } From a17d3a9127f6793faba82762b7c9d8471ec9ecbd Mon Sep 17 00:00:00 2001 From: HEJOK254 <90698026+HEJOK254@users.noreply.github.com> Date: Sun, 22 Sep 2024 23:37:33 +0200 Subject: [PATCH 07/13] Remove unused DiscordConfig parameter from DefaultInteractionServiceHandler constructor --- Commands/InteractionServiceHandler.cs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/Commands/InteractionServiceHandler.cs b/Commands/InteractionServiceHandler.cs index 414dd44..fcf6028 100644 --- a/Commands/InteractionServiceHandler.cs +++ b/Commands/InteractionServiceHandler.cs @@ -15,15 +15,13 @@ internal sealed class DefaultInteractionServiceHandler : IInteractionServiceHand private readonly DiscordSocketClient _client; private InteractionService _interactionService; private readonly InteractionServiceConfig _interactionServiceConfig; - private readonly Config.DiscordConfig _discordConfig; private static readonly SemaphoreSlim _initSemaphore = new(1); private static bool isReady = false; - public DefaultInteractionServiceHandler(DiscordSocketClient client, InteractionService interactionService, Config.DiscordConfig discordConfig, InteractionServiceConfig interactionServiceConfig) + public DefaultInteractionServiceHandler(DiscordSocketClient client, InteractionService interactionService, InteractionServiceConfig interactionServiceConfig) { _client = client; _interactionService = interactionService; - _discordConfig = discordConfig; _interactionServiceConfig = interactionServiceConfig; } From 498c0a490e2d3d47799509d34f3ba83021d4fb12 Mon Sep 17 00:00:00 2001 From: HEJOK254 <90698026+HEJOK254@users.noreply.github.com> Date: Mon, 23 Sep 2024 13:46:34 +0200 Subject: [PATCH 08/13] Split Config into ConfigManager and Config Sections --- Config/{Config.cs => ConfigManager.cs} | 13 ------------- Config/DiscordConfig.cs | 18 ++++++++++++++++++ 2 files changed, 18 insertions(+), 13 deletions(-) rename Config/{Config.cs => ConfigManager.cs} (75%) create mode 100644 Config/DiscordConfig.cs diff --git a/Config/Config.cs b/Config/ConfigManager.cs similarity index 75% rename from Config/Config.cs rename to Config/ConfigManager.cs index afb23e1..c0718aa 100644 --- a/Config/Config.cs +++ b/Config/ConfigManager.cs @@ -51,16 +51,3 @@ private static void ValidateConfig(object config) } } -public sealed class DiscordConfig -{ - public const string ConfigurationSectionName = "DiscordConfig"; - - // TODO: Move the Token to user secrets at some point - - [Required] - [RegularExpression(@"^([MN][\w-]{23,25})\.([\w-]{6})\.([\w-]{27,39})$", ErrorMessage = "Invalid token format")] - public required string Token { get; set; } - public ActivityType StatusType { get; set; } - public string? Status { get; set; } - public bool Debug { get; set; } -} diff --git a/Config/DiscordConfig.cs b/Config/DiscordConfig.cs new file mode 100644 index 0000000..ab15e29 --- /dev/null +++ b/Config/DiscordConfig.cs @@ -0,0 +1,18 @@ +using System.ComponentModel.DataAnnotations; +using Discord; + +namespace QuickEdit.Config; + +public sealed class DiscordConfig +{ + public const string ConfigurationSectionName = "DiscordConfig"; + + // TODO: Move the Token to user secrets at some point + + [Required] + [RegularExpression(@"^([MN][\w-]{23,25})\.([\w-]{6})\.([\w-]{27,39})$", ErrorMessage = "Invalid token format")] + public required string Token { get; set; } + public ActivityType StatusType { get; set; } + public string? Status { get; set; } + public bool Debug { get; set; } +} From a38e6bce90bea3c76bd8aebf88c884a709acdb9f Mon Sep 17 00:00:00 2001 From: HEJOK254 <90698026+HEJOK254@users.noreply.github.com> Date: Sat, 26 Oct 2024 02:58:37 +0200 Subject: [PATCH 09/13] Refactor DefaultInteractionServiceHandler to use primary constructor --- Commands/InteractionServiceHandler.cs | 15 ++++----------- 1 file changed, 4 insertions(+), 11 deletions(-) diff --git a/Commands/InteractionServiceHandler.cs b/Commands/InteractionServiceHandler.cs index fcf6028..a9494f5 100644 --- a/Commands/InteractionServiceHandler.cs +++ b/Commands/InteractionServiceHandler.cs @@ -10,21 +10,14 @@ internal interface IInteractionServiceHandler Task InitAsync(); } -internal sealed class DefaultInteractionServiceHandler : IInteractionServiceHandler +internal sealed class DefaultInteractionServiceHandler(DiscordSocketClient client, InteractionService interactionService, InteractionServiceConfig interactionServiceConfig) : IInteractionServiceHandler { - private readonly DiscordSocketClient _client; - private InteractionService _interactionService; - private readonly InteractionServiceConfig _interactionServiceConfig; + private readonly DiscordSocketClient _client = client; + private InteractionService _interactionService = interactionService; + private readonly InteractionServiceConfig _interactionServiceConfig = interactionServiceConfig; private static readonly SemaphoreSlim _initSemaphore = new(1); private static bool isReady = false; - public DefaultInteractionServiceHandler(DiscordSocketClient client, InteractionService interactionService, InteractionServiceConfig interactionServiceConfig) - { - _client = client; - _interactionService = interactionService; - _interactionServiceConfig = interactionServiceConfig; - } - /// /// Initialize the InteractionService /// From 89e65804c32fecbf59c107c7e97e56bf45e91746 Mon Sep 17 00:00:00 2001 From: HEJOK254 <90698026+HEJOK254@users.noreply.github.com> Date: Tue, 29 Oct 2024 22:44:29 +0100 Subject: [PATCH 10/13] Refactor InteractionServiceHandler to user IHostedService InteractionServiceHandler now uses the IHostedService interface instead of a custom interface. This allows Bot.cs to not need to take care of the InteractionServiceHandler, as it can do it itself. --- Bot.cs | 19 +----------------- Commands/InteractionServiceHandler.cs | 29 ++++++++++++++++++--------- Program.cs | 2 +- 3 files changed, 21 insertions(+), 29 deletions(-) diff --git a/Bot.cs b/Bot.cs index 2c5e1bd..8bda7a4 100644 --- a/Bot.cs +++ b/Bot.cs @@ -7,17 +7,15 @@ namespace QuickEdit; -internal sealed class Bot(DiscordSocketClient client, Config.DiscordConfig discordConfig, IInteractionServiceHandler interactionServiceHandler, IHostApplicationLifetime appLifetime) : IHostedService +internal sealed class Bot(DiscordSocketClient client, Config.DiscordConfig discordConfig, IHostApplicationLifetime appLifetime) : IHostedService { private readonly DiscordSocketClient _client = client; private readonly Config.DiscordConfig _discordConfig = discordConfig; - private readonly IInteractionServiceHandler _interactionServiceHandler = interactionServiceHandler; private readonly IHostApplicationLifetime _appLifetime = appLifetime; public async Task StartAsync(CancellationToken cancellationToken) { _client.Log += AutoLog.LogMessage; - _client.Ready += OnReadyAsync; // Try-catch ValidateToken since LoginAsync doesn't throw exceptions, they just catch them // So there's no way to know if the token is invalid without checking it first (or is there?) @@ -66,19 +64,4 @@ public async Task StopAsync(CancellationToken cancellationToken) await _client.LogoutAsync(); await _client.StopAsync(); } - - private async Task OnReadyAsync() - { - try - { - await _interactionServiceHandler.InitAsync(); - } - catch - { - Log.Fatal("Program is exiting due to an error in InteractionServiceHandler."); - // The program cannot continue without the InteractionService, so terminate it. Nothing important should be running at this point. - Environment.ExitCode = 1; - _appLifetime.StopApplication(); - } - } } diff --git a/Commands/InteractionServiceHandler.cs b/Commands/InteractionServiceHandler.cs index a9494f5..febcac7 100644 --- a/Commands/InteractionServiceHandler.cs +++ b/Commands/InteractionServiceHandler.cs @@ -1,31 +1,39 @@ using Discord; using Discord.Interactions; using Discord.WebSocket; +using Microsoft.Extensions.Hosting; using Serilog; namespace QuickEdit.Commands; - -internal interface IInteractionServiceHandler -{ - Task InitAsync(); -} - -internal sealed class DefaultInteractionServiceHandler(DiscordSocketClient client, InteractionService interactionService, InteractionServiceConfig interactionServiceConfig) : IInteractionServiceHandler +internal sealed class InteractionServiceHandler(DiscordSocketClient client, InteractionService interactionService, InteractionServiceConfig interactionServiceConfig, IHostApplicationLifetime appLifetime) : IHostedService { private readonly DiscordSocketClient _client = client; private InteractionService _interactionService = interactionService; private readonly InteractionServiceConfig _interactionServiceConfig = interactionServiceConfig; + private readonly IHostApplicationLifetime _appLifetime = appLifetime; private static readonly SemaphoreSlim _initSemaphore = new(1); private static bool isReady = false; + public Task StartAsync(CancellationToken cancellationToken) + { + _client.Ready += InitAsync; + return Task.CompletedTask; + } + + public Task StopAsync(CancellationToken cancellationToken) + { + _interactionService?.Dispose(); + return Task.CompletedTask; + } + /// /// Initialize the InteractionService /// - public async Task InitAsync() + private async Task InitAsync() { await _initSemaphore.WaitAsync(); - // Prevent reinitialization + // Prevent re-initialization if (isReady) return; @@ -42,7 +50,8 @@ public async Task InitAsync() catch (Exception e) { Log.Fatal("Error initializing InteractionService: {e}", e); - throw; + Environment.ExitCode = 1; // TODO: Maybe implement different exit codes in the future + _appLifetime.StopApplication(); } finally { diff --git a/Program.cs b/Program.cs index a9fd455..de22e72 100644 --- a/Program.cs +++ b/Program.cs @@ -90,7 +90,7 @@ private static bool ConfigureServices(IServiceCollection services) services.AddSingleton(interactionServiceConfig); services.AddSingleton(); services.AddSingleton(x => new InteractionService(x.GetRequiredService(), interactionServiceConfig)); - services.AddSingleton(); + services.AddHostedService(); services.AddHostedService(); return true; } From 7a1e6d551ca38976dc5150b21dbdb7ac889c6bd2 Mon Sep 17 00:00:00 2001 From: HEJOK254 <90698026+HEJOK254@users.noreply.github.com> Date: Tue, 29 Oct 2024 22:47:07 +0100 Subject: [PATCH 11/13] Fix SlashCommand Exception Handling Log It now actually logs at least the type of error lol --- Commands/InteractionServiceHandler.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Commands/InteractionServiceHandler.cs b/Commands/InteractionServiceHandler.cs index febcac7..27ace51 100644 --- a/Commands/InteractionServiceHandler.cs +++ b/Commands/InteractionServiceHandler.cs @@ -127,7 +127,7 @@ private static async Task OnSlashCommandExecutedAsync(SlashCommandInfo commandIn try { - Log.Error("Error handling interaction: {result.Error}", result); + Log.Error("Error handling interaction: {Error}", result.Error); // TODO: Somehow get more information about the error await interactionContext.Interaction.FollowupAsync("An error occurred while executing the command.", ephemeral: true); } catch (Exception e) From c01319187e687f767b7cf8042bc18a08f9257057 Mon Sep 17 00:00:00 2001 From: HEJOK254 <90698026+HEJOK254@users.noreply.github.com> Date: Sat, 2 Nov 2024 00:16:55 +0100 Subject: [PATCH 12/13] Fix InteractionServiceHandler semaphore isReady check Co-authored-by: Filip55561 <88946851+Filip55561@users.noreply.github.com> --- Commands/InteractionServiceHandler.cs | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/Commands/InteractionServiceHandler.cs b/Commands/InteractionServiceHandler.cs index 27ace51..49aa85a 100644 --- a/Commands/InteractionServiceHandler.cs +++ b/Commands/InteractionServiceHandler.cs @@ -33,12 +33,11 @@ private async Task InitAsync() { await _initSemaphore.WaitAsync(); - // Prevent re-initialization - if (isReady) - return; - try { + // Prevent re-initialization + if (isReady) return; + _interactionService = new InteractionService(_client.Rest, _interactionServiceConfig); await RegisterModulesAsync(); From 7e06e1c994006283ccb0f26b203c714fa741c7ed Mon Sep 17 00:00:00 2001 From: HEJOK254 <90698026+HEJOK254@users.noreply.github.com> Date: Sun, 17 Nov 2024 00:04:50 +0100 Subject: [PATCH 13/13] Cleanup: Remove unused using directive --- Config/ConfigManager.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/Config/ConfigManager.cs b/Config/ConfigManager.cs index c0718aa..3d50a1f 100644 --- a/Config/ConfigManager.cs +++ b/Config/ConfigManager.cs @@ -1,6 +1,4 @@ -using Discord; using Serilog; - using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Configuration;