From 6017a46f38fade1168a65f2e0a8161686866f05d Mon Sep 17 00:00:00 2001 From: Octol1ttle Date: Tue, 11 Jul 2023 13:43:00 +0500 Subject: [PATCH] Split responders into separate class files Signed-off-by: Octol1ttle --- Boyfriend.csproj | 1 + src/Boyfriend.cs | 6 +- src/Commands/AboutCommandGroup.cs | 4 - src/Commands/BanCommandGroup.cs | 4 - src/Commands/ClearCommandGroup.cs | 7 +- src/Commands/ErrorLoggingEvents.cs | 2 - src/Commands/KickCommandGroup.cs | 4 - src/Commands/MuteCommandGroup.cs | 4 - src/Commands/PingCommandGroup.cs | 4 - src/Commands/RemindCommandGroup.cs | 4 - src/Commands/SettingsCommandGroup.cs | 9 +- src/Data/GuildSettings.cs | 1 - src/Data/Options/BoolOption.cs | 5 +- src/Data/Options/IOption.cs | 4 +- src/Data/Options/LanguageOption.cs | 6 +- src/Data/Options/Option.cs | 8 +- src/Data/Options/SnowflakeOption.cs | 6 +- src/Data/Options/TimeSpanOption.cs | 1 - src/EventResponders.cs | 339 ------------------ src/Extensions.cs | 1 - src/InteractionResponders.cs | 3 - {locale => src}/Messages.Designer.cs | 2 +- src/Responders/GuildLoadedResponder.cs | 63 ++++ src/Responders/GuildMemberJoinedResponder.cs | 65 ++++ .../GuildMemberRolesUpdatedResponder.cs | 26 ++ src/Responders/MessageDeletedResponder.cs | 78 ++++ src/Responders/MessageEditedResponder.cs | 89 +++++ src/Responders/MessageReceivedResponder.cs | 35 ++ .../ScheduledEventCancelledResponder.cs | 45 +++ src/Services/GuildUpdateService.cs | 1 - 30 files changed, 434 insertions(+), 393 deletions(-) delete mode 100644 src/EventResponders.cs rename {locale => src}/Messages.Designer.cs (99%) create mode 100644 src/Responders/GuildLoadedResponder.cs create mode 100644 src/Responders/GuildMemberJoinedResponder.cs create mode 100644 src/Responders/GuildMemberRolesUpdatedResponder.cs create mode 100644 src/Responders/MessageDeletedResponder.cs create mode 100644 src/Responders/MessageEditedResponder.cs create mode 100644 src/Responders/MessageReceivedResponder.cs create mode 100644 src/Responders/ScheduledEventCancelledResponder.cs diff --git a/Boyfriend.csproj b/Boyfriend.csproj index f6cc7dc..b7d095b 100644 --- a/Boyfriend.csproj +++ b/Boyfriend.csproj @@ -21,6 +21,7 @@ + diff --git a/src/Boyfriend.cs b/src/Boyfriend.cs index d285213..8045a60 100644 --- a/src/Boyfriend.cs +++ b/src/Boyfriend.cs @@ -64,15 +64,19 @@ public class Boyfriend { }); services.AddTransient() + // Init .AddDiscordCaching() .AddDiscordCommands(true) + .AddInteractivity() + // Slash command event handlers .AddPreparationErrorEvent() .AddPostExecutionEvent() - .AddInteractivity() .AddInteractionGroup() + // Services .AddSingleton() .AddSingleton() .AddHostedService() + // Slash commands .AddCommandTree() .WithCommandGroup() .WithCommandGroup() diff --git a/src/Commands/AboutCommandGroup.cs b/src/Commands/AboutCommandGroup.cs index 32ce07d..af65b15 100644 --- a/src/Commands/AboutCommandGroup.cs +++ b/src/Commands/AboutCommandGroup.cs @@ -1,7 +1,6 @@ using System.ComponentModel; using System.Text; using Boyfriend.Data; -using Boyfriend.locale; using Boyfriend.Services; using Remora.Commands.Attributes; using Remora.Commands.Groups; @@ -12,9 +11,6 @@ using Remora.Discord.Extensions.Embeds; using Remora.Discord.Extensions.Formatting; using Remora.Results; -// ReSharper disable ClassNeverInstantiated.Global -// ReSharper disable UnusedMember.Global - namespace Boyfriend.Commands; /// diff --git a/src/Commands/BanCommandGroup.cs b/src/Commands/BanCommandGroup.cs index b209683..bd2439e 100644 --- a/src/Commands/BanCommandGroup.cs +++ b/src/Commands/BanCommandGroup.cs @@ -1,7 +1,6 @@ using System.ComponentModel; using System.Text; using Boyfriend.Data; -using Boyfriend.locale; using Boyfriend.Services; using Remora.Commands.Attributes; using Remora.Commands.Groups; @@ -15,9 +14,6 @@ using Remora.Discord.Extensions.Embeds; using Remora.Discord.Extensions.Formatting; using Remora.Results; -// ReSharper disable ClassNeverInstantiated.Global -// ReSharper disable UnusedMember.Global - namespace Boyfriend.Commands; /// diff --git a/src/Commands/ClearCommandGroup.cs b/src/Commands/ClearCommandGroup.cs index dd396c3..21d6119 100644 --- a/src/Commands/ClearCommandGroup.cs +++ b/src/Commands/ClearCommandGroup.cs @@ -1,8 +1,8 @@ using System.ComponentModel; using System.Text; using Boyfriend.Data; -using Boyfriend.locale; using Boyfriend.Services; +using JetBrains.Annotations; using Remora.Commands.Attributes; using Remora.Commands.Groups; using Remora.Discord.API.Abstractions.Objects; @@ -16,14 +16,12 @@ using Remora.Discord.Extensions.Formatting; using Remora.Rest.Core; using Remora.Results; -// ReSharper disable ClassNeverInstantiated.Global -// ReSharper disable UnusedMember.Global - namespace Boyfriend.Commands; /// /// Handles the command to clear messages in a channel: /clear. /// +[UsedImplicitly] public class ClearCommandGroup : CommandGroup { private readonly IDiscordRestChannelAPI _channelApi; private readonly ICommandContext _context; @@ -54,6 +52,7 @@ public class ClearCommandGroup : CommandGroup { [RequireDiscordPermission(DiscordPermission.ManageMessages)] [RequireBotDiscordPermissions(DiscordPermission.ManageMessages)] [Description("Remove multiple messages")] + [UsedImplicitly] public async Task ClearMessagesAsync( [Description("Number of messages to remove (2-100)")] [MinValue(2)] [MaxValue(100)] int amount) { diff --git a/src/Commands/ErrorLoggingEvents.cs b/src/Commands/ErrorLoggingEvents.cs index 30869b4..e35493e 100644 --- a/src/Commands/ErrorLoggingEvents.cs +++ b/src/Commands/ErrorLoggingEvents.cs @@ -3,8 +3,6 @@ using Remora.Discord.Commands.Contexts; using Remora.Discord.Commands.Services; using Remora.Results; -// ReSharper disable ClassNeverInstantiated.Global - namespace Boyfriend.Commands; /// diff --git a/src/Commands/KickCommandGroup.cs b/src/Commands/KickCommandGroup.cs index a095c47..eb06b50 100644 --- a/src/Commands/KickCommandGroup.cs +++ b/src/Commands/KickCommandGroup.cs @@ -1,6 +1,5 @@ using System.ComponentModel; using Boyfriend.Data; -using Boyfriend.locale; using Boyfriend.Services; using Remora.Commands.Attributes; using Remora.Commands.Groups; @@ -13,9 +12,6 @@ using Remora.Discord.Commands.Feedback.Services; using Remora.Discord.Extensions.Embeds; using Remora.Results; -// ReSharper disable ClassNeverInstantiated.Global -// ReSharper disable UnusedMember.Global - namespace Boyfriend.Commands; /// diff --git a/src/Commands/MuteCommandGroup.cs b/src/Commands/MuteCommandGroup.cs index 791519d..76a44d2 100644 --- a/src/Commands/MuteCommandGroup.cs +++ b/src/Commands/MuteCommandGroup.cs @@ -1,7 +1,6 @@ using System.ComponentModel; using System.Text; using Boyfriend.Data; -using Boyfriend.locale; using Boyfriend.Services; using Remora.Commands.Attributes; using Remora.Commands.Groups; @@ -15,9 +14,6 @@ using Remora.Discord.Extensions.Embeds; using Remora.Discord.Extensions.Formatting; using Remora.Results; -// ReSharper disable ClassNeverInstantiated.Global -// ReSharper disable UnusedMember.Global - namespace Boyfriend.Commands; /// diff --git a/src/Commands/PingCommandGroup.cs b/src/Commands/PingCommandGroup.cs index f25b435..9f336ca 100644 --- a/src/Commands/PingCommandGroup.cs +++ b/src/Commands/PingCommandGroup.cs @@ -1,6 +1,5 @@ using System.ComponentModel; using Boyfriend.Data; -using Boyfriend.locale; using Boyfriend.Services; using Remora.Commands.Attributes; using Remora.Commands.Groups; @@ -11,9 +10,6 @@ using Remora.Discord.Extensions.Embeds; using Remora.Discord.Gateway; using Remora.Results; -// ReSharper disable ClassNeverInstantiated.Global -// ReSharper disable UnusedMember.Global - namespace Boyfriend.Commands; /// diff --git a/src/Commands/RemindCommandGroup.cs b/src/Commands/RemindCommandGroup.cs index 8305ef5..430c1cd 100644 --- a/src/Commands/RemindCommandGroup.cs +++ b/src/Commands/RemindCommandGroup.cs @@ -1,6 +1,5 @@ using System.ComponentModel; using Boyfriend.Data; -using Boyfriend.locale; using Boyfriend.Services; using Remora.Commands.Attributes; using Remora.Commands.Groups; @@ -11,9 +10,6 @@ using Remora.Discord.Extensions.Embeds; using Remora.Discord.Extensions.Formatting; using Remora.Results; -// ReSharper disable ClassNeverInstantiated.Global -// ReSharper disable UnusedMember.Global - namespace Boyfriend.Commands; /// diff --git a/src/Commands/SettingsCommandGroup.cs b/src/Commands/SettingsCommandGroup.cs index 61c5786..6b6c594 100644 --- a/src/Commands/SettingsCommandGroup.cs +++ b/src/Commands/SettingsCommandGroup.cs @@ -2,7 +2,6 @@ using System.Text; using Boyfriend.Data; using Boyfriend.Data.Options; -using Boyfriend.locale; using Boyfriend.Services; using Remora.Commands.Attributes; using Remora.Commands.Groups; @@ -13,9 +12,6 @@ using Remora.Discord.Extensions.Embeds; using Remora.Discord.Extensions.Formatting; using Remora.Results; -// ReSharper disable ClassNeverInstantiated.Global -// ReSharper disable UnusedMember.Global - namespace Boyfriend.Commands; /// @@ -77,8 +73,7 @@ public class SettingsCommandGroup : CommandGroup { foreach (var option in AllOptions) { builder.Append(Markdown.InlineCode(option.Name)) .Append(": "); - var something = option.GetAsObject(cfg); - builder.AppendLine(Markdown.InlineCode(something.ToString()!)); + builder.AppendLine(option.Display(cfg)); } var embed = new EmbedBuilder().WithSmallTitle(Messages.SettingsListTitle, currentUser) @@ -131,7 +126,7 @@ public class SettingsCommandGroup : CommandGroup { builder.Append(Markdown.InlineCode(option.Name)) .Append($" {Messages.SettingIsNow} ") - .Append(Markdown.InlineCode(option.GetAsObject(cfg).ToString()!)); + .Append(Markdown.InlineCode(option.Display(cfg))); var embed = new EmbedBuilder().WithSmallTitle(Messages.SettingSuccessfullyChanged, currentUser) .WithDescription(builder.ToString()) diff --git a/src/Data/GuildSettings.cs b/src/Data/GuildSettings.cs index fc76f21..4e5e6cc 100644 --- a/src/Data/GuildSettings.cs +++ b/src/Data/GuildSettings.cs @@ -1,5 +1,4 @@ using Boyfriend.Data.Options; -using Boyfriend.locale; using Remora.Discord.API.Abstractions.Objects; namespace Boyfriend.Data; diff --git a/src/Data/Options/BoolOption.cs b/src/Data/Options/BoolOption.cs index 0a3992c..a8ee954 100644 --- a/src/Data/Options/BoolOption.cs +++ b/src/Data/Options/BoolOption.cs @@ -1,5 +1,4 @@ using System.Text.Json.Nodes; -using Boyfriend.locale; using Remora.Results; namespace Boyfriend.Data.Options; @@ -7,6 +6,10 @@ namespace Boyfriend.Data.Options; public class BoolOption : Option { public BoolOption(string name, bool defaultValue) : base(name, defaultValue) { } + public override string Display(JsonNode settings) { + return Get(settings) ? Messages.Yes : Messages.No; + } + public override Result Set(JsonNode settings, string from) { if (!TryParseBool(from, out var value)) return Result.FromError(new ArgumentInvalidError(nameof(from), Messages.InvalidSettingValue)); diff --git a/src/Data/Options/IOption.cs b/src/Data/Options/IOption.cs index 211c5b2..fc0f747 100644 --- a/src/Data/Options/IOption.cs +++ b/src/Data/Options/IOption.cs @@ -5,6 +5,6 @@ namespace Boyfriend.Data.Options; public interface IOption { string Name { get; } - object GetAsObject(JsonNode settings); - Result Set(JsonNode settings, string from); + string Display(JsonNode settings); + Result Set(JsonNode settings, string from); } diff --git a/src/Data/Options/LanguageOption.cs b/src/Data/Options/LanguageOption.cs index 7e741b2..6c4a49f 100644 --- a/src/Data/Options/LanguageOption.cs +++ b/src/Data/Options/LanguageOption.cs @@ -1,6 +1,6 @@ using System.Globalization; using System.Text.Json.Nodes; -using Boyfriend.locale; +using Remora.Discord.Extensions.Formatting; using Remora.Results; namespace Boyfriend.Data.Options; @@ -15,6 +15,10 @@ public class LanguageOption : Option { public LanguageOption(string name, string defaultValue) : base(name, CultureInfoCache[defaultValue]) { } + public override string Display(JsonNode settings) { + return Markdown.InlineCode(settings[Name]?.GetValue() ?? "en"); + } + /// public override CultureInfo Get(JsonNode settings) { var property = settings[Name]; diff --git a/src/Data/Options/Option.cs b/src/Data/Options/Option.cs index 77eace9..742d3a9 100644 --- a/src/Data/Options/Option.cs +++ b/src/Data/Options/Option.cs @@ -1,4 +1,5 @@ using System.Text.Json.Nodes; +using Remora.Discord.Extensions.Formatting; using Remora.Results; namespace Boyfriend.Data.Options; @@ -7,7 +8,8 @@ namespace Boyfriend.Data.Options; /// Represents an per-guild option. /// /// The type of the option. -public class Option : IOption { +public class Option : IOption +where T : notnull { internal readonly T DefaultValue; public Option(string name, T defaultValue) { @@ -17,8 +19,8 @@ public class Option : IOption { public string Name { get; } - public object GetAsObject(JsonNode settings) { - return Get(settings)!; + public virtual string Display(JsonNode settings) { + return Markdown.InlineCode(Get(settings).ToString()!); } /// diff --git a/src/Data/Options/SnowflakeOption.cs b/src/Data/Options/SnowflakeOption.cs index d50f833..f65065c 100644 --- a/src/Data/Options/SnowflakeOption.cs +++ b/src/Data/Options/SnowflakeOption.cs @@ -1,5 +1,5 @@ using System.Text.Json.Nodes; -using Boyfriend.locale; +using Remora.Discord.Extensions.Formatting; using Remora.Rest.Core; using Remora.Results; @@ -8,6 +8,10 @@ namespace Boyfriend.Data.Options; public class SnowflakeOption : Option { public SnowflakeOption(string name) : base(name, 0UL.ToSnowflake()) { } + public override string Display(JsonNode settings) { + return Name.EndsWith("Channel") ? Mention.Channel(Get(settings)) : Mention.Role(Get(settings)); + } + public override Snowflake Get(JsonNode settings) { var property = settings[Name]; return property != null ? property.GetValue().ToSnowflake() : DefaultValue; diff --git a/src/Data/Options/TimeSpanOption.cs b/src/Data/Options/TimeSpanOption.cs index 68b33ad..4be2439 100644 --- a/src/Data/Options/TimeSpanOption.cs +++ b/src/Data/Options/TimeSpanOption.cs @@ -1,5 +1,4 @@ using System.Text.Json.Nodes; -using Boyfriend.locale; using Remora.Commands.Parsers; using Remora.Results; diff --git a/src/EventResponders.cs b/src/EventResponders.cs deleted file mode 100644 index 0b5b309..0000000 --- a/src/EventResponders.cs +++ /dev/null @@ -1,339 +0,0 @@ -using Boyfriend.Data; -using Boyfriend.locale; -using Boyfriend.Services; -using DiffPlex.DiffBuilder; -using Microsoft.Extensions.Logging; -using Remora.Discord.API.Abstractions.Gateway.Events; -using Remora.Discord.API.Abstractions.Objects; -using Remora.Discord.API.Abstractions.Rest; -using Remora.Discord.Caching; -using Remora.Discord.Caching.Services; -using Remora.Discord.Extensions.Embeds; -using Remora.Discord.Extensions.Formatting; -using Remora.Discord.Gateway.Responders; -using Remora.Rest.Core; -using Remora.Results; - -// ReSharper disable UnusedType.Global - -namespace Boyfriend; - -/// -/// Handles sending a message to a guild that has just initialized if that guild -/// has enabled -/// -public class GuildCreateResponder : IResponder { - private readonly IDiscordRestChannelAPI _channelApi; - private readonly GuildDataService _dataService; - private readonly ILogger _logger; - private readonly IDiscordRestUserAPI _userApi; - - public GuildCreateResponder( - IDiscordRestChannelAPI channelApi, GuildDataService dataService, ILogger logger, - IDiscordRestUserAPI userApi) { - _channelApi = channelApi; - _dataService = dataService; - _logger = logger; - _userApi = userApi; - } - - public async Task RespondAsync(IGuildCreate gatewayEvent, CancellationToken ct = default) { - if (!gatewayEvent.Guild.IsT0) return Result.FromSuccess(); // Guild isn't IAvailableGuild - - var guild = gatewayEvent.Guild.AsT0; - _logger.LogInformation("Joined guild \"{Name}\"", guild.Name); - - var cfg = await _dataService.GetSettings(guild.ID, ct); - if (!GuildSettings.ReceiveStartupMessages.Get(cfg)) - return Result.FromSuccess(); - if (GuildSettings.PrivateFeedbackChannel.Get(cfg).Empty()) - return Result.FromSuccess(); - - var currentUserResult = await _userApi.GetCurrentUserAsync(ct); - if (!currentUserResult.IsDefined(out var currentUser)) return Result.FromError(currentUserResult); - - Messages.Culture = GuildSettings.Language.Get(cfg); - var i = Random.Shared.Next(1, 4); - - var embed = new EmbedBuilder().WithSmallTitle(currentUser.GetTag(), currentUser) - .WithTitle($"Beep{i}".Localized()) - .WithDescription(Messages.Ready) - .WithCurrentTimestamp() - .WithColour(ColorsList.Blue) - .Build(); - if (!embed.IsDefined(out var built)) return Result.FromError(embed); - - return (Result)await _channelApi.CreateMessageAsync( - GuildSettings.PrivateFeedbackChannel.Get(cfg), embeds: new[] { built }, ct: ct); - } -} - -/// -/// Handles logging the contents of a deleted message and the user who deleted the message -/// to a guild's if one is set. -/// -public class MessageDeletedResponder : IResponder { - private readonly IDiscordRestAuditLogAPI _auditLogApi; - private readonly IDiscordRestChannelAPI _channelApi; - private readonly GuildDataService _dataService; - private readonly IDiscordRestUserAPI _userApi; - - public MessageDeletedResponder( - IDiscordRestAuditLogAPI auditLogApi, IDiscordRestChannelAPI channelApi, - GuildDataService dataService, IDiscordRestUserAPI userApi) { - _auditLogApi = auditLogApi; - _channelApi = channelApi; - _dataService = dataService; - _userApi = userApi; - } - - public async Task RespondAsync(IMessageDelete gatewayEvent, CancellationToken ct = default) { - if (!gatewayEvent.GuildID.IsDefined(out var guildId)) return Result.FromSuccess(); - - var cfg = await _dataService.GetSettings(guildId, ct); - if (GuildSettings.PrivateFeedbackChannel.Get(cfg).Empty()) return Result.FromSuccess(); - - var messageResult = await _channelApi.GetChannelMessageAsync(gatewayEvent.ChannelID, gatewayEvent.ID, ct); - if (!messageResult.IsDefined(out var message)) return Result.FromError(messageResult); - if (string.IsNullOrWhiteSpace(message.Content)) return Result.FromSuccess(); - - var auditLogResult = await _auditLogApi.GetGuildAuditLogAsync( - guildId, actionType: AuditLogEvent.MessageDelete, limit: 1, ct: ct); - if (!auditLogResult.IsDefined(out var auditLogPage)) return Result.FromError(auditLogResult); - - var auditLog = auditLogPage.AuditLogEntries.Single(); - if (!auditLog.Options.IsDefined(out var options)) - return Result.FromError(new ArgumentNullError(nameof(auditLog.Options))); - - var user = message.Author; - if (options.ChannelID == gatewayEvent.ChannelID - && DateTimeOffset.UtcNow.Subtract(auditLog.ID.Timestamp).TotalSeconds <= 2) { - var userResult = await _userApi.GetUserAsync(auditLog.UserID!.Value, ct); - if (!userResult.IsDefined(out user)) return Result.FromError(userResult); - } - - Messages.Culture = GuildSettings.Language.Get(cfg); - - var embed = new EmbedBuilder() - .WithSmallTitle( - string.Format( - Messages.CachedMessageDeleted, - message.Author.GetTag()), message.Author) - .WithDescription( - $"{Mention.Channel(gatewayEvent.ChannelID)}\n{message.Content.InBlockCode()}") - .WithActionFooter(user) - .WithTimestamp(message.Timestamp) - .WithColour(ColorsList.Red) - .Build(); - if (!embed.IsDefined(out var built)) return Result.FromError(embed); - - return (Result)await _channelApi.CreateMessageAsync( - GuildSettings.PrivateFeedbackChannel.Get(cfg), embeds: new[] { built }, - allowedMentions: Boyfriend.NoMentions, ct: ct); - } -} - -/// -/// Handles logging the difference between an edited message's old and new content -/// to a guild's if one is set. -/// -public class MessageEditedResponder : IResponder { - private readonly CacheService _cacheService; - private readonly IDiscordRestChannelAPI _channelApi; - private readonly GuildDataService _dataService; - private readonly IDiscordRestUserAPI _userApi; - - public MessageEditedResponder( - CacheService cacheService, IDiscordRestChannelAPI channelApi, GuildDataService dataService, - IDiscordRestUserAPI userApi) { - _cacheService = cacheService; - _channelApi = channelApi; - _dataService = dataService; - _userApi = userApi; - } - - public async Task RespondAsync(IMessageUpdate gatewayEvent, CancellationToken ct = default) { - if (!gatewayEvent.GuildID.IsDefined(out var guildId)) - return Result.FromSuccess(); - var cfg = await _dataService.GetSettings(guildId, ct); - if (GuildSettings.PrivateFeedbackChannel.Get(cfg).Empty()) - return Result.FromSuccess(); - if (!gatewayEvent.Content.IsDefined(out var newContent)) - return Result.FromSuccess(); - if (!gatewayEvent.EditedTimestamp.IsDefined(out var timestamp)) - return Result.FromSuccess(); // The message wasn't actually edited - - if (!gatewayEvent.ChannelID.IsDefined(out var channelId)) - return Result.FromError(new ArgumentNullError(nameof(gatewayEvent.ChannelID))); - if (!gatewayEvent.ID.IsDefined(out var messageId)) - return Result.FromError(new ArgumentNullError(nameof(gatewayEvent.ID))); - - var cacheKey = new KeyHelpers.MessageCacheKey(channelId, messageId); - var messageResult = await _cacheService.TryGetValueAsync( - cacheKey, ct); - if (!messageResult.IsDefined(out var message)) return Result.FromError(messageResult); - if (message.Content == newContent) return Result.FromSuccess(); - - // Custom event responders are called earlier than responders responsible for message caching - // This means that subsequent edit logs may contain the wrong content - // We can work around this by evicting the message from the cache - await _cacheService.EvictAsync(cacheKey, ct); - // However, since we evicted the message, subsequent edits won't have a cached instance to work with - // Getting the message will put it back in the cache, resolving all issues - // We don't need to await this since the result is not needed - // NOTE: Because this is not awaited, there may be a race condition depending on how fast clients are able to edit their messages - // NOTE: Awaiting this might not even solve this if the same responder is called asynchronously - _ = _channelApi.GetChannelMessageAsync(channelId, messageId, ct); - - var currentUserResult = await _userApi.GetCurrentUserAsync(ct); - if (!currentUserResult.IsDefined(out var currentUser)) return Result.FromError(currentUserResult); - - var diff = InlineDiffBuilder.Diff(message.Content, newContent); - - Messages.Culture = GuildSettings.Language.Get(cfg); - - var embed = new EmbedBuilder() - .WithSmallTitle(string.Format(Messages.CachedMessageEdited, message.Author.GetTag()), message.Author) - .WithDescription($"https://discord.com/channels/{guildId}/{channelId}/{messageId}\n{diff.AsMarkdown()}") - .WithUserFooter(currentUser) - .WithTimestamp(timestamp.Value) - .WithColour(ColorsList.Yellow) - .Build(); - if (!embed.IsDefined(out var built)) return Result.FromError(embed); - - return (Result)await _channelApi.CreateMessageAsync( - GuildSettings.PrivateFeedbackChannel.Get(cfg), embeds: new[] { built }, - allowedMentions: Boyfriend.NoMentions, ct: ct); - } -} - -/// -/// Handles sending a guild's if one is set. -/// If is enabled, roles will be returned. -/// -/// -public class GuildMemberAddResponder : IResponder { - private readonly IDiscordRestChannelAPI _channelApi; - private readonly GuildDataService _dataService; - private readonly IDiscordRestGuildAPI _guildApi; - - public GuildMemberAddResponder( - IDiscordRestChannelAPI channelApi, GuildDataService dataService, IDiscordRestGuildAPI guildApi) { - _channelApi = channelApi; - _dataService = dataService; - _guildApi = guildApi; - } - - public async Task RespondAsync(IGuildMemberAdd gatewayEvent, CancellationToken ct = default) { - if (!gatewayEvent.User.IsDefined(out var user)) - return Result.FromError(new ArgumentNullError(nameof(gatewayEvent.User))); - var data = await _dataService.GetData(gatewayEvent.GuildID, ct); - var cfg = data.Settings; - if (GuildSettings.PublicFeedbackChannel.Get(cfg).Empty() - || GuildSettings.WelcomeMessage.Get(cfg) is "off" or "disable" or "disabled") - return Result.FromSuccess(); - if (GuildSettings.ReturnRolesOnRejoin.Get(cfg)) { - var result = await _guildApi.ModifyGuildMemberAsync( - gatewayEvent.GuildID, user.ID, - roles: data.GetMemberData(user.ID).Roles.ConvertAll(r => r.ToSnowflake()), ct: ct); - if (!result.IsSuccess) return Result.FromError(result.Error); - } - - Messages.Culture = GuildSettings.Language.Get(cfg); - var welcomeMessage = GuildSettings.WelcomeMessage.Get(cfg) is "default" or "reset" - ? Messages.DefaultWelcomeMessage - : GuildSettings.WelcomeMessage.Get(cfg); - - var guildResult = await _guildApi.GetGuildAsync(gatewayEvent.GuildID, ct: ct); - if (!guildResult.IsDefined(out var guild)) return Result.FromError(guildResult); - - var embed = new EmbedBuilder() - .WithSmallTitle(string.Format(welcomeMessage, user.GetTag(), guild.Name), user) - .WithGuildFooter(guild) - .WithTimestamp(gatewayEvent.JoinedAt) - .WithColour(ColorsList.Green) - .Build(); - if (!embed.IsDefined(out var built)) return Result.FromError(embed); - - return (Result)await _channelApi.CreateMessageAsync( - GuildSettings.PublicFeedbackChannel.Get(cfg), embeds: new[] { built }, - allowedMentions: Boyfriend.NoMentions, ct: ct); - } -} - -/// -/// Handles sending a notification when a scheduled event has been cancelled -/// in a guild's if one is set. -/// -public class GuildScheduledEventDeleteResponder : IResponder { - private readonly IDiscordRestChannelAPI _channelApi; - private readonly GuildDataService _dataService; - - public GuildScheduledEventDeleteResponder(IDiscordRestChannelAPI channelApi, GuildDataService dataService) { - _channelApi = channelApi; - _dataService = dataService; - } - - public async Task RespondAsync(IGuildScheduledEventDelete gatewayEvent, CancellationToken ct = default) { - var guildData = await _dataService.GetData(gatewayEvent.GuildID, ct); - guildData.ScheduledEvents.Remove(gatewayEvent.ID.Value); - - if (GuildSettings.EventNotificationChannel.Get(guildData.Settings).Empty()) - return Result.FromSuccess(); - - var embed = new EmbedBuilder() - .WithSmallTitle(string.Format(Messages.EventCancelled, gatewayEvent.Name)) - .WithDescription(":(") - .WithColour(ColorsList.Red) - .WithCurrentTimestamp() - .Build(); - - if (!embed.IsDefined(out var built)) return Result.FromError(embed); - - return (Result)await _channelApi.CreateMessageAsync( - GuildSettings.EventNotificationChannel.Get(guildData.Settings), embeds: new[] { built }, ct: ct); - } -} - -/// -/// Handles updating when a guild member is updated. -/// -public class GuildMemberUpdateResponder : IResponder { - private readonly GuildDataService _dataService; - - public GuildMemberUpdateResponder(GuildDataService dataService) { - _dataService = dataService; - } - - public async Task RespondAsync(IGuildMemberUpdate gatewayEvent, CancellationToken ct = default) { - var memberData = await _dataService.GetMemberData(gatewayEvent.GuildID, gatewayEvent.User.ID, ct); - memberData.Roles = gatewayEvent.Roles.ToList().ConvertAll(r => r.Value); - return Result.FromSuccess(); - } -} - -/// -/// Handles sending replies to easter egg messages. -/// -public class MessageCreateResponder : IResponder { - private readonly IDiscordRestChannelAPI _channelApi; - - public MessageCreateResponder(IDiscordRestChannelAPI channelApi) { - _channelApi = channelApi; - } - - public Task RespondAsync(IMessageCreate gatewayEvent, CancellationToken ct = default) { - _ = _channelApi.CreateMessageAsync( - gatewayEvent.ChannelID, ct: ct, content: gatewayEvent.Content switch { - "whoami" => "`nobody`", - "сука !!" => "`root`", - "воооо" => "`removing /...`", - "пон" => - "https://cdn.discordapp.com/attachments/837385840946053181/1087236080950055023/vUORS10xPaY-1.jpg", - "++++" => "#", - "осу" => "https://github.com/ppy/osu", - _ => default(Optional) - }); - return Task.FromResult(Result.FromSuccess()); - } -} diff --git a/src/Extensions.cs b/src/Extensions.cs index 0f9a786..eab0afc 100644 --- a/src/Extensions.cs +++ b/src/Extensions.cs @@ -1,7 +1,6 @@ using System.Diagnostics.CodeAnalysis; using System.Net; using System.Text; -using Boyfriend.locale; using DiffPlex.DiffBuilder.Model; using Remora.Discord.API; using Remora.Discord.API.Abstractions.Objects; diff --git a/src/InteractionResponders.cs b/src/InteractionResponders.cs index 231df31..49af44c 100644 --- a/src/InteractionResponders.cs +++ b/src/InteractionResponders.cs @@ -4,9 +4,6 @@ using Remora.Discord.Commands.Feedback.Services; using Remora.Discord.Interactivity; using Remora.Results; -// ReSharper disable ClassNeverInstantiated.Global -// ReSharper disable UnusedMember.Global - namespace Boyfriend; /// diff --git a/locale/Messages.Designer.cs b/src/Messages.Designer.cs similarity index 99% rename from locale/Messages.Designer.cs rename to src/Messages.Designer.cs index 8ab20f8..42a05be 100644 --- a/locale/Messages.Designer.cs +++ b/src/Messages.Designer.cs @@ -7,7 +7,7 @@ // //------------------------------------------------------------------------------ -namespace Boyfriend.locale { +namespace Boyfriend { [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")] [System.Diagnostics.DebuggerNonUserCodeAttribute()] [System.Runtime.CompilerServices.CompilerGeneratedAttribute()] diff --git a/src/Responders/GuildLoadedResponder.cs b/src/Responders/GuildLoadedResponder.cs new file mode 100644 index 0000000..081f526 --- /dev/null +++ b/src/Responders/GuildLoadedResponder.cs @@ -0,0 +1,63 @@ +using Boyfriend.Data; +using Boyfriend.Services; +using JetBrains.Annotations; +using Microsoft.Extensions.Logging; +using Remora.Discord.API.Abstractions.Gateway.Events; +using Remora.Discord.API.Abstractions.Rest; +using Remora.Discord.API.Gateway.Events; +using Remora.Discord.Extensions.Embeds; +using Remora.Discord.Gateway.Responders; +using Remora.Results; + +namespace Boyfriend.Responders; + +/// +/// Handles sending a message to a guild that has just initialized if that guild +/// has enabled +/// +[UsedImplicitly] +public class GuildLoadedResponder : IResponder { + private readonly IDiscordRestChannelAPI _channelApi; + private readonly GuildDataService _dataService; + private readonly ILogger _logger; + private readonly IDiscordRestUserAPI _userApi; + + public GuildLoadedResponder( + IDiscordRestChannelAPI channelApi, GuildDataService dataService, ILogger logger, + IDiscordRestUserAPI userApi) { + _channelApi = channelApi; + _dataService = dataService; + _logger = logger; + _userApi = userApi; + } + + public async Task RespondAsync(IGuildCreate gatewayEvent, CancellationToken ct = default) { + if (!gatewayEvent.Guild.IsT0) return Result.FromSuccess(); // Guild is not IAvailableGuild + + var guild = gatewayEvent.Guild.AsT0; + _logger.LogInformation("Joined guild \"{Name}\"", guild.Name); + + var cfg = await _dataService.GetSettings(guild.ID, ct); + if (!GuildSettings.ReceiveStartupMessages.Get(cfg)) + return Result.FromSuccess(); + if (GuildSettings.PrivateFeedbackChannel.Get(cfg).Empty()) + return Result.FromSuccess(); + + var currentUserResult = await _userApi.GetCurrentUserAsync(ct); + if (!currentUserResult.IsDefined(out var currentUser)) return Result.FromError(currentUserResult); + + Messages.Culture = GuildSettings.Language.Get(cfg); + var i = Random.Shared.Next(1, 4); + + var embed = new EmbedBuilder().WithSmallTitle(currentUser.GetTag(), currentUser) + .WithTitle($"Beep{i}".Localized()) + .WithDescription(Messages.Ready) + .WithCurrentTimestamp() + .WithColour(ColorsList.Blue) + .Build(); + if (!embed.IsDefined(out var built)) return Result.FromError(embed); + + return (Result)await _channelApi.CreateMessageAsync( + GuildSettings.PrivateFeedbackChannel.Get(cfg), embeds: new[] { built }, ct: ct); + } +} diff --git a/src/Responders/GuildMemberJoinedResponder.cs b/src/Responders/GuildMemberJoinedResponder.cs new file mode 100644 index 0000000..c61e500 --- /dev/null +++ b/src/Responders/GuildMemberJoinedResponder.cs @@ -0,0 +1,65 @@ +using Boyfriend.Data; +using Boyfriend.Services; +using JetBrains.Annotations; +using Remora.Discord.API.Abstractions.Gateway.Events; +using Remora.Discord.API.Abstractions.Rest; +using Remora.Discord.Extensions.Embeds; +using Remora.Discord.Gateway.Responders; +using Remora.Results; + +namespace Boyfriend.Responders; + +/// +/// Handles sending a guild's if one is set. +/// If is enabled, roles will be returned. +/// +/// +[UsedImplicitly] +public class GuildMemberJoinedResponder : IResponder { + private readonly IDiscordRestChannelAPI _channelApi; + private readonly GuildDataService _dataService; + private readonly IDiscordRestGuildAPI _guildApi; + + public GuildMemberJoinedResponder( + IDiscordRestChannelAPI channelApi, GuildDataService dataService, IDiscordRestGuildAPI guildApi) { + _channelApi = channelApi; + _dataService = dataService; + _guildApi = guildApi; + } + + public async Task RespondAsync(IGuildMemberAdd gatewayEvent, CancellationToken ct = default) { + if (!gatewayEvent.User.IsDefined(out var user)) + return Result.FromError(new ArgumentNullError(nameof(gatewayEvent.User))); + var data = await _dataService.GetData(gatewayEvent.GuildID, ct); + var cfg = data.Settings; + if (GuildSettings.PublicFeedbackChannel.Get(cfg).Empty() + || GuildSettings.WelcomeMessage.Get(cfg) is "off" or "disable" or "disabled") + return Result.FromSuccess(); + if (GuildSettings.ReturnRolesOnRejoin.Get(cfg)) { + var result = await _guildApi.ModifyGuildMemberAsync( + gatewayEvent.GuildID, user.ID, + roles: data.GetMemberData(user.ID).Roles.ConvertAll(r => r.ToSnowflake()), ct: ct); + if (!result.IsSuccess) return Result.FromError(result.Error); + } + + Messages.Culture = GuildSettings.Language.Get(cfg); + var welcomeMessage = GuildSettings.WelcomeMessage.Get(cfg) is "default" or "reset" + ? Messages.DefaultWelcomeMessage + : GuildSettings.WelcomeMessage.Get(cfg); + + var guildResult = await _guildApi.GetGuildAsync(gatewayEvent.GuildID, ct: ct); + if (!guildResult.IsDefined(out var guild)) return Result.FromError(guildResult); + + var embed = new EmbedBuilder() + .WithSmallTitle(string.Format(welcomeMessage, user.GetTag(), guild.Name), user) + .WithGuildFooter(guild) + .WithTimestamp(gatewayEvent.JoinedAt) + .WithColour(ColorsList.Green) + .Build(); + if (!embed.IsDefined(out var built)) return Result.FromError(embed); + + return (Result)await _channelApi.CreateMessageAsync( + GuildSettings.PublicFeedbackChannel.Get(cfg), embeds: new[] { built }, + allowedMentions: Boyfriend.NoMentions, ct: ct); + } +} diff --git a/src/Responders/GuildMemberRolesUpdatedResponder.cs b/src/Responders/GuildMemberRolesUpdatedResponder.cs new file mode 100644 index 0000000..b61ce32 --- /dev/null +++ b/src/Responders/GuildMemberRolesUpdatedResponder.cs @@ -0,0 +1,26 @@ +using Boyfriend.Data; +using Boyfriend.Services; +using JetBrains.Annotations; +using Remora.Discord.API.Abstractions.Gateway.Events; +using Remora.Discord.Gateway.Responders; +using Remora.Results; + +namespace Boyfriend.Responders; + +/// +/// Handles updating when a guild member is updated. +/// +[UsedImplicitly] +public class GuildMemberUpdateResponder : IResponder { + private readonly GuildDataService _dataService; + + public GuildMemberUpdateResponder(GuildDataService dataService) { + _dataService = dataService; + } + + public async Task RespondAsync(IGuildMemberUpdate gatewayEvent, CancellationToken ct = default) { + var memberData = await _dataService.GetMemberData(gatewayEvent.GuildID, gatewayEvent.User.ID, ct); + memberData.Roles = gatewayEvent.Roles.ToList().ConvertAll(r => r.Value); + return Result.FromSuccess(); + } +} diff --git a/src/Responders/MessageDeletedResponder.cs b/src/Responders/MessageDeletedResponder.cs new file mode 100644 index 0000000..903db84 --- /dev/null +++ b/src/Responders/MessageDeletedResponder.cs @@ -0,0 +1,78 @@ +using Boyfriend.Data; +using Boyfriend.Services; +using JetBrains.Annotations; +using Remora.Discord.API.Abstractions.Gateway.Events; +using Remora.Discord.API.Abstractions.Objects; +using Remora.Discord.API.Abstractions.Rest; +using Remora.Discord.Extensions.Embeds; +using Remora.Discord.Extensions.Formatting; +using Remora.Discord.Gateway.Responders; +using Remora.Results; + +namespace Boyfriend.Responders; + +/// +/// Handles logging the contents of a deleted message and the user who deleted the message +/// to a guild's if one is set. +/// +[UsedImplicitly] +public class MessageDeletedResponder : IResponder { + private readonly IDiscordRestAuditLogAPI _auditLogApi; + private readonly IDiscordRestChannelAPI _channelApi; + private readonly GuildDataService _dataService; + private readonly IDiscordRestUserAPI _userApi; + + public MessageDeletedResponder( + IDiscordRestAuditLogAPI auditLogApi, IDiscordRestChannelAPI channelApi, + GuildDataService dataService, IDiscordRestUserAPI userApi) { + _auditLogApi = auditLogApi; + _channelApi = channelApi; + _dataService = dataService; + _userApi = userApi; + } + + public async Task RespondAsync(IMessageDelete gatewayEvent, CancellationToken ct = default) { + if (!gatewayEvent.GuildID.IsDefined(out var guildId)) return Result.FromSuccess(); + + var cfg = await _dataService.GetSettings(guildId, ct); + if (GuildSettings.PrivateFeedbackChannel.Get(cfg).Empty()) return Result.FromSuccess(); + + var messageResult = await _channelApi.GetChannelMessageAsync(gatewayEvent.ChannelID, gatewayEvent.ID, ct); + if (!messageResult.IsDefined(out var message)) return Result.FromError(messageResult); + if (string.IsNullOrWhiteSpace(message.Content)) return Result.FromSuccess(); + + var auditLogResult = await _auditLogApi.GetGuildAuditLogAsync( + guildId, actionType: AuditLogEvent.MessageDelete, limit: 1, ct: ct); + if (!auditLogResult.IsDefined(out var auditLogPage)) return Result.FromError(auditLogResult); + + var auditLog = auditLogPage.AuditLogEntries.Single(); + if (!auditLog.Options.IsDefined(out var options)) + return Result.FromError(new ArgumentNullError(nameof(auditLog.Options))); + + var user = message.Author; + if (options.ChannelID == gatewayEvent.ChannelID + && DateTimeOffset.UtcNow.Subtract(auditLog.ID.Timestamp).TotalSeconds <= 2) { + var userResult = await _userApi.GetUserAsync(auditLog.UserID!.Value, ct); + if (!userResult.IsDefined(out user)) return Result.FromError(userResult); + } + + Messages.Culture = GuildSettings.Language.Get(cfg); + + var embed = new EmbedBuilder() + .WithSmallTitle( + string.Format( + Messages.CachedMessageDeleted, + message.Author.GetTag()), message.Author) + .WithDescription( + $"{Mention.Channel(gatewayEvent.ChannelID)}\n{message.Content.InBlockCode()}") + .WithActionFooter(user) + .WithTimestamp(message.Timestamp) + .WithColour(ColorsList.Red) + .Build(); + if (!embed.IsDefined(out var built)) return Result.FromError(embed); + + return (Result)await _channelApi.CreateMessageAsync( + GuildSettings.PrivateFeedbackChannel.Get(cfg), embeds: new[] { built }, + allowedMentions: Boyfriend.NoMentions, ct: ct); + } +} diff --git a/src/Responders/MessageEditedResponder.cs b/src/Responders/MessageEditedResponder.cs new file mode 100644 index 0000000..0211170 --- /dev/null +++ b/src/Responders/MessageEditedResponder.cs @@ -0,0 +1,89 @@ +using Boyfriend.Data; +using Boyfriend.Services; +using DiffPlex.DiffBuilder; +using JetBrains.Annotations; +using Remora.Discord.API.Abstractions.Gateway.Events; +using Remora.Discord.API.Abstractions.Objects; +using Remora.Discord.API.Abstractions.Rest; +using Remora.Discord.Caching; +using Remora.Discord.Caching.Services; +using Remora.Discord.Extensions.Embeds; +using Remora.Discord.Gateway.Responders; +using Remora.Results; + +namespace Boyfriend.Responders; + +/// +/// Handles logging the difference between an edited message's old and new content +/// to a guild's if one is set. +/// +[UsedImplicitly] +public class MessageEditedResponder : IResponder { + private readonly CacheService _cacheService; + private readonly IDiscordRestChannelAPI _channelApi; + private readonly GuildDataService _dataService; + private readonly IDiscordRestUserAPI _userApi; + + public MessageEditedResponder( + CacheService cacheService, IDiscordRestChannelAPI channelApi, GuildDataService dataService, + IDiscordRestUserAPI userApi) { + _cacheService = cacheService; + _channelApi = channelApi; + _dataService = dataService; + _userApi = userApi; + } + + public async Task RespondAsync(IMessageUpdate gatewayEvent, CancellationToken ct = default) { + if (!gatewayEvent.GuildID.IsDefined(out var guildId)) + return Result.FromSuccess(); + var cfg = await _dataService.GetSettings(guildId, ct); + if (GuildSettings.PrivateFeedbackChannel.Get(cfg).Empty()) + return Result.FromSuccess(); + if (!gatewayEvent.Content.IsDefined(out var newContent)) + return Result.FromSuccess(); + if (!gatewayEvent.EditedTimestamp.IsDefined(out var timestamp)) + return Result.FromSuccess(); // The message wasn't actually edited + + if (!gatewayEvent.ChannelID.IsDefined(out var channelId)) + return Result.FromError(new ArgumentNullError(nameof(gatewayEvent.ChannelID))); + if (!gatewayEvent.ID.IsDefined(out var messageId)) + return Result.FromError(new ArgumentNullError(nameof(gatewayEvent.ID))); + + var cacheKey = new KeyHelpers.MessageCacheKey(channelId, messageId); + var messageResult = await _cacheService.TryGetValueAsync( + cacheKey, ct); + if (!messageResult.IsDefined(out var message)) return Result.FromError(messageResult); + if (message.Content == newContent) return Result.FromSuccess(); + + // Custom event responders are called earlier than responders responsible for message caching + // This means that subsequent edit logs may contain the wrong content + // We can work around this by evicting the message from the cache + await _cacheService.EvictAsync(cacheKey, ct); + // However, since we evicted the message, subsequent edits won't have a cached instance to work with + // Getting the message will put it back in the cache, resolving all issues + // We don't need to await this since the result is not needed + // NOTE: Because this is not awaited, there may be a race condition depending on how fast clients are able to edit their messages + // NOTE: Awaiting this might not even solve this if the same responder is called asynchronously + _ = _channelApi.GetChannelMessageAsync(channelId, messageId, ct); + + var currentUserResult = await _userApi.GetCurrentUserAsync(ct); + if (!currentUserResult.IsDefined(out var currentUser)) return Result.FromError(currentUserResult); + + var diff = InlineDiffBuilder.Diff(message.Content, newContent); + + Messages.Culture = GuildSettings.Language.Get(cfg); + + var embed = new EmbedBuilder() + .WithSmallTitle(string.Format(Messages.CachedMessageEdited, message.Author.GetTag()), message.Author) + .WithDescription($"https://discord.com/channels/{guildId}/{channelId}/{messageId}\n{diff.AsMarkdown()}") + .WithUserFooter(currentUser) + .WithTimestamp(timestamp.Value) + .WithColour(ColorsList.Yellow) + .Build(); + if (!embed.IsDefined(out var built)) return Result.FromError(embed); + + return (Result)await _channelApi.CreateMessageAsync( + GuildSettings.PrivateFeedbackChannel.Get(cfg), embeds: new[] { built }, + allowedMentions: Boyfriend.NoMentions, ct: ct); + } +} diff --git a/src/Responders/MessageReceivedResponder.cs b/src/Responders/MessageReceivedResponder.cs new file mode 100644 index 0000000..baaae04 --- /dev/null +++ b/src/Responders/MessageReceivedResponder.cs @@ -0,0 +1,35 @@ +using JetBrains.Annotations; +using Remora.Discord.API.Abstractions.Gateway.Events; +using Remora.Discord.API.Abstractions.Rest; +using Remora.Discord.Gateway.Responders; +using Remora.Rest.Core; +using Remora.Results; + +namespace Boyfriend.Responders; + +/// +/// Handles sending replies to easter egg messages. +/// +[UsedImplicitly] +public class MessageCreateResponder : IResponder { + private readonly IDiscordRestChannelAPI _channelApi; + + public MessageCreateResponder(IDiscordRestChannelAPI channelApi) { + _channelApi = channelApi; + } + + public Task RespondAsync(IMessageCreate gatewayEvent, CancellationToken ct = default) { + _ = _channelApi.CreateMessageAsync( + gatewayEvent.ChannelID, ct: ct, content: gatewayEvent.Content switch { + "whoami" => "`nobody`", + "сука !!" => "`root`", + "воооо" => "`removing /...`", + "пон" => + "https://cdn.discordapp.com/attachments/837385840946053181/1087236080950055023/vUORS10xPaY-1.jpg", + "++++" => "#", + "осу" => "https://github.com/ppy/osu", + _ => default(Optional) + }); + return Task.FromResult(Result.FromSuccess()); + } +} diff --git a/src/Responders/ScheduledEventCancelledResponder.cs b/src/Responders/ScheduledEventCancelledResponder.cs new file mode 100644 index 0000000..86453ef --- /dev/null +++ b/src/Responders/ScheduledEventCancelledResponder.cs @@ -0,0 +1,45 @@ +using Boyfriend.Data; +using Boyfriend.Services; +using JetBrains.Annotations; +using Remora.Discord.API.Abstractions.Gateway.Events; +using Remora.Discord.API.Abstractions.Rest; +using Remora.Discord.Extensions.Embeds; +using Remora.Discord.Gateway.Responders; +using Remora.Results; + +namespace Boyfriend.Responders; + +/// +/// Handles sending a notification when a scheduled event has been cancelled +/// in a guild's if one is set. +/// +[UsedImplicitly] +public class GuildScheduledEventDeleteResponder : IResponder { + private readonly IDiscordRestChannelAPI _channelApi; + private readonly GuildDataService _dataService; + + public GuildScheduledEventDeleteResponder(IDiscordRestChannelAPI channelApi, GuildDataService dataService) { + _channelApi = channelApi; + _dataService = dataService; + } + + public async Task RespondAsync(IGuildScheduledEventDelete gatewayEvent, CancellationToken ct = default) { + var guildData = await _dataService.GetData(gatewayEvent.GuildID, ct); + guildData.ScheduledEvents.Remove(gatewayEvent.ID.Value); + + if (GuildSettings.EventNotificationChannel.Get(guildData.Settings).Empty()) + return Result.FromSuccess(); + + var embed = new EmbedBuilder() + .WithSmallTitle(string.Format(Messages.EventCancelled, gatewayEvent.Name)) + .WithDescription(":(") + .WithColour(ColorsList.Red) + .WithCurrentTimestamp() + .Build(); + + if (!embed.IsDefined(out var built)) return Result.FromError(embed); + + return (Result)await _channelApi.CreateMessageAsync( + GuildSettings.EventNotificationChannel.Get(guildData.Settings), embeds: new[] { built }, ct: ct); + } +} diff --git a/src/Services/GuildUpdateService.cs b/src/Services/GuildUpdateService.cs index e315deb..99b94c1 100644 --- a/src/Services/GuildUpdateService.cs +++ b/src/Services/GuildUpdateService.cs @@ -1,6 +1,5 @@ using System.Text.Json.Nodes; using Boyfriend.Data; -using Boyfriend.locale; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using Remora.Discord.API.Abstractions.Objects;