Skip to content

Commit

Permalink
Merge pull request #1035 from discord-csharp/refactor-quoting
Browse files Browse the repository at this point in the history
V3: MediatR, refactor message quoting, refactor startup
  • Loading branch information
patrickklaeren authored Nov 13, 2024
2 parents 6e593a1 + f960eb2 commit 20ad348
Show file tree
Hide file tree
Showing 23 changed files with 572 additions and 637 deletions.
2 changes: 1 addition & 1 deletion .editorconfig
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ dotnet_style_predefined_type_for_locals_parameters_members = true:suggestion
dotnet_style_predefined_type_for_member_access = true:suggestion

# Modifier settings
dotnet_style_require_accessibility_modifiers = always:error
dotnet_style_require_accessibility_modifiers = always:suggestion
dotnet_style_readonly_field = true:suggestion

# Parentheses settings
Expand Down
1 change: 1 addition & 0 deletions src/Modix.Bot/Modix.Bot.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Humanizer.Core" />
<PackageReference Include="MediatR" Version="12.4.1" />
<PackageReference Include="Microsoft.Extensions.Hosting" />
<PackageReference Include="Microsoft.Extensions.Http" />
<PackageReference Include="Newtonsoft.Json" />
Expand Down
214 changes: 107 additions & 107 deletions src/Modix.Bot/ModixBot.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,125 +10,104 @@
using Discord.Interactions;
using Discord.Rest;
using Discord.WebSocket;
using MediatR;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Modix.Bot.Notifications;
using Modix.Data.Models.Core;

