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

V3: MediatR, refactor message quoting, refactor startup #1035

Merged
merged 2 commits into from
Nov 13, 2024
Merged
Show file tree
Hide file tree
Changes from all 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
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
Loading