From 9e15f557860ca4a4d2ef8ba589eb2ebf57c14a2f Mon Sep 17 00:00:00 2001 From: Patrick Klaeren Date: Wed, 13 Nov 2024 17:38:25 +0000 Subject: [PATCH] Introduces MediatR, refactors quote service/handler. Introduces more primary constructors, namespace changes --- .editorconfig | 2 +- src/Modix.Bot/Modix.Bot.csproj | 1 + src/Modix.Bot/ModixBot.cs | 214 ++++++++--------- src/Modix.Bot/Modules/DocumentationModule.cs | 4 +- src/Modix.Bot/Modules/IlModule.cs | 4 +- src/Modix.Bot/Modules/LegacyLinkModule.cs | 4 +- src/Modix.Bot/Modules/ReplModule.cs | 4 +- src/Modix.Bot/Modules/SharpLabModule.cs | 4 +- src/Modix.Bot/Modules/UserInfoModule.cs | 7 +- .../MessageReceivedNotificationV3.cs | 10 + .../MessageUpdatedNotificationV3.cs | 12 + .../MessageQuotes/MessageQuoteEmbedHelper.cs | 170 +++++++++++++ .../MessageQuotes/MessageQuoteResponder.cs | 120 +++++++++ .../Responders}/StarboardHandler.cs | 12 +- .../AutoRemoveMessageHandler.cs | 4 +- .../AutoRemoveMessageService.cs | 90 ++----- .../AutoRemoveMessageSetup.cs | 2 +- src/Modix.Services/Modix.Services.csproj | 1 + .../Quote/MessageLinkBehavior.cs | 147 ------------ src/Modix.Services/Quote/QuoteService.cs | 178 -------------- .../Starboard/StarboardSetup.cs | 4 +- .../Extensions/ServiceCollectionExtensions.cs | 227 +++++++++--------- src/Modix/Program.cs | 1 + 23 files changed, 579 insertions(+), 643 deletions(-) create mode 100644 src/Modix.Bot/Notifications/MessageReceivedNotificationV3.cs create mode 100644 src/Modix.Bot/Notifications/MessageUpdatedNotificationV3.cs create mode 100644 src/Modix.Bot/Responders/MessageQuotes/MessageQuoteEmbedHelper.cs create mode 100644 src/Modix.Bot/Responders/MessageQuotes/MessageQuoteResponder.cs rename src/{Modix.Services/Starboard => Modix.Bot/Responders}/StarboardHandler.cs (92%) delete mode 100644 src/Modix.Services/Quote/MessageLinkBehavior.cs delete mode 100644 src/Modix.Services/Quote/QuoteService.cs diff --git a/.editorconfig b/.editorconfig index 0833e25b4..26b2dd1eb 100644 --- a/.editorconfig +++ b/.editorconfig @@ -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 diff --git a/src/Modix.Bot/Modix.Bot.csproj b/src/Modix.Bot/Modix.Bot.csproj index 6d0eb7882..0b9b582f1 100644 --- a/src/Modix.Bot/Modix.Bot.csproj +++ b/src/Modix.Bot/Modix.Bot.csproj @@ -4,6 +4,7 @@ + diff --git a/src/Modix.Bot/ModixBot.cs b/src/Modix.Bot/ModixBot.cs index f453aacf7..5e08e3b61 100644 --- a/src/Modix.Bot/ModixBot.cs +++ b/src/Modix.Bot/ModixBot.cs @@ -10,78 +10,58 @@ 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, + CommandService commandService, + InteractionService interactionService, + DiscordSerilogAdapter discordSerilogAdapter, + IHostApplicationLifetime hostApplicationLifetime, + IServiceProvider serviceProvider, + ILogger 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 _commandScopes = new(); - - public ModixBot( - DiscordSocketClient discordClient, - DiscordRestClient restClient, - IOptions modixConfig, - CommandService commandService, - InteractionService interactions, - DiscordSerilogAdapter serilogAdapter, - IHostApplicationLifetime applicationLifetime, - IServiceProvider serviceProvider, - ILogger 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 Log { get; } + private TaskCompletionSource _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 @@ -89,46 +69,45 @@ protected override async Task ExecuteAsync(CancellationToken stoppingToken) // 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 { @@ -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) { @@ -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")); } @@ -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(); + _whenReadySource = new TaskCompletionSource(); 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(); + await mediator.Publish(new MessageReceivedNotificationV3(arg)); + } + + private async Task OnMessageUpdated(Cacheable cachedMessage, SocketMessage newMessage, ISocketMessageChannel channel) + { + using var scope = serviceProvider.CreateScope(); + var mediator = scope.ServiceProvider.GetRequiredService(); + 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(); } } } diff --git a/src/Modix.Bot/Modules/DocumentationModule.cs b/src/Modix.Bot/Modules/DocumentationModule.cs index da60b6140..8ecc754c9 100644 --- a/src/Modix.Bot/Modules/DocumentationModule.cs +++ b/src/Modix.Bot/Modules/DocumentationModule.cs @@ -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; diff --git a/src/Modix.Bot/Modules/IlModule.cs b/src/Modix.Bot/Modules/IlModule.cs index 7841e0246..e1d9b3590 100644 --- a/src/Modix.Bot/Modules/IlModule.cs +++ b/src/Modix.Bot/Modules/IlModule.cs @@ -23,12 +23,12 @@ public class IlModule : ModuleBase private const string DefaultIlRemoteUrl = "http://csdiscord-repl-service:31337/Il"; private readonly string _ilUrl; private readonly CodePasteService _pasteService; - private readonly IAutoRemoveMessageService _autoRemoveMessageService; + private readonly AutoRemoveMessageService _autoRemoveMessageService; private readonly IHttpClientFactory _httpClientFactory; public IlModule( CodePasteService pasteService, - IAutoRemoveMessageService autoRemoveMessageService, + AutoRemoveMessageService autoRemoveMessageService, IHttpClientFactory httpClientFactory, IOptions modixConfig) { diff --git a/src/Modix.Bot/Modules/LegacyLinkModule.cs b/src/Modix.Bot/Modules/LegacyLinkModule.cs index 70e48476f..86732e117 100644 --- a/src/Modix.Bot/Modules/LegacyLinkModule.cs +++ b/src/Modix.Bot/Modules/LegacyLinkModule.cs @@ -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; } diff --git a/src/Modix.Bot/Modules/ReplModule.cs b/src/Modix.Bot/Modules/ReplModule.cs index 60338f59a..ecdd81280 100644 --- a/src/Modix.Bot/Modules/ReplModule.cs +++ b/src/Modix.Bot/Modules/ReplModule.cs @@ -37,12 +37,12 @@ public class ReplModule : ModuleBase private const string DefaultReplRemoteUrl = "http://csdiscord-repl-service:31337/Eval"; private readonly string _replUrl; private readonly CodePasteService _pasteService; - private readonly IAutoRemoveMessageService _autoRemoveMessageService; + private readonly AutoRemoveMessageService _autoRemoveMessageService; private readonly IHttpClientFactory _httpClientFactory; public ReplModule( CodePasteService pasteService, - IAutoRemoveMessageService autoRemoveMessageService, + AutoRemoveMessageService autoRemoveMessageService, IHttpClientFactory httpClientFactory, IOptions modixConfig) { diff --git a/src/Modix.Bot/Modules/SharpLabModule.cs b/src/Modix.Bot/Modules/SharpLabModule.cs index e3f66d67f..af2f5e8c5 100644 --- a/src/Modix.Bot/Modules/SharpLabModule.cs +++ b/src/Modix.Bot/Modules/SharpLabModule.cs @@ -16,9 +16,9 @@ namespace Modix.Bot.Modules [ModuleHelp("SharpLab", "Commands for working with SharpLab.")] public class SharpLabModule : InteractionModuleBase { - private readonly IAutoRemoveMessageService _autoRemoveMessageService; + private readonly AutoRemoveMessageService _autoRemoveMessageService; - public SharpLabModule(IAutoRemoveMessageService autoRemoveMessageService) + public SharpLabModule(AutoRemoveMessageService autoRemoveMessageService) { _autoRemoveMessageService = autoRemoveMessageService; } diff --git a/src/Modix.Bot/Modules/UserInfoModule.cs b/src/Modix.Bot/Modules/UserInfoModule.cs index d37581660..7bb5d87af 100644 --- a/src/Modix.Bot/Modules/UserInfoModule.cs +++ b/src/Modix.Bot/Modules/UserInfoModule.cs @@ -43,9 +43,8 @@ public class UserInfoModule : InteractionModuleBase private readonly IPromotionsService _promotionsService; private readonly IImageService _imageService; private readonly ModixConfig _config; - private readonly IAutoRemoveMessageService _autoRemoveMessageService; + private readonly AutoRemoveMessageService _autoRemoveMessageService; - //optimization: UtcNow is slow and the module is created per-request private readonly DateTime _utcNow = DateTime.UtcNow; public UserInfoModule( @@ -58,9 +57,9 @@ public UserInfoModule( IPromotionsService promotionsService, IImageService imageService, IOptions config, - IAutoRemoveMessageService autoRemoveMessageService) + AutoRemoveMessageService autoRemoveMessageService) { - _log = logger ?? new NullLogger(); + _log = logger; _userService = userService; _moderationService = moderationService; _authorizationService = authorizationService; diff --git a/src/Modix.Bot/Notifications/MessageReceivedNotificationV3.cs b/src/Modix.Bot/Notifications/MessageReceivedNotificationV3.cs new file mode 100644 index 000000000..4717c01cb --- /dev/null +++ b/src/Modix.Bot/Notifications/MessageReceivedNotificationV3.cs @@ -0,0 +1,10 @@ +using Discord; +using Discord.WebSocket; +using MediatR; + +namespace Modix.Bot.Notifications; + +public class MessageReceivedNotificationV3(SocketMessage message) : INotification +{ + public IMessage Message { get; } = message; +} diff --git a/src/Modix.Bot/Notifications/MessageUpdatedNotificationV3.cs b/src/Modix.Bot/Notifications/MessageUpdatedNotificationV3.cs new file mode 100644 index 000000000..43d8a42ee --- /dev/null +++ b/src/Modix.Bot/Notifications/MessageUpdatedNotificationV3.cs @@ -0,0 +1,12 @@ +using Discord; +using Discord.WebSocket; +using MediatR; + +namespace Modix.Bot.Notifications; + +public class MessageUpdatedNotificationV3(Cacheable cachedMessage, SocketMessage newMessage, ISocketMessageChannel channel) : INotification +{ + public Cacheable Cached { get; } = cachedMessage; + public SocketMessage Message { get; } = newMessage; + public ISocketMessageChannel Channel { get; } = channel; +} diff --git a/src/Modix.Bot/Responders/MessageQuotes/MessageQuoteEmbedHelper.cs b/src/Modix.Bot/Responders/MessageQuotes/MessageQuoteEmbedHelper.cs new file mode 100644 index 000000000..2f3df42a0 --- /dev/null +++ b/src/Modix.Bot/Responders/MessageQuotes/MessageQuoteEmbedHelper.cs @@ -0,0 +1,170 @@ +using System; +using System.Linq; +using System.Threading.Tasks; +using Discord; +using Humanizer.Bytes; +using Modix.Services.AutoRemoveMessage; +using Modix.Services.Utilities; + +namespace Modix.Bot.Responders.MessageQuotes; + +public class MessageQuoteEmbedHelper(AutoRemoveMessageService autoRemoveMessageService) +{ + public async Task BuildRemovableEmbed(IMessage message, IUser executingUser, + Func> callback) + { + var embed = BuildQuoteEmbed(message, executingUser); + + if (callback is null || embed is null) + { + return; + } + + await autoRemoveMessageService.RegisterRemovableMessageAsync(executingUser, embed, + async (e) => await callback.Invoke(e)); + } + + public static EmbedBuilder BuildQuoteEmbed(IMessage message, IUser executingUser) + { + if (IsQuote(message)) + { + return null; + } + + var embed = new EmbedBuilder(); + + if (TryAddRichEmbed(message, executingUser, ref embed)) + { + return embed; + } + + if (message.Attachments.Any(x => x.IsSpoiler()) + || message.Embeds.Any() && FormatUtilities.ContainsSpoiler(message.Content)) + { + embed.AddField("Spoiler warning", "The quoted message contains spoilered content."); + } + else if (!TryAddImageAttachment(message, embed)) + { + if (!TryAddImageEmbed(message, embed)) + { + if (!TryAddThumbnailEmbed(message, embed)) + { + TryAddOtherAttachment(message, embed); + } + } + } + + AddContent(message, embed); + AddOtherEmbed(message, embed); + AddActivity(message, embed); + AddMeta(message, executingUser, embed); + + return embed; + } + + private static bool TryAddImageAttachment(IMessage message, EmbedBuilder embed) + { + var firstAttachment = message.Attachments.FirstOrDefault(); + + if (firstAttachment?.Height is null) + return false; + + embed.WithImageUrl(firstAttachment.Url); + + return true; + } + + private static void TryAddOtherAttachment(IMessage message, EmbedBuilder embed) + { + var firstAttachment = message.Attachments.FirstOrDefault(); + + if (firstAttachment == null) + return; + + embed.AddField($"Attachment (Size: {new ByteSize(firstAttachment.Size)})", firstAttachment.Url); + } + + private static bool TryAddImageEmbed(IMessage message, EmbedBuilder embed) + { + var imageEmbed = message.Embeds.Select(x => x.Image).FirstOrDefault(x => x is { }); + + if (imageEmbed is null) + return false; + + embed.WithImageUrl(imageEmbed.Value.Url); + + return true; + } + + private static bool TryAddThumbnailEmbed(IMessage message, EmbedBuilder embed) + { + var thumbnailEmbed = message.Embeds.Select(x => x.Thumbnail).FirstOrDefault(x => x is { }); + + if (thumbnailEmbed is null) + return false; + + embed.WithImageUrl(thumbnailEmbed.Value.Url); + + return true; + } + + private static bool TryAddRichEmbed(IMessage message, IUser executingUser, ref EmbedBuilder embed) + { + var firstEmbed = message.Embeds.FirstOrDefault(); + + if (firstEmbed?.Type != EmbedType.Rich) + return false; + + embed = message.Embeds + .First() + .ToEmbedBuilder() + .AddField("Quoted by", $"{executingUser.Mention} from **{message.GetJumpUrlForEmbed()}**", true); + + if (firstEmbed.Color == null) + { + embed.Color = Color.DarkGrey; + } + + return true; + } + + private static void AddActivity(IMessage message, EmbedBuilder embed) + { + if (message.Activity == null) return; + + embed + .AddField("Invite Type", message.Activity.Type) + .AddField("Party Id", message.Activity.PartyId); + } + + private static void AddOtherEmbed(IMessage message, EmbedBuilder embed) + { + if (message.Embeds.Count == 0) return; + + embed.AddField("Embed Type", message.Embeds.First().Type); + } + + private static void AddContent(IMessage message, EmbedBuilder embed) + { + if (string.IsNullOrWhiteSpace(message.Content)) return; + + embed.WithDescription(message.Content); + } + + private static void AddMeta(IMessage message, IUser executingUser, EmbedBuilder embed) + { + embed + .WithUserAsAuthor(message.Author) + .WithTimestamp(message.Timestamp) + .WithColor(new Color(95, 186, 125)) + .AddField("Quoted by", $"{executingUser.Mention} from **{message.GetJumpUrlForEmbed()}**", true); + } + + private static bool IsQuote(IMessage message) + { + return message + .Embeds? + .SelectMany(d => d.Fields) + .Any(d => d.Name == "Quoted by") == true; + } +} diff --git a/src/Modix.Bot/Responders/MessageQuotes/MessageQuoteResponder.cs b/src/Modix.Bot/Responders/MessageQuotes/MessageQuoteResponder.cs new file mode 100644 index 000000000..8c7c4dc51 --- /dev/null +++ b/src/Modix.Bot/Responders/MessageQuotes/MessageQuoteResponder.cs @@ -0,0 +1,120 @@ +using System; +using System.Text.RegularExpressions; +using System.Threading; +using System.Threading.Tasks; +using Discord; +using Discord.WebSocket; +using MediatR; +using Microsoft.Extensions.Logging; +using Modix.Bot.Notifications; + +namespace Modix.Bot.Responders.MessageQuotes; + +public class MessageQuoteResponder( + DiscordSocketClient discordSocketClient, MessageQuoteEmbedHelper messageQuoteEmbedHelper, + ILogger logger) + : INotificationHandler, INotificationHandler +{ + private static readonly Regex _pattern = new( + @"^(?[\s\S]*?)?(?<)?https?://(?:(?:ptb|canary)\.)?discord(app)?\.com/channels/(?\d+)/(?\d+)/(?\d+)/?(?>)?(?[\s\S]*)?$", + RegexOptions.Compiled | RegexOptions.IgnoreCase | RegexOptions.CultureInvariant); + + public async Task Handle(MessageUpdatedNotificationV3 notification, CancellationToken cancellationToken) + { + var cachedMessage = await notification.Cached.GetOrDownloadAsync(); + + if (_pattern.IsMatch(cachedMessage.Content)) + return; + + await OnMessageReceived(cachedMessage); + } + + public async Task Handle(MessageReceivedNotificationV3 notification, CancellationToken cancellationToken) + { + await OnMessageReceived(notification.Message); + } + + private async Task OnMessageReceived(IMessage message) + { + if (message is not IUserMessage userMessage + || userMessage.Author is not IGuildUser guildUser + || guildUser.IsBot + || guildUser.IsWebhook) + { + return; + } + + foreach (Match match in _pattern.Matches(message.Content)) + { + // check if the link is surrounded with < and >. This was too annoying to do in regex + if (match.Groups["OpenBrace"].Success && match.Groups["CloseBrace"].Success) + continue; + + if (ulong.TryParse(match.Groups["GuildId"].Value, out var guildId) + && ulong.TryParse(match.Groups["ChannelId"].Value, out var channelId) + && ulong.TryParse(match.Groups["MessageId"].Value, out var messageId)) + { + try + { + var channel = discordSocketClient.GetChannel(channelId); + + if (channel is ITextChannel { IsNsfw: true }) + { + return; + } + + if (channel is IGuildChannel guildChannel && + channel is ISocketMessageChannel messageChannel) + { + var currentUser = await guildChannel.Guild.GetCurrentUserAsync(); + var botChannelPermissions = currentUser.GetPermissions(guildChannel); + var userChannelPermissions = guildUser.GetPermissions(guildChannel); + + if (!botChannelPermissions.ViewChannel || !userChannelPermissions.ViewChannel) + { + return; + } + + var cacheMode = botChannelPermissions.ReadMessageHistory + ? CacheMode.AllowDownload + : CacheMode.CacheOnly; + + var msg = await messageChannel.GetMessageAsync(messageId, cacheMode); + + if (msg == null) + return; + + var success = await TrySendQuoteEmbed(msg, userMessage); + if (success + && string.IsNullOrEmpty(match.Groups["Prelink"].Value) + && string.IsNullOrEmpty(match.Groups["Postlink"].Value)) + { + await userMessage.DeleteAsync(); + } + } + } + catch (Exception ex) + { + logger.LogError(ex, "An error occurred while attempting to create a quote embed"); + } + } + } + } + + private async Task TrySendQuoteEmbed(IMessage message, IMessage source) + { + var success = false; + + await messageQuoteEmbedHelper.BuildRemovableEmbed(message, source.Author, + async embed => //If embed building is unsuccessful, this won't execute + { + success = true; + return await source.Channel.SendMessageAsync( + embed: embed.Build(), + messageReference: source.Reference, + allowedMentions: AllowedMentions.None); + }); + + return success; + } +} diff --git a/src/Modix.Services/Starboard/StarboardHandler.cs b/src/Modix.Bot/Responders/StarboardHandler.cs similarity index 92% rename from src/Modix.Services/Starboard/StarboardHandler.cs rename to src/Modix.Bot/Responders/StarboardHandler.cs index ccfde311b..22a19c39d 100644 --- a/src/Modix.Services/Starboard/StarboardHandler.cs +++ b/src/Modix.Bot/Responders/StarboardHandler.cs @@ -1,13 +1,14 @@ using System.Threading; using System.Threading.Tasks; using Discord; +using Modix.Bot.Responders.MessageQuotes; using Modix.Common.Messaging; using Modix.Data.Models.Core; using Modix.Services.Core; -using Modix.Services.Quote; +using Modix.Services.Starboard; using Modix.Services.Utilities; -namespace Modix.Services.Starboard +namespace Modix.Bot.Responders { public class StarboardHandler : INotificationHandler, @@ -15,16 +16,13 @@ public class StarboardHandler : { private readonly IStarboardService _starboardService; private readonly IDesignatedChannelService _designatedChannelService; - private readonly IQuoteService _quoteService; public StarboardHandler( IStarboardService starboardService, - IDesignatedChannelService designatedChannelService, - IQuoteService quoteService) + IDesignatedChannelService designatedChannelService) { _starboardService = starboardService; _designatedChannelService = designatedChannelService; - _quoteService = quoteService; } public Task HandleNotificationAsync(ReactionAddedNotification notification, CancellationToken cancellationToken) @@ -104,7 +102,7 @@ private string FormatContent(int reactionCount) private Embed GetStarEmbed(IUserMessage message, Color color) { var author = message.Author as IGuildUser; - var embed = _quoteService.BuildQuoteEmbed(message, author); + var embed = MessageQuoteEmbedHelper.BuildQuoteEmbed(message, author); if (embed is null) { diff --git a/src/Modix.Services/AutoRemoveMessage/AutoRemoveMessageHandler.cs b/src/Modix.Services/AutoRemoveMessage/AutoRemoveMessageHandler.cs index b008fab13..7dc12eb4b 100644 --- a/src/Modix.Services/AutoRemoveMessage/AutoRemoveMessageHandler.cs +++ b/src/Modix.Services/AutoRemoveMessage/AutoRemoveMessageHandler.cs @@ -18,7 +18,7 @@ public class AutoRemoveMessageHandler : { public AutoRemoveMessageHandler( IMemoryCache cache, - IAutoRemoveMessageService autoRemoveMessageService) + AutoRemoveMessageService autoRemoveMessageService) { Cache = cache; AutoRemoveMessageService = autoRemoveMessageService; @@ -75,7 +75,7 @@ public Task HandleNotificationAsync(RemovableMessageRemovedNotification notifica protected IMemoryCache Cache { get; } - protected IAutoRemoveMessageService AutoRemoveMessageService { get; } + protected AutoRemoveMessageService AutoRemoveMessageService { get; } private static object GetKey(ulong messageId) => new diff --git a/src/Modix.Services/AutoRemoveMessage/AutoRemoveMessageService.cs b/src/Modix.Services/AutoRemoveMessage/AutoRemoveMessageService.cs index 3103cc2b1..121bfff81 100644 --- a/src/Modix.Services/AutoRemoveMessage/AutoRemoveMessageService.cs +++ b/src/Modix.Services/AutoRemoveMessage/AutoRemoveMessageService.cs @@ -1,87 +1,39 @@ using System; using System.Threading.Tasks; - using Discord; - using Modix.Common.Messaging; -namespace Modix.Services.AutoRemoveMessage -{ - /// - /// Defines a service used to track removable messages. - /// - public interface IAutoRemoveMessageService - { - /// - /// Registers a removable message with the service and adds an indicator for this to the provided embed. - /// - /// The user who can remove the message. - /// The embed to operate on - /// A callback that returns the to register as removable. The modified embed is provided with this callback. - /// - /// If the provided is null. - /// - /// A that will complete when the operation completes. - /// - Task RegisterRemovableMessageAsync(IUser user, EmbedBuilder embed, Func> callback); +namespace Modix.Services.AutoRemoveMessage; - /// - /// Registers a removable message with the service and adds an indicator for this to the provided embed. - /// - /// The users who can remove the message. - /// The embed to operate on - /// A callback that returns the to register as removable. The modified embed is provided with this callback. - /// - /// If the provided is null. - /// - /// A that will complete when the operation completes. - /// - Task RegisterRemovableMessageAsync(IUser[] users, EmbedBuilder embed, Func> callback); +public class AutoRemoveMessageService(IMessageDispatcher messageDispatcher) +{ + private const string FOOTER_MESSAGE = "React with ❌ to remove this embed."; - /// - /// Unregisters a removable message from the service. - /// - /// The removable message. - void UnregisterRemovableMessage(IMessage message); + public Task RegisterRemovableMessageAsync(IUser user, EmbedBuilder embed, + Func> callback) + { + return RegisterRemovableMessageAsync([user], embed, callback); } - /// - internal class AutoRemoveMessageService : IAutoRemoveMessageService + public async Task RegisterRemovableMessageAsync(IUser[] users, EmbedBuilder embed, + Func> callback) { - private const string _footerReactMessage = "React with ❌ to remove this embed."; + if (callback == null) + throw new ArgumentNullException(nameof(callback)); - public AutoRemoveMessageService(IMessageDispatcher messageDispatcher) + if (embed.Footer?.Text == null) { - MessageDispatcher = messageDispatcher; + embed.WithFooter(FOOTER_MESSAGE); } - - /// - public Task RegisterRemovableMessageAsync(IUser user, EmbedBuilder embed, Func> callback) - => RegisterRemovableMessageAsync(new[] { user }, embed, callback); - - /// - public async Task RegisterRemovableMessageAsync(IUser[] user, EmbedBuilder embed, Func> callback) + else if (!embed.Footer.Text.Contains(FOOTER_MESSAGE)) { - if (callback == null) - throw new ArgumentNullException(nameof(callback)); - - if (embed.Footer?.Text == null) - { - embed.WithFooter(_footerReactMessage); - } - else if (!embed.Footer.Text.Contains(_footerReactMessage)) - { - embed.Footer.Text += $" | {_footerReactMessage}"; - } - - var msg = await callback.Invoke(embed); - MessageDispatcher.Dispatch(new RemovableMessageSentNotification(msg, user)); + embed.Footer.Text += $" | {FOOTER_MESSAGE}"; } - /// - public void UnregisterRemovableMessage(IMessage message) - => MessageDispatcher.Dispatch(new RemovableMessageRemovedNotification(message)); - - internal protected IMessageDispatcher MessageDispatcher { get; } + var msg = await callback.Invoke(embed); + messageDispatcher.Dispatch(new RemovableMessageSentNotification(msg, users)); } + + public void UnregisterRemovableMessage(IMessage message) + => messageDispatcher.Dispatch(new RemovableMessageRemovedNotification(message)); } diff --git a/src/Modix.Services/AutoRemoveMessage/AutoRemoveMessageSetup.cs b/src/Modix.Services/AutoRemoveMessage/AutoRemoveMessageSetup.cs index 9363cda1c..a79b8f325 100644 --- a/src/Modix.Services/AutoRemoveMessage/AutoRemoveMessageSetup.cs +++ b/src/Modix.Services/AutoRemoveMessage/AutoRemoveMessageSetup.cs @@ -18,7 +18,7 @@ public static class AutoRemoveMessageSetup /// public static IServiceCollection AddAutoRemoveMessage(this IServiceCollection services) => services - .AddScoped() + .AddScoped() .AddScoped, AutoRemoveMessageHandler>() .AddScoped, AutoRemoveMessageHandler>() .AddScoped, AutoRemoveMessageHandler>(); diff --git a/src/Modix.Services/Modix.Services.csproj b/src/Modix.Services/Modix.Services.csproj index 131e08976..42ae63664 100644 --- a/src/Modix.Services/Modix.Services.csproj +++ b/src/Modix.Services/Modix.Services.csproj @@ -2,6 +2,7 @@ + diff --git a/src/Modix.Services/Quote/MessageLinkBehavior.cs b/src/Modix.Services/Quote/MessageLinkBehavior.cs deleted file mode 100644 index cbd85a60c..000000000 --- a/src/Modix.Services/Quote/MessageLinkBehavior.cs +++ /dev/null @@ -1,147 +0,0 @@ -using System; -using System.Text.RegularExpressions; -using System.Threading.Tasks; -using Discord; -using Discord.WebSocket; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; - -namespace Modix.Services.Quote -{ - public class MessageLinkBehavior : BehaviorBase - { - private static readonly Regex Pattern = new( - @"^(?[\s\S]*?)?(?<)?https?://(?:(?:ptb|canary)\.)?discord(app)?\.com/channels/(?\d+)/(?\d+)/(?\d+)/?(?>)?(?[\s\S]*)?$", - RegexOptions.Compiled | RegexOptions.IgnoreCase | RegexOptions.CultureInvariant); - - public MessageLinkBehavior(DiscordSocketClient discordClient, IServiceProvider serviceProvider) - : base(serviceProvider) - { - DiscordClient = discordClient; - Log = serviceProvider.GetRequiredService>(); - } - - private DiscordSocketClient DiscordClient { get; } - - private ILogger Log { get; } - - protected internal override Task OnStartingAsync() - { - DiscordClient.MessageReceived += OnMessageReceivedAsync; - DiscordClient.MessageUpdated += OnMessageUpdatedAsync; - - return Task.CompletedTask; - } - - protected internal override Task OnStoppedAsync() - { - DiscordClient.MessageReceived -= OnMessageReceivedAsync; - DiscordClient.MessageUpdated -= OnMessageUpdatedAsync; - - return Task.CompletedTask; - } - - private async Task OnMessageUpdatedAsync(Cacheable cached, SocketMessage message, ISocketMessageChannel channel) - { - var cachedMessage = await cached.GetOrDownloadAsync(); - - if (Pattern.IsMatch(cachedMessage.Content)) - return; - - await OnMessageReceivedAsync(message); - } - - private async Task OnMessageReceivedAsync(IMessage message) - { - if (message is not IUserMessage userMessage - || userMessage.Author is not IGuildUser guildUser - || guildUser.IsBot - || guildUser.IsWebhook) - { - return; - } - - foreach (Match match in Pattern.Matches(message.Content)) - { - // check if the link is surrounded with < and >. This was too annoying to do in regex - if (match.Groups["OpenBrace"].Success && match.Groups["CloseBrace"].Success) - continue; - - if (ulong.TryParse(match.Groups["GuildId"].Value, out var guildId) - && ulong.TryParse(match.Groups["ChannelId"].Value, out var channelId) - && ulong.TryParse(match.Groups["MessageId"].Value, out var messageId)) - { - try - { - var channel = DiscordClient.GetChannel(channelId); - - if (channel is ITextChannel { IsNsfw: true }) - { - return; - } - - if (channel is IGuildChannel guildChannel && - channel is ISocketMessageChannel messageChannel) - { - var currentUser = await guildChannel.Guild.GetCurrentUserAsync(); - var botChannelPermissions = currentUser.GetPermissions(guildChannel); - var userChannelPermissions = guildUser.GetPermissions(guildChannel); - - if (!botChannelPermissions.ViewChannel || !userChannelPermissions.ViewChannel) - { - return; - } - - var cacheMode = botChannelPermissions.ReadMessageHistory - ? CacheMode.AllowDownload - : CacheMode.CacheOnly; - - var msg = await messageChannel.GetMessageAsync(messageId, cacheMode); - - if (msg == null) - return; - - var success = await SendQuoteEmbedAsync(msg, userMessage); - if (success - && string.IsNullOrEmpty(match.Groups["Prelink"].Value) - && string.IsNullOrEmpty(match.Groups["Postlink"].Value)) - { - await userMessage.DeleteAsync(); - } - } - } - catch (Exception ex) - { - Log.LogError(ex, "An error occurred while attempting to create a quote embed."); - } - } - } - } - - /// - /// Creates a quote embed and sends the message to the same channel the message link is from. - /// Will also reply to the same message the is replying to. - /// - /// The message that will be quoted. - /// The message that contains the message link. - /// True when the the quote succeeds, otherwise False. - private async Task SendQuoteEmbedAsync(IMessage message, IMessage source) - { - var success = false; - await SelfExecuteRequest(async quoteService => - { - await quoteService.BuildRemovableEmbed(message, source.Author, - async embed => //If embed building is unsuccessful, this won't execute - { - success = true; - return await source.Channel.SendMessageAsync( - embed: embed.Build(), - messageReference: source.Reference, - allowedMentions: AllowedMentions.None); - }); - }); - - return success; - } - } -} diff --git a/src/Modix.Services/Quote/QuoteService.cs b/src/Modix.Services/Quote/QuoteService.cs deleted file mode 100644 index f5161c33f..000000000 --- a/src/Modix.Services/Quote/QuoteService.cs +++ /dev/null @@ -1,178 +0,0 @@ -using System; -using System.Linq; -using System.Threading.Tasks; -using Discord; -using Humanizer.Bytes; -using Modix.Services.AutoRemoveMessage; -using Modix.Services.Utilities; - -namespace Modix.Services.Quote -{ - public interface IQuoteService - { - /// - /// Build an embed quote for the given message. Returns null if the message could not be quoted. - /// - /// The message to quote - /// The user that is doing the quoting - EmbedBuilder BuildQuoteEmbed(IMessage message, IUser executingUser); - - Task BuildRemovableEmbed(IMessage message, IUser executingUser, Func> callback); - } - - public class QuoteService : IQuoteService - { - private readonly IAutoRemoveMessageService _autoRemoveMessageService; - - public QuoteService(IAutoRemoveMessageService autoRemoveMessageService) - { - _autoRemoveMessageService = autoRemoveMessageService; - } - - /// - public EmbedBuilder BuildQuoteEmbed(IMessage message, IUser executingUser) - { - if (IsQuote(message)) - { - return null; - } - - var embed = new EmbedBuilder(); - if (TryAddRichEmbed(message, executingUser, ref embed)) - { - return embed; - } - - if (message.Attachments.Any(x => x.IsSpoiler()) - || message.Embeds.Any() && FormatUtilities.ContainsSpoiler(message.Content)) - { - embed.AddField("Spoiler warning", "The quoted message contains spoilered content."); - } - else if (!TryAddImageAttachment(message, embed)) - if (!TryAddImageEmbed(message, embed)) - if (!TryAddThumbnailEmbed(message, embed)) - TryAddOtherAttachment(message, embed); - - AddContent(message, embed); - AddOtherEmbed(message, embed); - AddActivity(message, embed); - AddMeta(message, executingUser, embed); - - return embed; - } - - public async Task BuildRemovableEmbed(IMessage message, IUser executingUser, Func> callback) - { - var embed = BuildQuoteEmbed(message, executingUser); - - if(callback == null || embed == null) - { - return; - } - - await _autoRemoveMessageService.RegisterRemovableMessageAsync(executingUser, embed, - async (e) => await callback.Invoke(e)); - } - - private bool TryAddImageAttachment(IMessage message, EmbedBuilder embed) - { - var firstAttachment = message.Attachments.FirstOrDefault(); - if (firstAttachment == null || firstAttachment.Height == null) - return false; - - embed.WithImageUrl(firstAttachment.Url); - - return true; - } - - private bool TryAddOtherAttachment(IMessage message, EmbedBuilder embed) - { - var firstAttachment = message.Attachments.FirstOrDefault(); - if (firstAttachment == null) return false; - - embed.AddField($"Attachment (Size: {new ByteSize(firstAttachment.Size)})", firstAttachment.Url); - - return true; - } - - private bool TryAddImageEmbed(IMessage message, EmbedBuilder embed) - { - var imageEmbed = message.Embeds.Select(x => x.Image).FirstOrDefault(x => x is { }); - if (imageEmbed is null) - return false; - - embed.WithImageUrl(imageEmbed.Value.Url); - - return true; - } - - private bool TryAddThumbnailEmbed(IMessage message, EmbedBuilder embed) - { - var thumbnailEmbed = message.Embeds.Select(x => x.Thumbnail).FirstOrDefault(x => x is { }); - if (thumbnailEmbed is null) - return false; - - embed.WithImageUrl(thumbnailEmbed.Value.Url); - - return true; - } - - private bool TryAddRichEmbed(IMessage message, IUser executingUser, ref EmbedBuilder embed) - { - var firstEmbed = message.Embeds.FirstOrDefault(); - if (firstEmbed?.Type != EmbedType.Rich) { return false; } - - embed = message.Embeds - .First() - .ToEmbedBuilder() - .AddField("Quoted by", $"{executingUser.Mention} from **{message.GetJumpUrlForEmbed()}**", true); - - if (firstEmbed.Color == null) - { - embed.Color = Color.DarkGrey; - } - - return true; - } - - private void AddActivity(IMessage message, EmbedBuilder embed) - { - if (message.Activity == null) { return; } - - embed - .AddField("Invite Type", message.Activity.Type) - .AddField("Party Id", message.Activity.PartyId); - } - - private void AddOtherEmbed(IMessage message, EmbedBuilder embed) - { - if (message.Embeds.Count == 0) return; - - embed.AddField("Embed Type", message.Embeds.First().Type); - } - - private void AddContent(IMessage message, EmbedBuilder embed) - { - if (string.IsNullOrWhiteSpace(message.Content)) return; - - embed.WithDescription(message.Content); - } - - private void AddMeta(IMessage message, IUser executingUser, EmbedBuilder embed) - { - embed - .WithUserAsAuthor(message.Author) - .WithTimestamp(message.Timestamp) - .WithColor(new Color(95, 186, 125)) - .AddField("Quoted by", $"{executingUser.Mention} from **{message.GetJumpUrlForEmbed()}**", true); - } - - private bool IsQuote(IMessage message) - { - return message - .Embeds? - .SelectMany(d => d.Fields) - .Any(d => d.Name == "Quoted by") == true; - } - } -} diff --git a/src/Modix.Services/Starboard/StarboardSetup.cs b/src/Modix.Services/Starboard/StarboardSetup.cs index f869dbbab..21dcf96f9 100644 --- a/src/Modix.Services/Starboard/StarboardSetup.cs +++ b/src/Modix.Services/Starboard/StarboardSetup.cs @@ -10,8 +10,6 @@ public static class StarboardSetup { public static IServiceCollection AddStarboard(this IServiceCollection services) => services - .AddScoped() - .AddScoped, StarboardHandler>() - .AddScoped, StarboardHandler>(); + .AddScoped(); } } diff --git a/src/Modix/Extensions/ServiceCollectionExtensions.cs b/src/Modix/Extensions/ServiceCollectionExtensions.cs index 6f107f37e..64cb00288 100644 --- a/src/Modix/Extensions/ServiceCollectionExtensions.cs +++ b/src/Modix/Extensions/ServiceCollectionExtensions.cs @@ -1,21 +1,20 @@ using System; using System.Net; using System.Net.Http; - using Discord; using Discord.Commands; using Discord.Interactions; using Discord.Rest; using Discord.WebSocket; - using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Options; - -using Modix; using Modix.Behaviors; using Modix.Bot; using Modix.Bot.Behaviors; +using Modix.Bot.Responders; +using Modix.Bot.Responders.MessageQuotes; using Modix.Common; using Modix.Common.Messaging; using Modix.Data.Models.Core; @@ -31,106 +30,104 @@ using Modix.Services.Images; using Modix.Services.Moderation; using Modix.Services.Promotions; -using Modix.Services.Quote; using Modix.Services.Starboard; using Modix.Services.Tags; using Modix.Services.Utilities; using Modix.Services.Wikipedia; - using Polly; using Polly.Extensions.Http; -namespace Microsoft.Extensions.DependencyInjection +namespace Modix.Extensions; + +internal static class ServiceCollectionExtensions { - internal static class ServiceCollectionExtensions + public static IServiceCollection AddModixHttpClients(this IServiceCollection services) { - public static IServiceCollection AddModixHttpClients(this IServiceCollection services) - { - services.AddHttpClient(); + services.AddHttpClient(); - services.AddHttpClient(HttpClientNames.RetryOnTransientErrorPolicy) - .AddPolicyHandler(HttpPolicyExtensions.HandleTransientHttpError() - .WaitAndRetryAsync(2, retryAttempt => TimeSpan.FromSeconds(5))); + services.AddHttpClient(HttpClientNames.RetryOnTransientErrorPolicy) + .AddPolicyHandler(HttpPolicyExtensions.HandleTransientHttpError() + .WaitAndRetryAsync(2, retryAttempt => TimeSpan.FromSeconds(5))); - services.AddHttpClient(HttpClientNames.TimeoutFiveSeconds) - .ConfigureHttpClient(client => - { - client.Timeout = TimeSpan.FromSeconds(5); - }); + services.AddHttpClient(HttpClientNames.TimeoutFiveSeconds) + .ConfigureHttpClient(client => + { + client.Timeout = TimeSpan.FromSeconds(5); + }); - services.AddHttpClient(HttpClientNames.Timeout300ms) - .ConfigureHttpClient(client => - { - client.Timeout = TimeSpan.FromMilliseconds(300); - }); + services.AddHttpClient(HttpClientNames.Timeout300ms) + .ConfigureHttpClient(client => + { + client.Timeout = TimeSpan.FromMilliseconds(300); + }); - services.AddHttpClient(HttpClientNames.AutomaticGZipDecompression) - .ConfigurePrimaryHttpMessageHandler(() => + services.AddHttpClient(HttpClientNames.AutomaticGZipDecompression) + .ConfigurePrimaryHttpMessageHandler(() => new HttpClientHandler() { AutomaticDecompression = DecompressionMethods.GZip, }); - return services; - } + return services; + } - public static IServiceCollection AddModix( - this IServiceCollection services, - IConfiguration configuration) - { - services - .AddSingleton( - provider => new DiscordSocketClient(config: new DiscordSocketConfig - { - AlwaysDownloadUsers = true, - GatewayIntents = - GatewayIntents.GuildBans | // GUILD_BAN_ADD, GUILD_BAN_REMOVE - GatewayIntents.GuildMembers | // GUILD_MEMBER_ADD, GUILD_MEMBER_UPDATE, GUILD_MEMBER_REMOVE - GatewayIntents.GuildMessageReactions | // MESSAGE_REACTION_ADD, MESSAGE_REACTION_REMOVE, - // MESSAGE_REACTION_REMOVE_ALL, MESSAGE_REACTION_REMOVE_EMOJI - GatewayIntents.GuildMessages | // MESSAGE_CREATE, MESSAGE_UPDATE, MESSAGE_DELETE, MESSAGE_DELETE_BULK - GatewayIntents.Guilds | // GUILD_CREATE, GUILD_UPDATE, GUILD_DELETE, GUILD_ROLE_CREATE, - // GUILD_ROLE_UPDATE, GUILD_ROLE_DELETE, CHANNEL_CREATE, - // CHANNEL_UPDATE, CHANNEL_DELETE, CHANNEL_PINS_UPDATE - GatewayIntents.MessageContent, // MESSAGE_CONTENT - LogLevel = LogSeverity.Debug, - MessageCacheSize = provider - .GetRequiredService>() - .Value - .MessageCacheSize //needed to log deletions - })) - .AddSingleton(provider => provider.GetRequiredService()); - - services - .AddSingleton( - provider => new DiscordRestClient(config: new DiscordRestConfig + public static IServiceCollection AddModix( + this IServiceCollection services, + IConfiguration configuration) + { + services + .AddSingleton( + provider => new DiscordSocketClient(config: new DiscordSocketConfig + { + AlwaysDownloadUsers = true, + GatewayIntents = + GatewayIntents.GuildBans | // GUILD_BAN_ADD, GUILD_BAN_REMOVE + GatewayIntents.GuildMembers | // GUILD_MEMBER_ADD, GUILD_MEMBER_UPDATE, GUILD_MEMBER_REMOVE + GatewayIntents.GuildMessageReactions | // MESSAGE_REACTION_ADD, MESSAGE_REACTION_REMOVE, + // MESSAGE_REACTION_REMOVE_ALL, MESSAGE_REACTION_REMOVE_EMOJI + GatewayIntents.GuildMessages | // MESSAGE_CREATE, MESSAGE_UPDATE, MESSAGE_DELETE, MESSAGE_DELETE_BULK + GatewayIntents.Guilds | // GUILD_CREATE, GUILD_UPDATE, GUILD_DELETE, GUILD_ROLE_CREATE, + // GUILD_ROLE_UPDATE, GUILD_ROLE_DELETE, CHANNEL_CREATE, + // CHANNEL_UPDATE, CHANNEL_DELETE, CHANNEL_PINS_UPDATE + GatewayIntents.MessageContent, // MESSAGE_CONTENT + LogLevel = LogSeverity.Debug, + MessageCacheSize = provider + .GetRequiredService>() + .Value + .MessageCacheSize //needed to log deletions + })) + .AddSingleton(provider => provider.GetRequiredService()); + + services + .AddSingleton( + provider => new DiscordRestClient(config: new DiscordRestConfig + { + LogLevel = LogSeverity.Debug, + })); + + services.AddSingleton(_ => + { + var service = new CommandService( + new CommandServiceConfig { LogLevel = LogSeverity.Debug, - })); + DefaultRunMode = Discord.Commands.RunMode.Sync, + CaseSensitiveCommands = false, + SeparatorChar = ' ' + }); + + service.AddTypeReader(new EmoteTypeReader()); + service.AddTypeReader(new UserEntityTypeReader()); + service.AddTypeReader>(new AnyGuildMessageTypeReader()); + service.AddTypeReader(new TimeSpanTypeReader(), true); + service.AddTypeReader(new UserOrMessageAuthorEntityTypeReader()); + service.AddTypeReader(new UriTypeReader()); - services.AddSingleton(_ => - { - var service = new CommandService( - new CommandServiceConfig - { - LogLevel = LogSeverity.Debug, - DefaultRunMode = Discord.Commands.RunMode.Sync, - CaseSensitiveCommands = false, - SeparatorChar = ' ' - }); - - service.AddTypeReader(new EmoteTypeReader()); - service.AddTypeReader(new UserEntityTypeReader()); - service.AddTypeReader>(new AnyGuildMessageTypeReader()); - service.AddTypeReader(new TimeSpanTypeReader(), true); - service.AddTypeReader(new UserOrMessageAuthorEntityTypeReader()); - service.AddTypeReader(new UriTypeReader()); - - return service; - }) - .AddScoped, CommandListeningBehavior>(); - - services.AddSingleton(provider => + return service; + }) + .AddScoped, CommandListeningBehavior>(); + + services.AddSingleton(provider => { var socketClient = provider.GetRequiredService(); var service = new InteractionService(socketClient, new() @@ -148,37 +145,39 @@ public static IServiceCollection AddModix( }) .AddScoped, InteractionListeningBehavior>(); - services.AddSingleton(); - - services - .AddModixCommon(configuration) - .AddModixServices(configuration) - .AddModixBot(configuration) - .AddModixCore() - .AddModixModeration() - .AddModixPromotions() - .AddCodePaste() - .AddCommandHelp() - .AddGuildStats() - .AddModixTags() - .AddStarboard() - .AddAutoRemoveMessage() - .AddEmojiStats() - .AddImages(); - - services.AddScoped(); - services.AddSingleton(); - services.AddMemoryCache(); - - services.AddScoped(); - services.AddScoped(); - - services.AddScoped(); - services.AddScoped, PromotionLoggingHandler>(); - - services.AddHostedService(); - - return services; - } + services.AddSingleton(); + + services + .AddModixCommon(configuration) + .AddModixServices(configuration) + .AddModixBot(configuration) + .AddModixCore() + .AddModixModeration() + .AddModixPromotions() + .AddCodePaste() + .AddCommandHelp() + .AddGuildStats() + .AddModixTags() + .AddStarboard() + .AddAutoRemoveMessage() + .AddEmojiStats() + .AddImages(); + + services.AddMediatR(cfg => cfg.RegisterServicesFromAssembly(typeof(ModixBot).Assembly)); + services.AddScoped(); + services.AddScoped, StarboardHandler>(); + services.AddScoped, StarboardHandler>(); + + services.AddMemoryCache(); + + services.AddScoped(); + services.AddScoped(); + + services.AddScoped(); + services.AddScoped, PromotionLoggingHandler>(); + + services.AddHostedService(); + + return services; } } diff --git a/src/Modix/Program.cs b/src/Modix/Program.cs index 61013ce95..12be23b58 100644 --- a/src/Modix/Program.cs +++ b/src/Modix/Program.cs @@ -17,6 +17,7 @@ using Modix.Configuration; using Modix.Data; using Modix.Data.Models.Core; +using Modix.Extensions; using Modix.Services.CodePaste; using Modix.Services.Utilities; using Modix.Web;