namespace Modix
namespace Modix.Bot
{
public sealed class ModixBot : BackgroundService
public sealed class ModixBot(
DiscordSocketClient discordSocketClient,
DiscordRestClient discordRestClient,
IOptions<ModixConfig> modixConfig,
CommandService commandService,
InteractionService interactionService,
DiscordSerilogAdapter discordSerilogAdapter,
IHostApplicationLifetime hostApplicationLifetime,
IServiceProvider serviceProvider,
ILogger<ModixBot> logger,
IHostEnvironment hostEnvironment) : BackgroundService
{
private readonly DiscordSocketClient _client;
private readonly DiscordRestClient _restClient;
private readonly CommandService _commands;
private readonly InteractionService _interactions;
private readonly IServiceProvider _provider;
private readonly ModixConfig _config;
private readonly DiscordSerilogAdapter _serilogAdapter;
private readonly IHostApplicationLifetime _applicationLifetime;
private readonly IHostEnvironment _env;
private IServiceScope _scope;
private readonly ConcurrentDictionary<ICommandContext, IServiceScope> _commandScopes = new();

public ModixBot(
DiscordSocketClient discordClient,
DiscordRestClient restClient,
IOptions<ModixConfig> modixConfig,
CommandService commandService,
InteractionService interactions,
DiscordSerilogAdapter serilogAdapter,
IHostApplicationLifetime applicationLifetime,
IServiceProvider serviceProvider,
ILogger<ModixBot> logger,
IHostEnvironment env)
{
_client = discordClient ?? throw new ArgumentNullException(nameof(discordClient));
_restClient = restClient ?? throw new ArgumentNullException(nameof(restClient));
_config = modixConfig?.Value ?? throw new ArgumentNullException(nameof(modixConfig));
_commands = commandService ?? throw new ArgumentNullException(nameof(commandService));
_interactions = interactions ?? throw new ArgumentNullException(nameof(interactions));
_provider = serviceProvider ?? throw new ArgumentNullException(nameof(serviceProvider));
_serilogAdapter = serilogAdapter ?? throw new ArgumentNullException(nameof(serilogAdapter));
_applicationLifetime = applicationLifetime ?? throw new ArgumentNullException(nameof(applicationLifetime));
Log = logger ?? throw new ArgumentNullException(nameof(logger));
_env = env;
}

private ILogger<ModixBot> Log { get; }
private TaskCompletionSource<object> _whenReadySource;

protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
Thread.CurrentThread.CurrentCulture = new CultureInfo("en-us");
Thread.CurrentThread.CurrentUICulture = Thread.CurrentThread.CurrentCulture;

Log.LogInformation("Starting bot background service.");
logger.LogInformation("Starting bot background service");

IServiceScope scope = null;

try
{
// Create a new scope for the session.
scope = _provider.CreateScope();
scope = serviceProvider.CreateScope();

Log.LogTrace("Registering listeners for Discord client events.");
logger.LogTrace("Registering listeners for Discord client events");

_client.LatencyUpdated += OnLatencyUpdated;
_client.Disconnected += OnDisconnect;
discordSocketClient.LatencyUpdated += OnLatencyUpdated;
discordSocketClient.Disconnected += OnDisconnect;
discordSocketClient.Log += discordSerilogAdapter.HandleLog;
discordSocketClient.Ready += OnClientReady;
discordSocketClient.MessageReceived += OnMessageReceived;
discordSocketClient.MessageUpdated += OnMessageUpdated;

_client.Log += _serilogAdapter.HandleLog;
_restClient.Log += _serilogAdapter.HandleLog;
_commands.Log += _serilogAdapter.HandleLog;
discordRestClient.Log += discordSerilogAdapter.HandleLog;
commandService.Log += discordSerilogAdapter.HandleLog;

// Register with the cancellation token so we can stop listening to client events if the service is
// shutting down or being disposed.
stoppingToken.Register(OnStopping);

// The only thing that could go wrong at this point is the client failing to login and start. Promote
// our local service scope to a field so that it's available to the HandleCommand method once events
// start firing after we've connected.
_scope = scope;

Log.LogInformation("Loading command modules...");
logger.LogInformation("Loading command modules...");

await _commands.AddModulesAsync(typeof(ModixBot).Assembly, _scope.ServiceProvider);
await commandService.AddModulesAsync(typeof(ModixBot).Assembly, _scope.ServiceProvider);

Log.LogInformation("{Modules} modules loaded, containing {Commands} commands",
_commands.Modules.Count(), _commands.Modules.SelectMany(d=>d.Commands).Count());
logger.LogInformation("{Modules} modules loaded, containing {Commands} commands",
commandService.Modules.Count(), commandService.Modules.SelectMany(d=>d.Commands).Count());

Log.LogInformation("Logging into Discord and starting the client.");
logger.LogInformation("Logging into Discord and starting the client");

await StartClient(stoppingToken);

Log.LogInformation("Discord client started successfully.");
logger.LogInformation("Discord client started successfully");

Log.LogInformation("Loading interaction modules...");
logger.LogInformation("Loading interaction modules...");

var modules = (await _interactions.AddModulesAsync(typeof(ModixBot).Assembly, _scope.ServiceProvider)).ToArray();
var modules = (await interactionService.AddModulesAsync(typeof(ModixBot).Assembly, _scope.ServiceProvider)).ToArray();

foreach (var guild in _client.Guilds)
foreach (var guild in discordSocketClient.Guilds)
{
var commands = await _interactions.AddModulesToGuildAsync(guild, deleteMissing: true, modules);
var commands = await interactionService.AddModulesToGuildAsync(guild, deleteMissing: true, modules);
}

Log.LogInformation("{Modules} interaction modules loaded.", modules.Length);
Log.LogInformation("Loaded {SlashCommands} slash commands.", modules.SelectMany(x => x.SlashCommands).Count());
Log.LogInformation("Loaded {ContextCommands} context commands.", modules.SelectMany(x => x.ContextCommands).Count());
Log.LogInformation("Loaded {ModalCommands} modal commands.", modules.SelectMany(x => x.ModalCommands).Count());
Log.LogInformation("Loaded {ComponentCommands} component commands.", modules.SelectMany(x => x.ComponentCommands).Count());
logger.LogInformation("{Modules} interaction modules loaded", modules.Length);
logger.LogInformation("Loaded {SlashCommands} slash commands", modules.SelectMany(x => x.SlashCommands).Count());
logger.LogInformation("Loaded {ContextCommands} context commands", modules.SelectMany(x => x.ContextCommands).Count());
logger.LogInformation("Loaded {ModalCommands} modal commands", modules.SelectMany(x => x.ModalCommands).Count());
logger.LogInformation("Loaded {ComponentCommands} component commands", modules.SelectMany(x => x.ComponentCommands).Count());

await Task.Delay(-1);
await Task.Delay(-1, stoppingToken);
}
catch (Exception ex)
{
Log.LogError(ex, "An error occurred while attempting to start the background service.");
logger.LogError(ex, "An error occurred while attempting to start the background service");

try
{
OnStopping();

Log.LogInformation("Logging out of Discord.");
await _client.LogoutAsync();
logger.LogInformation("Logging out of Discord");
await discordSocketClient.LogoutAsync();
}
finally
{
Expand All @@ -139,16 +118,16 @@ protected override async Task ExecuteAsync(CancellationToken stoppingToken)
throw;
}

return;

void OnStopping()
{
Log.LogInformation("Stopping background service.");
logger.LogInformation("Stopping background service");

_client.Disconnected -= OnDisconnect;
_client.LatencyUpdated -= OnLatencyUpdated;
UnregisterClientHandlers();

_client.Log -= _serilogAdapter.HandleLog;
_commands.Log -= _serilogAdapter.HandleLog;
_restClient.Log -= _serilogAdapter.HandleLog;
commandService.Log -= discordSerilogAdapter.HandleLog;
discordRestClient.Log -= discordSerilogAdapter.HandleLog;

foreach (var context in _commandScopes.Keys)
{
Expand All @@ -160,7 +139,7 @@ void OnStopping()

private Task OnLatencyUpdated(int arg1, int arg2)
{
if (_env.IsProduction())
if (hostEnvironment.IsProduction())
{
return File.WriteAllTextAsync("healthcheck.txt", DateTimeOffset.UtcNow.ToString("o"));
}
Expand All @@ -174,62 +153,83 @@ private Task OnDisconnect(Exception ex)
// don't need to worry about handling this ourselves
if(ex is GatewayReconnectException)
{
Log.LogInformation("Received gateway reconnect");
logger.LogInformation("Received gateway reconnect");
return Task.CompletedTask;
}

Log.LogInformation(ex, "The bot disconnected unexpectedly. Stopping the application.");
_applicationLifetime.StopApplication();
logger.LogInformation(ex, "The bot disconnected unexpectedly. Stopping the application");
hostApplicationLifetime.StopApplication();
return Task.CompletedTask;
}

public override void Dispose()
{
try
{
// If the service is currently running, this will cancel the cancellation token that was passed into
// our ExecuteAsync method, unregistering our event handlers for us.
base.Dispose();
}
finally
{
_scope?.Dispose();
_client.Dispose();
_restClient.Dispose();
}
}

private async Task StartClient(CancellationToken cancellationToken)
{
var whenReadySource = new TaskCompletionSource<object>();
_whenReadySource = new TaskCompletionSource<object>();

try
{
_client.Ready += OnClientReady;

cancellationToken.ThrowIfCancellationRequested();

await _client.LoginAsync(TokenType.Bot, _config.DiscordToken);
await _client.StartAsync();
await discordSocketClient.LoginAsync(TokenType.Bot, modixConfig.Value.DiscordToken);
await discordSocketClient.StartAsync();

await _restClient.LoginAsync(TokenType.Bot, _config.DiscordToken);
await discordRestClient.LoginAsync(TokenType.Bot, modixConfig.Value.DiscordToken);

await whenReadySource.Task;
await _whenReadySource.Task;
}
catch (Exception)
{
_client.Ready -= OnClientReady;

UnregisterClientHandlers();
throw;
}
}

async Task OnClientReady()
{
Log.LogTrace("Discord client is ready. Setting game status.");
_client.Ready -= OnClientReady;
await _client.SetGameAsync(_config.WebsiteBaseUrl);
private void UnregisterClientHandlers()
{
discordSocketClient.LatencyUpdated -= OnLatencyUpdated;
discordSocketClient.Disconnected -= OnDisconnect;
discordSocketClient.Log -= discordSerilogAdapter.HandleLog;

discordSocketClient.Ready -= OnClientReady;

discordSocketClient.MessageReceived -= OnMessageReceived;
discordSocketClient.MessageUpdated -= OnMessageUpdated;
}

private async Task OnClientReady()
{
await discordSocketClient.SetGameAsync(modixConfig.Value.WebsiteBaseUrl);
_whenReadySource.SetResult(null);
}

private async Task OnMessageReceived(SocketMessage arg)
{
using var scope = serviceProvider.CreateScope();
var mediator = scope.ServiceProvider.GetRequiredService<IMediator>();
await mediator.Publish(new MessageReceivedNotificationV3(arg));
}

private async Task OnMessageUpdated(Cacheable<IMessage, ulong> cachedMessage, SocketMessage newMessage, ISocketMessageChannel channel)
{
using var scope = serviceProvider.CreateScope();
var mediator = scope.ServiceProvider.GetRequiredService<IMediator>();
await mediator.Publish(new MessageUpdatedNotificationV3(cachedMessage, newMessage, channel));
}

whenReadySource.SetResult(null);
public override void Dispose()
{
try
{
// If the service is currently running, this will cancel the cancellation token that was passed into
// our ExecuteAsync method, unregistering our event handlers for us.
base.Dispose();
}
finally
{
_scope?.Dispose();
discordSocketClient.Dispose();
discordRestClient.Dispose();
}
}
}
Expand Down
4 changes: 2 additions & 2 deletions src/Modix.Bot/Modules/DocumentationModule.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,14 @@ namespace Modix.Modules
[HelpTags("docs")]
public class DocumentationModule : InteractionModuleBase
{
private readonly IAutoRemoveMessageService _autoRemoveMessageService;
private readonly AutoRemoveMessageService _autoRemoveMessageService;
private readonly DocumentationService _documentationService;

// lang=regex
private const string QueryPattern = "^[0-9A-Za-z.<>]$";

public DocumentationModule(DocumentationService documentationService,
IAutoRemoveMessageService autoRemoveMessageService)
AutoRemoveMessageService autoRemoveMessageService)
{
_documentationService = documentationService;
_autoRemoveMessageService = autoRemoveMessageService;
Expand Down
4 changes: 2 additions & 2 deletions src/Modix.Bot/Modules/IlModule.cs
Original file line number Diff line number Diff line change
Expand Up @@ -23,12 +23,12 @@ public class IlModule : ModuleBase
private const string DefaultIlRemoteUrl = "http://csdiscord-repl-service:31337/Il";
private readonly string _ilUrl;
private readonly PasteService _pasteService;
private readonly IAutoRemoveMessageService _autoRemoveMessageService;
private readonly AutoRemoveMessageService _autoRemoveMessageService;
private readonly IHttpClientFactory _httpClientFactory;

public IlModule(
PasteService pasteService,
IAutoRemoveMessageService autoRemoveMessageService,
AutoRemoveMessageService autoRemoveMessageService,
IHttpClientFactory httpClientFactory,
IOptions<ModixConfig> modixConfig)
{
Expand Down
4 changes: 2 additions & 2 deletions src/Modix.Bot/Modules/LegacyLinkModule.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,9 @@ namespace Modix.Bot.Modules
[Summary("Commands for working with links.")]
public class LegacyLinkModule : ModuleBase
{
private readonly IAutoRemoveMessageService _autoRemoveMessageService;
private readonly AutoRemoveMessageService _autoRemoveMessageService;

public LegacyLinkModule(IAutoRemoveMessageService autoRemoveMessageService)
public LegacyLinkModule(AutoRemoveMessageService autoRemoveMessageService)
{
_autoRemoveMessageService = autoRemoveMessageService;
}
Expand Down
4 changes: 2 additions & 2 deletions src/Modix.Bot/Modules/ReplModule.cs
Original file line number Diff line number Diff line change
Expand Up @@ -37,12 +37,12 @@ public class ReplModule : ModuleBase
private const string DefaultReplRemoteUrl = "http://csdiscord-repl-service:31337/Eval";
private readonly string _replUrl;
private readonly PasteService _pasteService;
private readonly IAutoRemoveMessageService _autoRemoveMessageService;
private readonly AutoRemoveMessageService _autoRemoveMessageService;
private readonly IHttpClientFactory _httpClientFactory;

public ReplModule(
PasteService pasteService,
IAutoRemoveMessageService autoRemoveMessageService,
AutoRemoveMessageService autoRemoveMessageService,
IHttpClientFactory httpClientFactory,
IOptions<ModixConfig> modixConfig)
{
Expand Down
Loading

0 comments on commit 20ad348

Please sign in to comment.