Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

.NET Generic Host Implementation #32

Merged
merged 13 commits into from
Nov 16, 2024
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
84 changes: 84 additions & 0 deletions Bot.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
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, IInteractionServiceHandler interactionServiceHandler, 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?)
// 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)
{
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();
}
}
}
47 changes: 27 additions & 20 deletions Commands/InteractionServiceHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,31 @@
using Serilog;

namespace QuickEdit.Commands;
public class InteractionServiceHandler

internal interface IInteractionServiceHandler
{
private static readonly DiscordSocketClient? _client = Program.client;
private static InteractionService? _interactionService;
private static readonly InteractionServiceConfig _interactionServiceConfig = new() { UseCompiledLambda = true, DefaultRunMode = RunMode.Async };
Task InitAsync();
}
HEJOK254 marked this conversation as resolved.
Show resolved Hide resolved

internal sealed class DefaultInteractionServiceHandler : IInteractionServiceHandler
{
private readonly DiscordSocketClient _client;
private InteractionService _interactionService;
private readonly 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;
}
HEJOK254 marked this conversation as resolved.
Show resolved Hide resolved

/// <summary>
/// Initialize the InteractionService
/// </summary>
public static async Task InitAsync()
public async Task InitAsync()
{
await _initSemaphore.WaitAsync();

Expand All @@ -25,13 +38,7 @@ 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);
_interactionService = new InteractionService(_client.Rest, _interactionServiceConfig);
await RegisterModulesAsync();

// Can't simply get the result of the ExecuteCommandAsync, because of RunMode.Async
Expand All @@ -41,7 +48,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
Expand All @@ -53,7 +60,7 @@ public static async Task InitAsync()
/// <summary>
/// Register modules / commands
/// </summary>
public static async Task RegisterModulesAsync()
private async Task RegisterModulesAsync()
{
// The service might not have been initialized yet
if (_interactionService == null)
Expand All @@ -76,12 +83,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)
private async Task OnInteractionCreatedAsync(SocketInteraction interaction)
{
// The service might not have been initialized yet
if (_interactionService == null)
Expand All @@ -99,7 +106,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)
{
Expand All @@ -110,20 +117,20 @@ public static 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)
return;

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;
}
}
Expand Down
53 changes: 53 additions & 0 deletions Config/ConfigManager.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
using Discord;
HEJOK254 marked this conversation as resolved.
Show resolved Hide resolved
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
{
/// <summary>
/// Set up configuration services
/// </summary>
/// <param name="builder">The HostApplicationBuilder used for the program</param>
/// <returns>True is success, False if failure</returns>
internal static bool LoadConfiguration(HostApplicationBuilder builder)
{
try
{
// Binding
var discordConfig = builder.Configuration.GetRequiredSection(DiscordConfig.ConfigurationSectionName)
.Get<DiscordConfig>()!;

// 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);
}
}

18 changes: 18 additions & 0 deletions Config/DiscordConfig.cs
Original file line number Diff line number Diff line change
@@ -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")]
HEJOK254 marked this conversation as resolved.
Show resolved Hide resolved
public required string Token { get; set; }
public ActivityType StatusType { get; set; }
public string? Status { get; set; }
public bool Debug { get; set; }
}
35 changes: 0 additions & 35 deletions ConfigManager.cs

This file was deleted.

3 changes: 3 additions & 0 deletions Discord QuickEdit.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,10 @@
<ItemGroup>
<PackageReference Include="Discord.Net" Version="3.15.3" />
<PackageReference Include="FFMpegCore" Version="5.1.0" />
<PackageReference Include="Microsoft.Extensions.Hosting" Version="8.0.0" />
<PackageReference Include="Microsoft.Extensions.Options.DataAnnotations" Version="8.0.0" />
<PackageReference Include="Serilog" Version="4.0.1" />
<PackageReference Include="Serilog.Extensions.Hosting" Version="8.0.0" />
<PackageReference Include="Serilog.Sinks.Console" Version="6.0.0" />
<PackageReference Include="Serilog.Sinks.File" Version="6.0.0" />
</ItemGroup>
Expand Down
24 changes: 22 additions & 2 deletions Logger/SerilogConfiguration.cs
Original file line number Diff line number Diff line change
@@ -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");
Expand All @@ -23,4 +25,22 @@ public static void ConfigureLogger()

Log.Logger = loggerConfig.CreateLogger();
}

internal bool SetLoggingLevelFromConfig()
HEJOK254 marked this conversation as resolved.
Show resolved Hide resolved
{
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;
}
}
}
Loading