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;