From 9a6431acdd5dfee7a00741aa723dba98ded3c362 Mon Sep 17 00:00:00 2001 From: TraceLD Date: Fri, 30 Sep 2022 01:40:10 +0100 Subject: [PATCH 01/30] update Remora --- src/Mmcc.Bot.Caching/Mmcc.Bot.Caching.csproj | 6 +- .../Mmcc.Bot.Common.Extensions.csproj | 4 +- src/Mmcc.Bot.Common/Mmcc.Bot.Common.csproj | 6 +- .../Mmcc.Bot.Database.csproj | 14 +- src/Mmcc.Bot.Mojang/Mmcc.Bot.Mojang.csproj | 4 +- .../Mmcc.Bot.Polychat.csproj | 16 +- .../Services/DiscordSanitiserService.cs | 13 +- .../Conditions/RequireGuildCondition.cs | 6 +- .../Mmcc.Bot.RemoraAbstractions.csproj | 2 +- .../Services/DiscordPermissionsService.cs | 7 +- src/Mmcc.Bot/Commands/CommandsSetup.cs | 33 +++- .../Commands/Core/MmccInfoCommands.cs | 40 +---- .../Commands/Moderation/Bans/BanCommands.cs | 6 +- .../MemberApplicationsCommands.cs | 18 +-- .../PlayerInfo/PlayerInfoCommands.cs | 6 +- .../Commands/Moderation/Warns/WarnCommands.cs | 6 +- src/Mmcc.Bot/CoreSetup/ConfigurationSetup.cs | 3 +- .../ButtonInteractionCreateResponder.cs | 151 ------------------ .../EventResponders/Buttons/TestHandler.cs | 36 ----- .../EventResponders/EventRespondersSetup.cs | 2 - .../Guilds/GuildCreatedResponder.cs | 36 ++--- .../MemberApplicationCreatedResponder.cs | 2 +- .../MemberApplicationUpdatedResponder.cs | 2 +- src/Mmcc.Bot/Mmcc.Bot.csproj | 24 +-- src/Mmcc.Bot/Program.cs | 6 +- 25 files changed, 113 insertions(+), 336 deletions(-) delete mode 100644 src/Mmcc.Bot/EventResponders/Buttons/ButtonInteractionCreateResponder.cs delete mode 100644 src/Mmcc.Bot/EventResponders/Buttons/TestHandler.cs diff --git a/src/Mmcc.Bot.Caching/Mmcc.Bot.Caching.csproj b/src/Mmcc.Bot.Caching/Mmcc.Bot.Caching.csproj index 4227cc0..6c07f36 100644 --- a/src/Mmcc.Bot.Caching/Mmcc.Bot.Caching.csproj +++ b/src/Mmcc.Bot.Caching/Mmcc.Bot.Caching.csproj @@ -7,9 +7,9 @@ - - - + + + diff --git a/src/Mmcc.Bot.Common.Extensions/Mmcc.Bot.Common.Extensions.csproj b/src/Mmcc.Bot.Common.Extensions/Mmcc.Bot.Common.Extensions.csproj index 98bcc18..c5e0ed1 100644 --- a/src/Mmcc.Bot.Common.Extensions/Mmcc.Bot.Common.Extensions.csproj +++ b/src/Mmcc.Bot.Common.Extensions/Mmcc.Bot.Common.Extensions.csproj @@ -15,8 +15,8 @@ - - + + diff --git a/src/Mmcc.Bot.Common/Mmcc.Bot.Common.csproj b/src/Mmcc.Bot.Common/Mmcc.Bot.Common.csproj index c8d753c..e935976 100644 --- a/src/Mmcc.Bot.Common/Mmcc.Bot.Common.csproj +++ b/src/Mmcc.Bot.Common/Mmcc.Bot.Common.csproj @@ -7,9 +7,9 @@ - - - + + + diff --git a/src/Mmcc.Bot.Database/Mmcc.Bot.Database.csproj b/src/Mmcc.Bot.Database/Mmcc.Bot.Database.csproj index 0bcd71e..a2bf5a4 100644 --- a/src/Mmcc.Bot.Database/Mmcc.Bot.Database.csproj +++ b/src/Mmcc.Bot.Database/Mmcc.Bot.Database.csproj @@ -7,16 +7,16 @@ - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive - - - - - + + + + + diff --git a/src/Mmcc.Bot.Mojang/Mmcc.Bot.Mojang.csproj b/src/Mmcc.Bot.Mojang/Mmcc.Bot.Mojang.csproj index 587c0bb..9c2b785 100644 --- a/src/Mmcc.Bot.Mojang/Mmcc.Bot.Mojang.csproj +++ b/src/Mmcc.Bot.Mojang/Mmcc.Bot.Mojang.csproj @@ -7,8 +7,8 @@ - - + + diff --git a/src/Mmcc.Bot.Polychat/Mmcc.Bot.Polychat.csproj b/src/Mmcc.Bot.Polychat/Mmcc.Bot.Polychat.csproj index fa074f4..fa670f4 100644 --- a/src/Mmcc.Bot.Polychat/Mmcc.Bot.Polychat.csproj +++ b/src/Mmcc.Bot.Polychat/Mmcc.Bot.Polychat.csproj @@ -8,19 +8,19 @@ - - - - + + + + all runtime; build; native; contentfiles; analyzers; buildtransitive - - - + + + - + diff --git a/src/Mmcc.Bot.Polychat/Services/DiscordSanitiserService.cs b/src/Mmcc.Bot.Polychat/Services/DiscordSanitiserService.cs index 754f5bd..c11c3d7 100644 --- a/src/Mmcc.Bot.Polychat/Services/DiscordSanitiserService.cs +++ b/src/Mmcc.Bot.Polychat/Services/DiscordSanitiserService.cs @@ -3,6 +3,7 @@ using System.Text.RegularExpressions; using System.Threading.Tasks; using Mmcc.Bot.Polychat.Abstractions; +using Remora.Discord.API.Abstractions.Gateway.Events; using Remora.Discord.API.Abstractions.Objects; using Remora.Discord.API.Abstractions.Rest; using Remora.Rest.Core; @@ -64,10 +65,14 @@ public DiscordSanitiserService(IDiscordRestGuildAPI guildApi) public async Task SanitiseMessageContent(IMessage message) { var s = message.Content; - - s = SanitiseUsernameAndNicknameMentions(s, message.Mentions); - s = await SanitiseChannelMentions(s, message.GuildID); - s = await SanitiseRoleMentions(s, message.GuildID); + + if (message is IMessageCreate msgCreateEv) + { + s = SanitiseUsernameAndNicknameMentions(s, msgCreateEv.Mentions); + s = await SanitiseChannelMentions(s, msgCreateEv.GuildID); + s = await SanitiseRoleMentions(s, msgCreateEv.GuildID); + } + s = SanitiseStandardEmoji(s); s = SanitiseCustomEmoji(s); s = s.Replace("️", " "); diff --git a/src/Mmcc.Bot.RemoraAbstractions/Conditions/RequireGuildCondition.cs b/src/Mmcc.Bot.RemoraAbstractions/Conditions/RequireGuildCondition.cs index 65c4a55..36b073a 100644 --- a/src/Mmcc.Bot.RemoraAbstractions/Conditions/RequireGuildCondition.cs +++ b/src/Mmcc.Bot.RemoraAbstractions/Conditions/RequireGuildCondition.cs @@ -2,7 +2,6 @@ using System.Threading.Tasks; using Mmcc.Bot.RemoraAbstractions.Conditions.Attributes; using Remora.Commands.Conditions; -using Remora.Commands.Results; using Remora.Discord.Commands.Contexts; using Remora.Results; @@ -27,10 +26,9 @@ public RequireGuildCondition(MessageContext context) /// public ValueTask CheckAsync(RequireGuildAttribute attribute, CancellationToken ct) { - var guild = _context.Message.GuildID; + var guild = _context.GuildID; return new(!guild.HasValue - ? new ConditionNotSatisfiedError( - "Command that requires to be executed within a guild was executed outside of one") + ? new InvalidOperationError("Command that requires to be executed within a guild was executed outside of one") : Result.FromSuccess()); } } \ No newline at end of file diff --git a/src/Mmcc.Bot.RemoraAbstractions/Mmcc.Bot.RemoraAbstractions.csproj b/src/Mmcc.Bot.RemoraAbstractions/Mmcc.Bot.RemoraAbstractions.csproj index a06779b..7cdc5de 100644 --- a/src/Mmcc.Bot.RemoraAbstractions/Mmcc.Bot.RemoraAbstractions.csproj +++ b/src/Mmcc.Bot.RemoraAbstractions/Mmcc.Bot.RemoraAbstractions.csproj @@ -7,7 +7,7 @@ - + diff --git a/src/Mmcc.Bot.RemoraAbstractions/Services/DiscordPermissionsService.cs b/src/Mmcc.Bot.RemoraAbstractions/Services/DiscordPermissionsService.cs index 3f81974..d5a4bf8 100644 --- a/src/Mmcc.Bot.RemoraAbstractions/Services/DiscordPermissionsService.cs +++ b/src/Mmcc.Bot.RemoraAbstractions/Services/DiscordPermissionsService.cs @@ -1,7 +1,6 @@ using System.Linq; using System.Threading; using System.Threading.Tasks; -using Remora.Commands.Results; using Remora.Discord.API.Abstractions.Objects; using Remora.Discord.API.Abstractions.Rest; using Remora.Discord.API.Objects; @@ -65,8 +64,7 @@ public async ValueTask CheckHasRequiredPermission( var channel = getChannel.Entity; if (!channel.GuildID.HasValue) { - return new ConditionNotSatisfiedError( - "Command requires a guild permission but was executed outside of a guild."); + return new InvalidOperationError("Command requires a guild permission but was executed outside of a guild."); } var guildId = channel.GuildID.Value; @@ -135,8 +133,7 @@ public async ValueTask CheckHasRequiredPermission( var hasPermission = computedPermissions.HasPermission(permission); return !hasPermission - ? new ConditionNotSatisfiedError( - $"Guild User requesting the command does not have the required {permission.ToString()} permission") + ? new InvalidOperationError($"Guild User requesting the command does not have the required {permission.ToString()} permission") : Result.FromSuccess(); } } \ No newline at end of file diff --git a/src/Mmcc.Bot/Commands/CommandsSetup.cs b/src/Mmcc.Bot/Commands/CommandsSetup.cs index 3d883b5..7cc739b 100644 --- a/src/Mmcc.Bot/Commands/CommandsSetup.cs +++ b/src/Mmcc.Bot/Commands/CommandsSetup.cs @@ -1,8 +1,6 @@ using Microsoft.Extensions.DependencyInjection; using Mmcc.Bot.Commands.Core; -using Mmcc.Bot.Commands.Core.Help; using Mmcc.Bot.Commands.Diagnostics; -using Mmcc.Bot.Commands.Guilds; using Mmcc.Bot.Commands.Minecraft; using Mmcc.Bot.Commands.Minecraft.Restarts; using Mmcc.Bot.Commands.Moderation; @@ -29,11 +27,9 @@ public static class CommandsSetup /// The . public static IServiceCollection AddBotCommands(this IServiceCollection services) { - services.AddDiscordCommands(); - // core commands; - services.AddCommandGroup(); - services.AddCommandGroup(); + //services.AddCommandGroup(); + //services.AddCommandGroup(); services.AddCommandGroup(); // tags; @@ -55,7 +51,32 @@ public static IServiceCollection AddBotCommands(this IServiceCollection services services.AddCommandGroup(); services.AddCommandGroup(); services.AddCommandGroup(); + + services.AddDiscordCommands(); + /*services.AddCommandTree() + // add core commands; + .WithCommandGroup() + .WithCommandGroup() + .WithCommandGroup() + // add tags; + .WithCommandGroup() + .WithCommandGroup() + // add diagnostics; + .WithCommandGroup() + // add in-game; + .WithCommandGroup() + .WithCommandGroup() + // add member apps; + .WithCommandGroup() + // add moderation; + .WithCommandGroup() + .WithCommandGroup() + .WithCommandGroup() + .WithCommandGroup() + // and build it; + .Finish();*/ + return services; } } \ No newline at end of file diff --git a/src/Mmcc.Bot/Commands/Core/MmccInfoCommands.cs b/src/Mmcc.Bot/Commands/Core/MmccInfoCommands.cs index b9014a4..334aff7 100644 --- a/src/Mmcc.Bot/Commands/Core/MmccInfoCommands.cs +++ b/src/Mmcc.Bot/Commands/Core/MmccInfoCommands.cs @@ -1,13 +1,7 @@ -using System; -using System.Collections.Generic; +using System.Collections.Generic; using System.ComponentModel; using System.Threading.Tasks; -using Mmcc.Bot.Caching; -using Mmcc.Bot.Caching.Entities; using Mmcc.Bot.Common.Statics; -using Mmcc.Bot.EventResponders.Buttons; -using Mmcc.Bot.RemoraAbstractions.Services; -using Mmcc.Bot.RemoraAbstractions.Ui; using Remora.Commands.Attributes; using Remora.Commands.Groups; using Remora.Discord.API.Abstractions.Objects; @@ -21,24 +15,12 @@ namespace Mmcc.Bot.Commands.Core; public class MmccInfoCommands : CommandGroup { - private readonly ICommandResponder _responder; - private readonly IDiscordRestInteractionAPI _interactionApi; private readonly IDiscordRestChannelAPI _channelApi; - private readonly IDiscordRestWebhookAPI _webhookApi; - private readonly IButtonHandlerRepository _handlerRepository; - private readonly IInteractionResponder _interactionResponder; private readonly MessageContext _context; - public MmccInfoCommands(ICommandResponder responder, IDiscordRestInteractionAPI interactionApi, - IDiscordRestChannelAPI channelApi, IButtonHandlerRepository handlerRepository, - IDiscordRestWebhookAPI webhookApi, IInteractionResponder interactionResponder, MessageContext context) + public MmccInfoCommands(IDiscordRestChannelAPI channelApi, MessageContext context) { - _responder = responder; - _interactionApi = interactionApi; _channelApi = channelApi; - _handlerRepository = handlerRepository; - _webhookApi = webhookApi; - _interactionResponder = interactionResponder; _context = context; } @@ -64,22 +46,4 @@ public async Task Mmcc() components: new(components) ); } - -#if DEBUG - // TODO: remove once app buttons are implemented; - [Command("test")] - public async Task Test() - { - var id = new Snowflake((ulong) DateTimeOffset.UtcNow.ToUnixTimeMilliseconds()); - var component = new ButtonComponent(ButtonComponentStyle.Primary, "Test", - CustomID: id.ToString()); - var testButton = - HandleableButton.Create(id, component, - new TestHandler.Context(_context.ChannelID)); - - _handlerRepository.Register(testButton); - - return await _responder.RespondWithComponents(ActionRowUtils.FromButtons(testButton), "Test buttons"); - } -#endif } \ No newline at end of file diff --git a/src/Mmcc.Bot/Commands/Moderation/Bans/BanCommands.cs b/src/Mmcc.Bot/Commands/Moderation/Bans/BanCommands.cs index 0059a38..4cb9568 100644 --- a/src/Mmcc.Bot/Commands/Moderation/Bans/BanCommands.cs +++ b/src/Mmcc.Bot/Commands/Moderation/Bans/BanCommands.cs @@ -68,7 +68,7 @@ ICommandResponder responder public async Task BanDiscord(IUser user, ExpiryDate expiryDate, [Greedy] string reason) => await _mediator.Send(new BanModerationAction.Command { - GuildId = _context.Message.GuildID.Value, + GuildId = _context.GuildID.Value, ChannelId = _context.ChannelID, UserDiscordId = user.ID, Reason = reason, @@ -100,7 +100,7 @@ await _responder.Respond(_embedBase with public async Task BanIg(string ign, ExpiryDate expiryDate, [Greedy] string reason) => await _mediator.Send(new BanModerationAction.Command { - GuildId = _context.Message.GuildID.Value, + GuildId = _context.GuildID.Value, ChannelId = _context.ChannelID, UserIgn = ign, Reason = reason, @@ -133,7 +133,7 @@ public async Task BanAll(IUser discordUser, string ign, ExpiryDate expi [Greedy] string reason) => await _mediator.Send(new BanModerationAction.Command { - GuildId = _context.Message.GuildID.Value, + GuildId = _context.GuildID.Value, ChannelId = _context.ChannelID, UserIgn = ign, Reason = reason, diff --git a/src/Mmcc.Bot/Commands/Moderation/MemberApplications/MemberApplicationsCommands.cs b/src/Mmcc.Bot/Commands/Moderation/MemberApplications/MemberApplicationsCommands.cs index df27e5d..21c75d9 100644 --- a/src/Mmcc.Bot/Commands/Moderation/MemberApplications/MemberApplicationsCommands.cs +++ b/src/Mmcc.Bot/Commands/Moderation/MemberApplications/MemberApplicationsCommands.cs @@ -115,7 +115,7 @@ public async Task View(int id) => await _mediator.Send(new GetById.Query { ApplicationId = id, - GuildId = _context.Message.GuildID.Value + GuildId = _context.GuildID.Value }) switch { { IsSuccess: true, Entity: { } e } => @@ -134,7 +134,7 @@ await _responder.Respond(e.GetEmbed(_colourPalette)), [Command("next", "n")] [Description("Views the next pending application in the queue")] public async Task ViewNextPending() => - await _mediator.Send(new GetNextPending.Query { GuildId = _context.Message.GuildID.Value }) switch + await _mediator.Send(new GetNextPending.Query { GuildId = _context.GuildID.Value }) switch { { IsSuccess: true, Entity: { } e } => await _responder.Respond(e.GetEmbed(_colourPalette)), @@ -168,7 +168,7 @@ public async Task ViewPending() return await _mediator.Send(new GetByStatus.Query { - GuildId = _context.Message.GuildID.Value, + GuildId = _context.GuildID.Value, ApplicationStatus = ApplicationStatus.Pending, Limit = 25, SortByDescending = false @@ -202,7 +202,7 @@ public async Task ViewApproved() return await _mediator.Send(new GetByStatus.Query { - GuildId = _context.Message.GuildID.Value, + GuildId = _context.GuildID.Value, ApplicationStatus = ApplicationStatus.Approved, Limit = 10, SortByDescending = true @@ -236,7 +236,7 @@ public async Task ViewRejected() return await _mediator.Send(new GetByStatus.Query { - GuildId = _context.Message.GuildID.Value, + GuildId = _context.GuildID.Value, ApplicationStatus = ApplicationStatus.Rejected, Limit = 10, SortByDescending = true @@ -265,7 +265,7 @@ await _responder.Respond( [RequireUserGuildPermission(DiscordPermission.BanMembers)] public async Task Approve(int id, string serverPrefix, List ignsList) { - var getMembersChannelResult = await _guildApi.FindGuildChannelByName(_context.Message.GuildID.Value, + var getMembersChannelResult = await _guildApi.FindGuildChannelByName(_context.GuildID.Value, _discordSettings.ChannelNames.MemberApps); if (!getMembersChannelResult.IsSuccess) { @@ -275,7 +275,7 @@ public async Task Approve(int id, string serverPrefix, List ign var commandResult = await _mediator.Send(new ApproveAutomatically.Command { Id = id, - GuildId = _context.Message.GuildID.Value, + GuildId = _context.GuildID.Value, ChannelId = _context.ChannelID, ServerPrefix = serverPrefix, Igns = ignsList @@ -327,7 +327,7 @@ public async Task Approve(int id, string serverPrefix, List ign [RequireUserGuildPermission(DiscordPermission.BanMembers)] public async Task Reject(int id, [Greedy] string reason) { - var getMembersChannelResult = await _guildApi.FindGuildChannelByName(_context.Message.GuildID.Value, + var getMembersChannelResult = await _guildApi.FindGuildChannelByName(_context.GuildID.Value, _discordSettings.ChannelNames.MemberApps); if (!getMembersChannelResult.IsSuccess) { @@ -335,7 +335,7 @@ public async Task Reject(int id, [Greedy] string reason) } var rejectCommandResult = await _mediator.Send(new Reject.Command - {Id = id, GuildId = _context.Message.GuildID.Value}); + {Id = id, GuildId = _context.GuildID.Value}); if (!rejectCommandResult.IsSuccess) { return rejectCommandResult; diff --git a/src/Mmcc.Bot/Commands/Moderation/PlayerInfo/PlayerInfoCommands.cs b/src/Mmcc.Bot/Commands/Moderation/PlayerInfo/PlayerInfoCommands.cs index d37990b..12f50ec 100644 --- a/src/Mmcc.Bot/Commands/Moderation/PlayerInfo/PlayerInfoCommands.cs +++ b/src/Mmcc.Bot/Commands/Moderation/PlayerInfo/PlayerInfoCommands.cs @@ -86,7 +86,7 @@ public async Task InfoDiscord(IUser user) fields.Add(user.GetEmbedField()); - var getGuildMemberResult = await _guildApi.GetGuildMemberAsync(_context.Message.GuildID.Value, user.ID); + var getGuildMemberResult = await _guildApi.GetGuildMemberAsync(_context.GuildID.Value, user.ID); if (getGuildMemberResult.IsSuccess) { var guildMember = getGuildMemberResult.Entity; @@ -110,7 +110,7 @@ public async Task InfoDiscord(IUser user) } var queryResult = - await _mediator.Send(new GetByDiscordId.Query(_context.Message.GuildID.Value, user.ID.Value)); + await _mediator.Send(new GetByDiscordId.Query(_context.GuildID.Value, user.ID.Value)); if (queryResult.IsSuccess) { @@ -150,7 +150,7 @@ public async Task InfoIg(string ign) Thumbnail = EmbedProperties.MmccLogoThumbnail }; var fields = new List(); - var queryResult = await _mediator.Send(new GetByIgn.Query(_context.Message.GuildID.Value, ign)); + var queryResult = await _mediator.Send(new GetByIgn.Query(_context.GuildID.Value, ign)); var getUuid = await _mojangApi.GetPlayerUuidInfo(ign); if (getUuid.IsSuccess && getUuid.Entity is not null) diff --git a/src/Mmcc.Bot/Commands/Moderation/Warns/WarnCommands.cs b/src/Mmcc.Bot/Commands/Moderation/Warns/WarnCommands.cs index 10d6180..3b22af6 100644 --- a/src/Mmcc.Bot/Commands/Moderation/Warns/WarnCommands.cs +++ b/src/Mmcc.Bot/Commands/Moderation/Warns/WarnCommands.cs @@ -59,7 +59,7 @@ public async Task WarnDiscord(IUser user, [Greedy] string reason) => await _mediator.Send(new Warn.Command { UserDiscordId = user.ID, - GuildId = _context.Message.GuildID.Value, + GuildId = _context.GuildID.Value, Reason = reason, UserIgn = null }) switch @@ -80,7 +80,7 @@ public async Task WarnIg(string ign, [Greedy] string reason) => await _mediator.Send(new Warn.Command { UserIgn = ign, - GuildId = _context.Message.GuildID.Value, + GuildId = _context.GuildID.Value, Reason = reason, UserDiscordId = null }) switch @@ -105,7 +105,7 @@ public async Task WarnAll(IUser discordUser, string ign, [Greedy] strin { UserDiscordId = discordUser.ID, UserIgn = ign, - GuildId = _context.Message.GuildID.Value, + GuildId = _context.GuildID.Value, Reason = reason } ) switch diff --git a/src/Mmcc.Bot/CoreSetup/ConfigurationSetup.cs b/src/Mmcc.Bot/CoreSetup/ConfigurationSetup.cs index f162c96..510af7f 100644 --- a/src/Mmcc.Bot/CoreSetup/ConfigurationSetup.cs +++ b/src/Mmcc.Bot/CoreSetup/ConfigurationSetup.cs @@ -45,7 +45,8 @@ HostBuilderContext hostContext | GatewayIntents.GuildMembers | GatewayIntents.GuildBans | GatewayIntents.GuildMessages - | GatewayIntents.GuildMessageReactions; + | GatewayIntents.GuildMessageReactions + | GatewayIntents.MessageContents; }); return services; diff --git a/src/Mmcc.Bot/EventResponders/Buttons/ButtonInteractionCreateResponder.cs b/src/Mmcc.Bot/EventResponders/Buttons/ButtonInteractionCreateResponder.cs deleted file mode 100644 index b43994c..0000000 --- a/src/Mmcc.Bot/EventResponders/Buttons/ButtonInteractionCreateResponder.cs +++ /dev/null @@ -1,151 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Threading; -using System.Threading.Tasks; -using MediatR; -using Microsoft.Extensions.Logging; -using Mmcc.Bot.Caching; -using Mmcc.Bot.Common.Models.Colours; -using Mmcc.Bot.Common.Statics; -using Mmcc.Bot.RemoraAbstractions.Services; -using Remora.Commands.Results; -using Remora.Discord.API.Abstractions.Gateway.Events; -using Remora.Discord.API.Abstractions.Objects; -using Remora.Discord.API.Objects; -using Remora.Discord.Gateway.Responders; -using Remora.Rest.Core; -using Remora.Results; - -namespace Mmcc.Bot.EventResponders.Buttons; - -public class ButtonInteractionCreateResponder : IResponder -{ - private readonly IButtonHandlerRepository _handlerRepository; - private readonly IDiscordPermissionsService _permissionsService; - private readonly IColourPalette _colourPalette; - private readonly ILogger _logger; - private readonly IInteractionResponder _interactionResponder; - private readonly IMediator _mediator; - - public ButtonInteractionCreateResponder( - IButtonHandlerRepository handlerRepository, - IDiscordPermissionsService permissionsService, - IColourPalette colourPalette, - ILogger logger, - IInteractionResponder interactionResponder, - IMediator mediator - ) - { - _handlerRepository = handlerRepository; - _permissionsService = permissionsService; - _colourPalette = colourPalette; - _logger = logger; - _interactionResponder = interactionResponder; - _mediator = mediator; - } - - public async Task RespondAsync(IInteractionCreate ev, CancellationToken ct = new()) - { - if (ev.Type is not InteractionType.MessageComponent - || !ev.Message.HasValue - || !ev.Member.HasValue - || !ev.Member.Value.User.HasValue - || !ev.ChannelID.HasValue - || !ev.Data.HasValue - || !ev.Data.Value.CustomID.HasValue - ) - { - return Result.FromSuccess(); - } - - var notifyAboutDeferredRes = await _interactionResponder.NotifyDeferredMessageIsComing(ev.ID, ev.Token, ct); - if (!notifyAboutDeferredRes.IsSuccess) - { - return notifyAboutDeferredRes; - } - - var customId = ev.Data.Value.CustomID.Value; - var idParseSuccessful = Snowflake.TryParse(customId, out var id); - if (!idParseSuccessful) - { - var errorEmbed = new Embed - { - Title = ":x: Invalid custom ID format", - Description = - $"The custom ID: {customId} was not recognised as a valid Mmcc.Bot button ID.", - Thumbnail = EmbedProperties.MmccLogoThumbnail, - Colour = _colourPalette.Red, - Timestamp = DateTimeOffset.UtcNow - }; - var errSendRes = await _interactionResponder.SendFollowup(ev.Token, errorEmbed); - return errSendRes.IsSuccess - ? Result.FromError(new ParsingError(customId, "Could not parse button ID.")) - : errSendRes; - } - - var ulongId = id!.Value.Value; - var handler = _handlerRepository.GetOrDefault(ulongId); - if (handler is null) - { - _logger.LogWarning("Could not find a button handler for button with ID: {customId}.", ulongId); - - var errorEmbed = new Embed - { - Title = ":x: Interaction expired", - Description = - "This button has expired. Request the command with the button again to generate a new button.", - Thumbnail = EmbedProperties.MmccLogoThumbnail, - Colour = _colourPalette.Red, - Timestamp = DateTimeOffset.UtcNow - }; - return await _interactionResponder.SendFollowup(ev.Token, errorEmbed); - } - - // check if user has permission; - // ReSharper disable once InvertIf - if (handler.RequiredPermission.HasValue) - { - var checkResult = await _permissionsService.CheckHasRequiredPermission(handler.RequiredPermission.Value, - ev.ChannelID.Value, ev.Member.Value.User.Value, ct); - - // ReSharper disable once InvertIf - if (!checkResult.IsSuccess) - { - var errorEmbed = new Embed - { - Title = ":x: Unauthorised", - Description = - "You do not have permission to use this button.", - Fields = new List - { - new("Required permission", $"`{handler.RequiredPermission.Value.ToString()}`") - }, - Thumbnail = EmbedProperties.MmccLogoThumbnail, - Colour = _colourPalette.Red, - Timestamp = DateTimeOffset.UtcNow - }; - return await _interactionResponder.SendFollowup(ev.Token, errorEmbed); - } - } - - var context = handler.Context; - var command = Activator.CreateInstance(handler.HandlerCommandType, ev.Token, context); - var buttonActionResult = (Result) (await _mediator.Send(command!, ct))!; - - if (!buttonActionResult.IsSuccess) - { - var errorEmbed = new Embed - { - Thumbnail = EmbedProperties.MmccLogoThumbnail, - Colour = _colourPalette.Red, - Timestamp = DateTimeOffset.UtcNow, - Title = $":x: {buttonActionResult.Error.GetType()}.", - Description = buttonActionResult.Error.Message - }; - - return await _interactionResponder.SendFollowup(ev.Token, errorEmbed); - } - - return Result.FromSuccess(); - } -} \ No newline at end of file diff --git a/src/Mmcc.Bot/EventResponders/Buttons/TestHandler.cs b/src/Mmcc.Bot/EventResponders/Buttons/TestHandler.cs deleted file mode 100644 index c17b0b7..0000000 --- a/src/Mmcc.Bot/EventResponders/Buttons/TestHandler.cs +++ /dev/null @@ -1,36 +0,0 @@ -using System.Text.Json; -using System.Threading; -using System.Threading.Tasks; -using MediatR; -using Mmcc.Bot.RemoraAbstractions.Services; -using Remora.Rest.Core; -using Remora.Results; - -namespace Mmcc.Bot.EventResponders.Buttons; - -public class TestHandler -{ - public record Command(string InteractionToken, Context Context) : IRequest; - - public record Context(Snowflake Id); - - public class Handler : IRequestHandler - { - private readonly IInteractionResponder _responder; - - public Handler(IInteractionResponder responder) - { - _responder = responder; - } - - public async Task Handle(Command req, CancellationToken ct) - { - var msg = JsonSerializer.Serialize(req.Context); - - var respondRes = await _responder.SendFollowup(req.InteractionToken, msg); - return respondRes.IsSuccess - ? Result.FromSuccess() - : Result.FromError(respondRes.Error); - } - } -} \ No newline at end of file diff --git a/src/Mmcc.Bot/EventResponders/EventRespondersSetup.cs b/src/Mmcc.Bot/EventResponders/EventRespondersSetup.cs index 57fe061..b7749b5 100644 --- a/src/Mmcc.Bot/EventResponders/EventRespondersSetup.cs +++ b/src/Mmcc.Bot/EventResponders/EventRespondersSetup.cs @@ -1,5 +1,4 @@ using Microsoft.Extensions.DependencyInjection; -using Mmcc.Bot.EventResponders.Buttons; using Mmcc.Bot.EventResponders.Feedback; using Mmcc.Bot.EventResponders.Guilds; using Mmcc.Bot.EventResponders.Moderation.MemberApplications; @@ -27,7 +26,6 @@ public static IServiceCollection AddBotGatewayEventResponders(this IServiceColle services.AddResponder(); services.AddResponder(); services.AddResponder(); - services.AddResponder(); return services; } diff --git a/src/Mmcc.Bot/EventResponders/Guilds/GuildCreatedResponder.cs b/src/Mmcc.Bot/EventResponders/Guilds/GuildCreatedResponder.cs index c295c6a..fc0a1e8 100644 --- a/src/Mmcc.Bot/EventResponders/Guilds/GuildCreatedResponder.cs +++ b/src/Mmcc.Bot/EventResponders/Guilds/GuildCreatedResponder.cs @@ -52,43 +52,27 @@ public async Task RespondAsync(IGuildCreate ev, CancellationToken ct = d .Select(p => p.GetValue(_discordSettings.ChannelNames) as string) .Where(s => s is not null) .ToList()!; - - if (!channels.HasValue) + + + // ReSharper disable once ForeachCanBePartlyConvertedToQueryUsingAnotherGetEnumerator + foreach (var requiredChannel in requiredChannels) { - foreach (var requiredChannel in requiredChannels) + // ReSharper disable once InvertIf + if (channels.FirstOrDefault(c => c.Name.HasValue && c.Name.Value!.Equals(requiredChannel)) is null) { - var createChannelResult = await _guildApi.CreateGuildChannelAsync(ev.ID, requiredChannel, ChannelType.GuildText, ct :ct); + var createChannelResult = + await _guildApi.CreateGuildChannelAsync(ev.ID, requiredChannel, ChannelType.GuildText, ct: ct); if (!createChannelResult.IsSuccess) { - return new SetupError("Failed to create required channels."); + return new SetupError("Failed to create required channels."); } - + _logger.LogInformation( $"Created required channel \"{requiredChannel}\" in guild with ID: \"{ev.ID}\" and Name: \"{ev.Name}\""); } } - else - { - // ReSharper disable once ForeachCanBePartlyConvertedToQueryUsingAnotherGetEnumerator - foreach (var requiredChannel in requiredChannels) - { - // ReSharper disable once InvertIf - if (channels.Value.FirstOrDefault(c => c.Name.Value.Equals(requiredChannel)) is null) - { - var createChannelResult = await _guildApi.CreateGuildChannelAsync(ev.ID, requiredChannel, ChannelType.GuildText, ct :ct); - if (!createChannelResult.IsSuccess) - { - return new SetupError("Failed to create required channels."); - } - - _logger.LogInformation( - $"Created required channel \"{requiredChannel}\" in guild with ID: \"{ev.ID}\" and Name: \"{ev.Name}\""); - } - } - } - _logger.LogInformation($"Successfully set up guild with ID: \"{ev.ID}\" and Name: \"{ev.Name}\""); return Result.FromSuccess(); } diff --git a/src/Mmcc.Bot/EventResponders/Moderation/MemberApplications/MemberApplicationCreatedResponder.cs b/src/Mmcc.Bot/EventResponders/Moderation/MemberApplications/MemberApplicationCreatedResponder.cs index 81e6c53..7ebc506 100644 --- a/src/Mmcc.Bot/EventResponders/Moderation/MemberApplications/MemberApplicationCreatedResponder.cs +++ b/src/Mmcc.Bot/EventResponders/Moderation/MemberApplications/MemberApplicationCreatedResponder.cs @@ -68,7 +68,7 @@ public async Task RespondAsync(IMessageCreate ev, CancellationToken ct = } // return if the message isn't in #member-apps; - if (!channelName.Value.Equals(_discordSettings.ChannelNames.MemberApps)) + if (!channelName.Value!.Equals(_discordSettings.ChannelNames.MemberApps)) { return Result.FromSuccess(); } diff --git a/src/Mmcc.Bot/EventResponders/Moderation/MemberApplications/MemberApplicationUpdatedResponder.cs b/src/Mmcc.Bot/EventResponders/Moderation/MemberApplications/MemberApplicationUpdatedResponder.cs index 2654291..1c4e6d3 100644 --- a/src/Mmcc.Bot/EventResponders/Moderation/MemberApplications/MemberApplicationUpdatedResponder.cs +++ b/src/Mmcc.Bot/EventResponders/Moderation/MemberApplications/MemberApplicationUpdatedResponder.cs @@ -69,7 +69,7 @@ public async Task RespondAsync(IMessageUpdate ev, CancellationToken ct = } // return if the message isn't in #member-apps; - if (!channelName.Value.Equals(_discordSettings.ChannelNames.MemberApps)) + if (!channelName.Value!.Equals(_discordSettings.ChannelNames.MemberApps)) { return Result.FromSuccess(); } diff --git a/src/Mmcc.Bot/Mmcc.Bot.csproj b/src/Mmcc.Bot/Mmcc.Bot.csproj index c8585d6..baf6938 100644 --- a/src/Mmcc.Bot/Mmcc.Bot.csproj +++ b/src/Mmcc.Bot/Mmcc.Bot.csproj @@ -8,21 +8,21 @@ - - + + - - - - - - - - - - + + + + + + + + + + diff --git a/src/Mmcc.Bot/Program.cs b/src/Mmcc.Bot/Program.cs index 8161175..2d29d70 100644 --- a/src/Mmcc.Bot/Program.cs +++ b/src/Mmcc.Bot/Program.cs @@ -35,6 +35,7 @@ builder.AddDebug(); } }) + .AddDiscordService(provider => provider.GetRequiredService().Token) .ConfigureServices((hostContext, services) => { // config; @@ -71,11 +72,6 @@ services.AddHangfire(); }) - .AddDiscordService(provider => - { - var discordConfig = provider.GetRequiredService(); - return discordConfig.Token; - }) .UseSerilog(LoggerSetup.ConfigureLogger) .UseDefaultServiceProvider(options => options.ValidateScopes = true) .Build(); From 5201c86f7e65b23e73bcab495f3df646c56f9843 Mon Sep 17 00:00:00 2001 From: TraceLD Date: Fri, 30 Sep 2022 01:42:34 +0100 Subject: [PATCH 02/30] use the new way to build the command tree --- src/Mmcc.Bot/Commands/CommandsSetup.cs | 71 +++++++++----------------- 1 file changed, 24 insertions(+), 47 deletions(-) diff --git a/src/Mmcc.Bot/Commands/CommandsSetup.cs b/src/Mmcc.Bot/Commands/CommandsSetup.cs index 7cc739b..7806023 100644 --- a/src/Mmcc.Bot/Commands/CommandsSetup.cs +++ b/src/Mmcc.Bot/Commands/CommandsSetup.cs @@ -1,6 +1,8 @@ using Microsoft.Extensions.DependencyInjection; using Mmcc.Bot.Commands.Core; +using Mmcc.Bot.Commands.Core.Help; using Mmcc.Bot.Commands.Diagnostics; +using Mmcc.Bot.Commands.Guilds; using Mmcc.Bot.Commands.Minecraft; using Mmcc.Bot.Commands.Minecraft.Restarts; using Mmcc.Bot.Commands.Moderation; @@ -27,55 +29,30 @@ public static class CommandsSetup /// The . public static IServiceCollection AddBotCommands(this IServiceCollection services) { - // core commands; - //services.AddCommandGroup(); - //services.AddCommandGroup(); - services.AddCommandGroup(); - - // tags; - services.AddCommandGroup(); - services.AddCommandGroup(); - - // diagnostics; - services.AddCommandGroup(); - - // in game; - services.AddCommandGroup(); - services.AddCommandGroup(); - - // member apps; - services.AddCommandGroup(); - - // moderation; - services.AddCommandGroup(); - services.AddCommandGroup(); - services.AddCommandGroup(); - services.AddCommandGroup(); - services.AddDiscordCommands(); - /*services.AddCommandTree() - // add core commands; - .WithCommandGroup() - .WithCommandGroup() - .WithCommandGroup() - // add tags; - .WithCommandGroup() - .WithCommandGroup() - // add diagnostics; - .WithCommandGroup() - // add in-game; - .WithCommandGroup() - .WithCommandGroup() - // add member apps; - .WithCommandGroup() - // add moderation; - .WithCommandGroup() - .WithCommandGroup() - .WithCommandGroup() - .WithCommandGroup() - // and build it; - .Finish();*/ + services.AddCommandTree() + // add core commands; + .WithCommandGroup() + .WithCommandGroup() + .WithCommandGroup() + // add tags; + .WithCommandGroup() + .WithCommandGroup() + // add diagnostics; + .WithCommandGroup() + // add in-game; + .WithCommandGroup() + .WithCommandGroup() + // add member apps; + .WithCommandGroup() + // add moderation; + .WithCommandGroup() + .WithCommandGroup() + .WithCommandGroup() + .WithCommandGroup() + // and build it; + .Finish(); return services; } From 62b40373fd225c5e5cfb782bae00500305e162d3 Mon Sep 17 00:00:00 2001 From: TraceLD Date: Fri, 30 Sep 2022 01:51:45 +0100 Subject: [PATCH 03/30] .NET 7.0 LETS FUUUUUCKING GOOOOOOOOOOOOOOOOOOOOO --- src/Mmcc.Bot.Caching/Mmcc.Bot.Caching.csproj | 2 +- .../DependencyInjection/ServiceCollectionExtensions.cs | 8 +++++++- .../Mmcc.Bot.Common.Extensions.csproj | 2 +- .../Abstractions/Rest/DiscordRestGuildApiExtensions.cs | 2 +- src/Mmcc.Bot.Common/Mmcc.Bot.Common.csproj | 2 +- src/Mmcc.Bot.Database/DesignTimeBotContextFactory.cs | 2 +- src/Mmcc.Bot.Database/Mmcc.Bot.Database.csproj | 3 +-- src/Mmcc.Bot.Mojang/Mmcc.Bot.Mojang.csproj | 3 +-- src/Mmcc.Bot.Polychat/Mmcc.Bot.Polychat.csproj | 3 +-- .../Mmcc.Bot.RemoraAbstractions.csproj | 3 +-- src/Mmcc.Bot/Mmcc.Bot.csproj | 2 +- 11 files changed, 17 insertions(+), 15 deletions(-) diff --git a/src/Mmcc.Bot.Caching/Mmcc.Bot.Caching.csproj b/src/Mmcc.Bot.Caching/Mmcc.Bot.Caching.csproj index 6c07f36..92ecf99 100644 --- a/src/Mmcc.Bot.Caching/Mmcc.Bot.Caching.csproj +++ b/src/Mmcc.Bot.Caching/Mmcc.Bot.Caching.csproj @@ -1,7 +1,7 @@ - net6.0 + net7.0 enable 10 diff --git a/src/Mmcc.Bot.Common.Extensions/Microsoft/Extensions/DependencyInjection/ServiceCollectionExtensions.cs b/src/Mmcc.Bot.Common.Extensions/Microsoft/Extensions/DependencyInjection/ServiceCollectionExtensions.cs index 7c2afcb..9806af0 100644 --- a/src/Mmcc.Bot.Common.Extensions/Microsoft/Extensions/DependencyInjection/ServiceCollectionExtensions.cs +++ b/src/Mmcc.Bot.Common.Extensions/Microsoft/Extensions/DependencyInjection/ServiceCollectionExtensions.cs @@ -1,4 +1,5 @@ -using FluentValidation; +using System; +using FluentValidation; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Options; @@ -33,6 +34,11 @@ IConfigurationSection configurationSection where TValidator : AbstractValidator, new() { var config = configurationSection.Get(); + if (config is null) + { + throw new Exception($"Could not match a configuration section to {typeof(TConfig)}"); + } + var validator = new TValidator(); validator.ValidateAndThrow(config); diff --git a/src/Mmcc.Bot.Common.Extensions/Mmcc.Bot.Common.Extensions.csproj b/src/Mmcc.Bot.Common.Extensions/Mmcc.Bot.Common.Extensions.csproj index c5e0ed1..1ebdc66 100644 --- a/src/Mmcc.Bot.Common.Extensions/Mmcc.Bot.Common.Extensions.csproj +++ b/src/Mmcc.Bot.Common.Extensions/Mmcc.Bot.Common.Extensions.csproj @@ -1,7 +1,7 @@ - net6.0 + net7.0 enable 10 diff --git a/src/Mmcc.Bot.Common.Extensions/Remora/Discord/API/Abstractions/Rest/DiscordRestGuildApiExtensions.cs b/src/Mmcc.Bot.Common.Extensions/Remora/Discord/API/Abstractions/Rest/DiscordRestGuildApiExtensions.cs index c348bf3..e400161 100644 --- a/src/Mmcc.Bot.Common.Extensions/Remora/Discord/API/Abstractions/Rest/DiscordRestGuildApiExtensions.cs +++ b/src/Mmcc.Bot.Common.Extensions/Remora/Discord/API/Abstractions/Rest/DiscordRestGuildApiExtensions.cs @@ -33,7 +33,7 @@ public static async Task> FindGuildChannelByName(this IDiscordR var guildChannels = getGuildChannelsResult.Entity; var channel = guildChannels .Where(c => c.Name.HasValue) - .FirstOrDefault(c => c.Name.Value.Equals(channelName)); + .FirstOrDefault(c => c.Name.Value!.Equals(channelName)); if (channel is null) { return new NotFoundError( diff --git a/src/Mmcc.Bot.Common/Mmcc.Bot.Common.csproj b/src/Mmcc.Bot.Common/Mmcc.Bot.Common.csproj index e935976..362e5d5 100644 --- a/src/Mmcc.Bot.Common/Mmcc.Bot.Common.csproj +++ b/src/Mmcc.Bot.Common/Mmcc.Bot.Common.csproj @@ -1,7 +1,7 @@ - net6.0 + net7.0 enable 10 diff --git a/src/Mmcc.Bot.Database/DesignTimeBotContextFactory.cs b/src/Mmcc.Bot.Database/DesignTimeBotContextFactory.cs index ef7fab6..3013698 100644 --- a/src/Mmcc.Bot.Database/DesignTimeBotContextFactory.cs +++ b/src/Mmcc.Bot.Database/DesignTimeBotContextFactory.cs @@ -21,7 +21,7 @@ public BotContext CreateDbContext(string[] args) var optionsBuilder = new DbContextOptionsBuilder(); var boundConfig = config.GetSection("MySql").Get(); var connString = - $"Server={boundConfig.ServerIp};Port={boundConfig.Port};Database={boundConfig.DatabaseName};Uid={boundConfig.Username};Pwd={boundConfig.Password};Allow User Variables=True"; + $"Server={boundConfig!.ServerIp};Port={boundConfig.Port};Database={boundConfig.DatabaseName};Uid={boundConfig.Username};Pwd={boundConfig.Password};Allow User Variables=True"; optionsBuilder.UseMySql( connString, ServerVersion.Parse("10.4.11-mariadb"), diff --git a/src/Mmcc.Bot.Database/Mmcc.Bot.Database.csproj b/src/Mmcc.Bot.Database/Mmcc.Bot.Database.csproj index a2bf5a4..befe1a2 100644 --- a/src/Mmcc.Bot.Database/Mmcc.Bot.Database.csproj +++ b/src/Mmcc.Bot.Database/Mmcc.Bot.Database.csproj @@ -1,9 +1,8 @@ - net6.0 + net7.0 enable - 10 diff --git a/src/Mmcc.Bot.Mojang/Mmcc.Bot.Mojang.csproj b/src/Mmcc.Bot.Mojang/Mmcc.Bot.Mojang.csproj index 9c2b785..658e8c5 100644 --- a/src/Mmcc.Bot.Mojang/Mmcc.Bot.Mojang.csproj +++ b/src/Mmcc.Bot.Mojang/Mmcc.Bot.Mojang.csproj @@ -1,9 +1,8 @@ - net6.0 + net7.0 enable - 10 diff --git a/src/Mmcc.Bot.Polychat/Mmcc.Bot.Polychat.csproj b/src/Mmcc.Bot.Polychat/Mmcc.Bot.Polychat.csproj index fa670f4..3b12f81 100644 --- a/src/Mmcc.Bot.Polychat/Mmcc.Bot.Polychat.csproj +++ b/src/Mmcc.Bot.Polychat/Mmcc.Bot.Polychat.csproj @@ -1,9 +1,8 @@ - net6.0 + net7.0 enable - 10 diff --git a/src/Mmcc.Bot.RemoraAbstractions/Mmcc.Bot.RemoraAbstractions.csproj b/src/Mmcc.Bot.RemoraAbstractions/Mmcc.Bot.RemoraAbstractions.csproj index 7cdc5de..b4c2011 100644 --- a/src/Mmcc.Bot.RemoraAbstractions/Mmcc.Bot.RemoraAbstractions.csproj +++ b/src/Mmcc.Bot.RemoraAbstractions/Mmcc.Bot.RemoraAbstractions.csproj @@ -1,9 +1,8 @@ - net6.0 + net7.0 enable - 10 diff --git a/src/Mmcc.Bot/Mmcc.Bot.csproj b/src/Mmcc.Bot/Mmcc.Bot.csproj index baf6938..3a64747 100644 --- a/src/Mmcc.Bot/Mmcc.Bot.csproj +++ b/src/Mmcc.Bot/Mmcc.Bot.csproj @@ -1,7 +1,7 @@  - net6.0 + net7.0 dotnet-Mmcc.Bot-4BA5AD38-B3B4-456A-853F-080768D88F42 enable 10 From 04ded1939ed07783fa9bff6537e5cee8bc2d8140 Mon Sep 17 00:00:00 2001 From: TraceLD Date: Sat, 1 Oct 2022 04:09:50 +0100 Subject: [PATCH 04/30] finish proof of concept interaction --- .../ButtonHandlerRepository.cs | 84 --------- src/Mmcc.Bot.Caching/CachingSetup.cs | 17 -- .../Entities/ButtonHandler.cs | 12 -- .../Entities/HandleableButton.cs | 37 ---- .../Caching/ButtonExtensions.cs | 14 -- .../Results/ValidationFailuresExtensions.cs | 34 ---- .../Mmcc.Bot.Common.Extensions.csproj | 5 +- .../Errors/InteractionExpiredError.cs | 9 + .../Mmcc.Bot.InMemoryStore.csproj} | 6 +- .../Stores/MessageMemberAppContextStore.cs | 34 ++++ .../Stores/StoresSetup.cs | 17 ++ .../AbstractionsSetup.cs | 10 +- .../Mmcc.Bot.RemoraAbstractions.csproj | 2 +- .../Services/CommandResponder.cs | 95 ---------- .../Services/ErrorProcessingService.cs | 92 +++++++++ .../Services/InteractionHelperService.cs | 83 ++++++++ .../Services/InteractionResponder.cs | 67 ------- .../CommandMessageResponder.cs | 38 ++++ .../InteractionMessageResponder.cs | 38 ++++ .../MessageResponders/MessageResponderBase.cs | 79 ++++++++ .../UI/ActionRowUtils.cs | 11 ++ .../InteractionModalCallbackDataExtensions.cs | 10 + .../Extensions/MessageComponentExtensions.cs | 10 + .../UI/FluentCallbackModalBuilder.cs | 65 +++++++ .../UI/FluentTextInputBuilder.cs | 101 ++++++++++ .../Ui/ActionRowUtils.cs | 17 -- src/Mmcc.Bot.sln | 12 +- .../Commands/Core/Help/HelpCommands.cs | 6 +- .../Commands/Core/MmccInfoCommands.cs | 26 +++ .../Diagnostics/DiagnosticsCommands.cs | 6 +- src/Mmcc.Bot/Commands/Guilds/GuildCommands.cs | 6 +- .../Minecraft/MinecraftServersCommands.cs | 6 +- .../Restarts/MinecraftAutoRestartsCommands.cs | 6 +- .../Commands/Moderation/Bans/BanCommands.cs | 6 +- .../Moderation/GeneralModerationCommands.cs | 6 +- .../MemberApplicationsCommands.cs | 55 ++++-- .../PlayerInfo/PlayerInfoCommands.cs | 6 +- .../Commands/Moderation/Warns/WarnCommands.cs | 6 +- .../Tags/Management/TagsManagementCommands.cs | 6 +- .../Commands/Tags/Usage/TagsUsageCommands.cs | 6 +- .../Interactions/InteractionsSetup.cs | 25 +++ .../MemberApplicationsInteractions.cs | 178 ++++++++++++++++++ .../Middleware/ErrorNotificationMiddleware.cs | 61 +----- src/Mmcc.Bot/Mmcc.Bot.csproj | 6 +- src/Mmcc.Bot/Program.cs | 6 +- 45 files changed, 925 insertions(+), 497 deletions(-) delete mode 100644 src/Mmcc.Bot.Caching/ButtonHandlerRepository.cs delete mode 100644 src/Mmcc.Bot.Caching/CachingSetup.cs delete mode 100644 src/Mmcc.Bot.Caching/Entities/ButtonHandler.cs delete mode 100644 src/Mmcc.Bot.Caching/Entities/HandleableButton.cs delete mode 100644 src/Mmcc.Bot.Common.Extensions/Caching/ButtonExtensions.cs delete mode 100644 src/Mmcc.Bot.Common.Extensions/FluentValidation/Results/ValidationFailuresExtensions.cs create mode 100644 src/Mmcc.Bot.Common/Errors/InteractionExpiredError.cs rename src/{Mmcc.Bot.Caching/Mmcc.Bot.Caching.csproj => Mmcc.Bot.InMemoryStore/Mmcc.Bot.InMemoryStore.csproj} (54%) create mode 100644 src/Mmcc.Bot.InMemoryStore/Stores/MessageMemberAppContextStore.cs create mode 100644 src/Mmcc.Bot.InMemoryStore/Stores/StoresSetup.cs delete mode 100644 src/Mmcc.Bot.RemoraAbstractions/Services/CommandResponder.cs create mode 100644 src/Mmcc.Bot.RemoraAbstractions/Services/ErrorProcessingService.cs create mode 100644 src/Mmcc.Bot.RemoraAbstractions/Services/InteractionHelperService.cs delete mode 100644 src/Mmcc.Bot.RemoraAbstractions/Services/InteractionResponder.cs create mode 100644 src/Mmcc.Bot.RemoraAbstractions/Services/MessageResponders/CommandMessageResponder.cs create mode 100644 src/Mmcc.Bot.RemoraAbstractions/Services/MessageResponders/InteractionMessageResponder.cs create mode 100644 src/Mmcc.Bot.RemoraAbstractions/Services/MessageResponders/MessageResponderBase.cs create mode 100644 src/Mmcc.Bot.RemoraAbstractions/UI/ActionRowUtils.cs create mode 100644 src/Mmcc.Bot.RemoraAbstractions/UI/Extensions/InteractionModalCallbackDataExtensions.cs create mode 100644 src/Mmcc.Bot.RemoraAbstractions/UI/Extensions/MessageComponentExtensions.cs create mode 100644 src/Mmcc.Bot.RemoraAbstractions/UI/FluentCallbackModalBuilder.cs create mode 100644 src/Mmcc.Bot.RemoraAbstractions/UI/FluentTextInputBuilder.cs delete mode 100644 src/Mmcc.Bot.RemoraAbstractions/Ui/ActionRowUtils.cs create mode 100644 src/Mmcc.Bot/Interactions/InteractionsSetup.cs create mode 100644 src/Mmcc.Bot/Interactions/Moderation/MemberApplications/MemberApplicationsInteractions.cs diff --git a/src/Mmcc.Bot.Caching/ButtonHandlerRepository.cs b/src/Mmcc.Bot.Caching/ButtonHandlerRepository.cs deleted file mode 100644 index 3a3ba4c..0000000 --- a/src/Mmcc.Bot.Caching/ButtonHandlerRepository.cs +++ /dev/null @@ -1,84 +0,0 @@ -using System; -using Microsoft.Extensions.Caching.Memory; -using Mmcc.Bot.Caching.Entities; - -namespace Mmcc.Bot.Caching; - -/// -/// Represents a button handler repository. -/// -public interface IButtonHandlerRepository -{ - /// - /// Registers a new button handler from a object with the repository. - /// - /// object containing the handler. - void Register(HandleableButton handleableButton); - - /// - /// Registers a button handler with the repository. - /// - /// The ID the button handler corresponds to. - /// The . - void Register(ulong buttonId, ButtonHandler handler); - - /// - /// De-registers a button handler from the repository. - /// - /// The ID of the button to deregister. - void Deregister(ulong buttonId); - - /// - /// Gets a button handler for a button with a given ID or default value of if not found. - /// - /// The ID of the button for which to get a button handler. - /// The button handler corresponding to a button with a given ID or default value of if not found. - ButtonHandler? GetOrDefault(ulong buttonId); -} - -/// -public class ButtonHandlerRepository : IButtonHandlerRepository -{ - private readonly IMemoryCache _cache; - - /// - /// Sliding button handler cache expiration in minutes. - /// - private const int SlidingExpirationInMinutes = 5; - - /// - /// Absolute button handler cache expiration in minutes. - /// - /// - /// 15 minutes is how an interaction lasts by default in the Discord API. - private const int AbsoluteExpirationInMinutes = 15; - - /// - /// Instantiates a new instance of . - /// - /// The memory cache. - public ButtonHandlerRepository(IMemoryCache cache) - { - _cache = cache; - } - - /// - public void Register(HandleableButton handleableButton) => - Register(handleableButton.Id.Value, handleableButton.Handler); - - /// - public void Register(ulong buttonId, ButtonHandler handler) => - _cache.Set(buttonId, handler, new MemoryCacheEntryOptions - { - SlidingExpiration = TimeSpan.FromMinutes(SlidingExpirationInMinutes), - AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(AbsoluteExpirationInMinutes) - }); - - /// - public void Deregister(ulong buttonId) => - _cache.Remove(buttonId); - - /// - public ButtonHandler? GetOrDefault(ulong buttonId) => - _cache.Get(buttonId); -} \ No newline at end of file diff --git a/src/Mmcc.Bot.Caching/CachingSetup.cs b/src/Mmcc.Bot.Caching/CachingSetup.cs deleted file mode 100644 index 99292c0..0000000 --- a/src/Mmcc.Bot.Caching/CachingSetup.cs +++ /dev/null @@ -1,17 +0,0 @@ -using Microsoft.Extensions.DependencyInjection; - -namespace Mmcc.Bot.Caching; - -/// -/// Extension methods that register Mmcc.Bot.Caching with the service collection. -/// -public static class CachingSetup -{ - /// - /// Registers Mmcc.Bot.Caching classes with the service collection. - /// - /// The . - /// The . - public static IServiceCollection AddMmccCaching(this IServiceCollection services) => - services.AddSingleton(); -} \ No newline at end of file diff --git a/src/Mmcc.Bot.Caching/Entities/ButtonHandler.cs b/src/Mmcc.Bot.Caching/Entities/ButtonHandler.cs deleted file mode 100644 index c115a8d..0000000 --- a/src/Mmcc.Bot.Caching/Entities/ButtonHandler.cs +++ /dev/null @@ -1,12 +0,0 @@ -using System; -using Remora.Discord.API.Abstractions.Objects; -using Remora.Rest.Core; - -namespace Mmcc.Bot.Caching.Entities; - -public record ButtonHandler( - Type HandlerCommandType, - Type ContextType, - object Context, - Optional RequiredPermission = new() -); \ No newline at end of file diff --git a/src/Mmcc.Bot.Caching/Entities/HandleableButton.cs b/src/Mmcc.Bot.Caching/Entities/HandleableButton.cs deleted file mode 100644 index 150d7c2..0000000 --- a/src/Mmcc.Bot.Caching/Entities/HandleableButton.cs +++ /dev/null @@ -1,37 +0,0 @@ -using MediatR; -using Remora.Discord.API.Abstractions.Objects; -using Remora.Rest.Core; -using Remora.Results; - -namespace Mmcc.Bot.Caching.Entities; - -public record HandleableButton -{ - public Snowflake Id { get; init; } - public IButtonComponent Component { get; init; } - public ButtonHandler Handler { get; init; } - - private HandleableButton(Snowflake id, IButtonComponent component, ButtonHandler handler) - { - Id = id; - Component = component; - Handler = handler; - } - - public static HandleableButton Create( - Snowflake id, - IButtonComponent component, - TContext context, - Optional requiredPermission = new() - ) - where THandlerCommand : IRequest - where TContext : class - => new(id, component, new(typeof(THandlerCommand), typeof(TContext), context, requiredPermission)); - - public void Deconstruct(out Snowflake id, out IButtonComponent component, out ButtonHandler handler) - { - id = Id; - component = Component; - handler = Handler; - } -} \ No newline at end of file diff --git a/src/Mmcc.Bot.Common.Extensions/Caching/ButtonExtensions.cs b/src/Mmcc.Bot.Common.Extensions/Caching/ButtonExtensions.cs deleted file mode 100644 index 869120d..0000000 --- a/src/Mmcc.Bot.Common.Extensions/Caching/ButtonExtensions.cs +++ /dev/null @@ -1,14 +0,0 @@ -using Mmcc.Bot.Caching; -using Mmcc.Bot.Caching.Entities; - -namespace Mmcc.Bot.Common.Extensions.Caching -{ - public static class ButtonExtensions - { - public static HandleableButton RegisterWith(this HandleableButton handleableButton, IButtonHandlerRepository repository) - { - repository.Register(handleableButton); - return handleableButton; - } - } -} \ No newline at end of file diff --git a/src/Mmcc.Bot.Common.Extensions/FluentValidation/Results/ValidationFailuresExtensions.cs b/src/Mmcc.Bot.Common.Extensions/FluentValidation/Results/ValidationFailuresExtensions.cs deleted file mode 100644 index 73851f6..0000000 --- a/src/Mmcc.Bot.Common.Extensions/FluentValidation/Results/ValidationFailuresExtensions.cs +++ /dev/null @@ -1,34 +0,0 @@ -using System.Collections.Generic; -using System.Linq; -using FluentValidation.Results; -using Remora.Discord.API.Objects; - -namespace Mmcc.Bot.Common.Extensions.FluentValidation.Results -{ - /// - /// Extensions for . - /// - public static class ValidationFailuresExtensions - { - /// - /// Gets containing details of failures. - /// - /// Validation failures. - /// Whether the should be inline. Defaults to false. - /// containing details of failures. - public static EmbedField ToEmbedField(this IEnumerable validationFailures, bool inline = false) - { - var validationFailuresList = validationFailures.ToList(); - - if (!validationFailuresList.Any()) - { - return new("Failures", "No description."); - } - - var descriptionSb = string.Join("\n", - validationFailuresList - .Select((vf, i) => $"{i + 1}) {vf.ToString().Replace('\'', '`')}")); - return new("Reason(s)", descriptionSb, inline); - } - } -} \ No newline at end of file diff --git a/src/Mmcc.Bot.Common.Extensions/Mmcc.Bot.Common.Extensions.csproj b/src/Mmcc.Bot.Common.Extensions/Mmcc.Bot.Common.Extensions.csproj index 1ebdc66..23df98a 100644 --- a/src/Mmcc.Bot.Common.Extensions/Mmcc.Bot.Common.Extensions.csproj +++ b/src/Mmcc.Bot.Common.Extensions/Mmcc.Bot.Common.Extensions.csproj @@ -7,7 +7,6 @@ - @@ -19,4 +18,8 @@ + + + + diff --git a/src/Mmcc.Bot.Common/Errors/InteractionExpiredError.cs b/src/Mmcc.Bot.Common/Errors/InteractionExpiredError.cs new file mode 100644 index 0000000..553cb23 --- /dev/null +++ b/src/Mmcc.Bot.Common/Errors/InteractionExpiredError.cs @@ -0,0 +1,9 @@ +using Remora.Commands.Trees.Nodes; +using Remora.Results; + +namespace Mmcc.Bot.Common.Errors; + +/// +/// Represents a failure resulting from an interaction having expired in the corresponding store. +/// +public record InteractionExpiredError(string Message, IChildNode? Node = default) : ResultError(Message); \ No newline at end of file diff --git a/src/Mmcc.Bot.Caching/Mmcc.Bot.Caching.csproj b/src/Mmcc.Bot.InMemoryStore/Mmcc.Bot.InMemoryStore.csproj similarity index 54% rename from src/Mmcc.Bot.Caching/Mmcc.Bot.Caching.csproj rename to src/Mmcc.Bot.InMemoryStore/Mmcc.Bot.InMemoryStore.csproj index 92ecf99..90a8dec 100644 --- a/src/Mmcc.Bot.Caching/Mmcc.Bot.Caching.csproj +++ b/src/Mmcc.Bot.InMemoryStore/Mmcc.Bot.InMemoryStore.csproj @@ -1,15 +1,13 @@ - net7.0 + net6.0 + enable enable - 10 - - diff --git a/src/Mmcc.Bot.InMemoryStore/Stores/MessageMemberAppContextStore.cs b/src/Mmcc.Bot.InMemoryStore/Stores/MessageMemberAppContextStore.cs new file mode 100644 index 0000000..a9c42df --- /dev/null +++ b/src/Mmcc.Bot.InMemoryStore/Stores/MessageMemberAppContextStore.cs @@ -0,0 +1,34 @@ +using Microsoft.Extensions.Caching.Memory; + +namespace Mmcc.Bot.InMemoryStore.Stores; + +public interface IMessageMemberAppContextStore +{ + void Add(ulong messageId, int memberAppId); + void Remove(ulong messageId); + int? GetOrDefault(ulong key); +} + +public class MessageMemberAppContextStore : IMessageMemberAppContextStore +{ + private readonly IMemoryCache _memCache; + + private readonly TimeSpan _slidingExpiration = TimeSpan.FromMinutes(15); + public readonly TimeSpan _absoluteExpiration = TimeSpan.FromHours(1); + + public MessageMemberAppContextStore(IMemoryCache memCache) + => _memCache = memCache; + + public void Add(ulong messageId, int memberAppId) + => _memCache.Set(messageId, memberAppId, new MemoryCacheEntryOptions + { + SlidingExpiration = _slidingExpiration, + AbsoluteExpirationRelativeToNow = _absoluteExpiration + }); + + public void Remove(ulong messageId) + => _memCache.Remove(messageId); + + public int? GetOrDefault(ulong key) + => _memCache.Get(key); +} \ No newline at end of file diff --git a/src/Mmcc.Bot.InMemoryStore/Stores/StoresSetup.cs b/src/Mmcc.Bot.InMemoryStore/Stores/StoresSetup.cs new file mode 100644 index 0000000..908265c --- /dev/null +++ b/src/Mmcc.Bot.InMemoryStore/Stores/StoresSetup.cs @@ -0,0 +1,17 @@ +using Microsoft.Extensions.DependencyInjection; + +namespace Mmcc.Bot.InMemoryStore.Stores; + +/// +/// Extension methods that register stores in Mmcc.Bot.InMemoryStore.Stores with the service collection. +/// +public static class StoresSetup +{ + /// + /// Registers stores in Mmcc.Bot.InMemoryStore.Store classes with the service collection. + /// + /// The . + /// The . + public static IServiceCollection AddInMemoryStores(this IServiceCollection services) => + services.AddSingleton(); +} \ No newline at end of file diff --git a/src/Mmcc.Bot.RemoraAbstractions/AbstractionsSetup.cs b/src/Mmcc.Bot.RemoraAbstractions/AbstractionsSetup.cs index 7c99c2a..43a6c0c 100644 --- a/src/Mmcc.Bot.RemoraAbstractions/AbstractionsSetup.cs +++ b/src/Mmcc.Bot.RemoraAbstractions/AbstractionsSetup.cs @@ -2,6 +2,7 @@ using Mmcc.Bot.RemoraAbstractions.Conditions; using Mmcc.Bot.RemoraAbstractions.Parsers; using Mmcc.Bot.RemoraAbstractions.Services; +using Mmcc.Bot.RemoraAbstractions.Services.MessageResponders; using Remora.Commands.Extensions; namespace Mmcc.Bot.RemoraAbstractions; @@ -21,9 +22,12 @@ public static IServiceCollection AddRemoraAbstractions(this IServiceCollection s services.AddScoped(); services.AddScoped(); services.AddScoped(); - services.AddScoped(); - services.AddScoped(); - + services.AddScoped(); + services.AddScoped(); + + services.AddScoped(); + services.AddScoped(); + services.AddCondition(); services.AddCondition(); diff --git a/src/Mmcc.Bot.RemoraAbstractions/Mmcc.Bot.RemoraAbstractions.csproj b/src/Mmcc.Bot.RemoraAbstractions/Mmcc.Bot.RemoraAbstractions.csproj index b4c2011..bcee1b1 100644 --- a/src/Mmcc.Bot.RemoraAbstractions/Mmcc.Bot.RemoraAbstractions.csproj +++ b/src/Mmcc.Bot.RemoraAbstractions/Mmcc.Bot.RemoraAbstractions.csproj @@ -6,11 +6,11 @@ + - diff --git a/src/Mmcc.Bot.RemoraAbstractions/Services/CommandResponder.cs b/src/Mmcc.Bot.RemoraAbstractions/Services/CommandResponder.cs deleted file mode 100644 index dc282a9..0000000 --- a/src/Mmcc.Bot.RemoraAbstractions/Services/CommandResponder.cs +++ /dev/null @@ -1,95 +0,0 @@ -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using Remora.Discord.API.Abstractions.Objects; -using Remora.Discord.API.Abstractions.Rest; -using Remora.Discord.API.Objects; -using Remora.Discord.Commands.Contexts; -using Remora.Rest.Core; -using Remora.Results; - -namespace Mmcc.Bot.RemoraAbstractions.Services; - -/// -/// Responds to the message the command was invoked by. -/// -public interface ICommandResponder -{ - /// - /// Responds to the message the command was invoked by with a message with content. - /// - /// Content of the response. - /// Result of the asynchronous operation. - Task Respond(string message); - - /// - /// Responds to the message the command was invoked by with a message with s content. - /// - /// Content of the response. - /// Result of the asynchronous operation. - Task Respond(params Embed[] embeds); - - /// - /// Responds to the message the command was invoked by with a message with s content. - /// - /// Content of the response. - /// Result of the asynchronous operation. - Task Respond(List embeds); - - /// - /// Responds to the message the command was invoked by with a message with components. - /// - /// Components content of the response. - /// String content of the response. - /// Embeds content of the response. - /// - Task RespondWithComponents(IReadOnlyList components, Optional content = new(), params Embed[] embeds); -} - -/// -public class CommandResponder : ICommandResponder -{ - private readonly MessageContext _context; - private readonly IDiscordRestChannelAPI _channelApi; - - /// - /// Instantiates a new instance of the . - /// - /// The message context. - /// The channel API. - public CommandResponder(MessageContext context, IDiscordRestChannelAPI channelApi) - { - _context = context; - _channelApi = channelApi; - } - - /// - public async Task Respond(string message) => - await _channelApi.CreateMessageAsync( - channelID: _context.ChannelID, - content: message, - messageReference: new MessageReference(_context.MessageID, FailIfNotExists: false) - ); - - /// - public async Task Respond(params Embed[] embeds) => - await Respond(embeds.ToList()); - - /// - public async Task Respond(List embeds) => - await _channelApi.CreateMessageAsync( - channelID: _context.ChannelID, - embeds: embeds, - messageReference: new MessageReference(_context.MessageID, FailIfNotExists: false) - ); - - /// - public async Task RespondWithComponents(IReadOnlyList components, Optional content = new(), params Embed[] embeds) => - await _channelApi.CreateMessageAsync( - channelID: _context.ChannelID, - content: content, - embeds: embeds, - components: new(components), - messageReference: new MessageReference(_context.MessageID, FailIfNotExists: false) - ); -} \ No newline at end of file diff --git a/src/Mmcc.Bot.RemoraAbstractions/Services/ErrorProcessingService.cs b/src/Mmcc.Bot.RemoraAbstractions/Services/ErrorProcessingService.cs new file mode 100644 index 0000000..f0df52b --- /dev/null +++ b/src/Mmcc.Bot.RemoraAbstractions/Services/ErrorProcessingService.cs @@ -0,0 +1,92 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using FluentValidation.Results; +using Mmcc.Bot.Common.Errors; +using Mmcc.Bot.Common.Models.Colours; +using Mmcc.Bot.Common.Models.Settings; +using Mmcc.Bot.Common.Statics; +using Remora.Commands.Results; +using Remora.Discord.API.Objects; +using Remora.Results; + +namespace Mmcc.Bot.RemoraAbstractions.Services; + +public interface IErrorProcessingService +{ + public Embed GetErrorEmbed(IResultError err); +} + +public class ErrorProcessingService : IErrorProcessingService +{ + private readonly IColourPalette _colourPalette; + private readonly DiscordSettings _discordSettings; + + public ErrorProcessingService(IColourPalette colourPalette, DiscordSettings discordSettings) + { + _colourPalette = colourPalette; + _discordSettings = discordSettings; + } + + public Embed GetErrorEmbed(IResultError err) + { + var errorEmbed = new Embed + { + Thumbnail = EmbedProperties.MmccLogoThumbnail, + Colour = _colourPalette.Red, + Timestamp = DateTimeOffset.UtcNow + }; + errorEmbed = err switch + { + CommandNotFoundError cnfe => errorEmbed with + { + Title = ":exclamation: Command not found", + Description = $"Could not find a matching command for `{_discordSettings.Prefix}{cnfe.OriginalInput}`." + }, + ValidationError(var message, var validationFailures, _) => errorEmbed with + { + Title = ":exclamation: Validation error.", + Description = message.Replace('\'', '`'), + Fields = new List {ValidationFailuresToEmbedField(validationFailures)} + }, + NotFoundError => errorEmbed with + { + Title = ":x: Resource not found.", + Description = err.Message + }, + null => errorEmbed with + { + Title = ":exclamation: Error.", + Description = "Unknown error." + }, + _ => errorEmbed with + { + Title = $":x: {err.GetType()}.", + Description = err.Message + } + }; + + return errorEmbed; + } + + /// + /// Gets containing details of failures. + /// + /// Validation failures. + /// Whether the should be inline. Defaults to false. + /// containing details of failures. + private static EmbedField ValidationFailuresToEmbedField(IEnumerable validationFailures, bool inline = false) + { + var validationFailuresList = validationFailures.ToList(); + + if (!validationFailuresList.Any()) + { + return new("Failures", "No description."); + } + + var descriptionSb = string.Join("\n", + validationFailuresList + .Select((vf, i) => $"{i + 1}) {vf.ToString().Replace('\'', '`')}")); + return new("Reason(s)", descriptionSb, inline); + } +} \ No newline at end of file diff --git a/src/Mmcc.Bot.RemoraAbstractions/Services/InteractionHelperService.cs b/src/Mmcc.Bot.RemoraAbstractions/Services/InteractionHelperService.cs new file mode 100644 index 0000000..ff754a0 --- /dev/null +++ b/src/Mmcc.Bot.RemoraAbstractions/Services/InteractionHelperService.cs @@ -0,0 +1,83 @@ +using System.Linq; +using System.Threading.Tasks; +using Mmcc.Bot.Common.Models.Settings; +using Mmcc.Bot.RemoraAbstractions.UI.Extensions; +using Remora.Discord.API.Abstractions.Objects; +using Remora.Discord.API.Abstractions.Rest; +using Remora.Discord.API.Objects; +using Remora.Discord.Commands.Contexts; +using Remora.Results; + +namespace Mmcc.Bot.RemoraAbstractions.Services; + +public interface IInteractionHelperService +{ + Task NotifyDeferredMessageIsComing(); + Task RespondWithModal(InteractionModalCallbackData modalCallbackData); + Task SendFollowup(params Embed[] embeds); + Task SendFollowup(string msg); + Task SendErrorNotification(IResultError err); +} + +public class InteractionHelperService : IInteractionHelperService +{ + private readonly InteractionContext _context; + private readonly IDiscordRestInteractionAPI _interactionApi; + private readonly DiscordSettings _discordSettings; + private readonly IErrorProcessingService _errorProcessingService; + + public InteractionHelperService( + InteractionContext context, + IDiscordRestInteractionAPI interactionApi, + DiscordSettings discordSettings, + IErrorProcessingService errorProcessingService + ) + { + _context = context; + _interactionApi = interactionApi; + _discordSettings = discordSettings; + _errorProcessingService = errorProcessingService; + } + + public async Task NotifyDeferredMessageIsComing() + => await _interactionApi.CreateInteractionResponseAsync(_context.ID, _context.Token, + new InteractionResponse(InteractionCallbackType.DeferredChannelMessageWithSource)); + + public async Task RespondWithModal(InteractionModalCallbackData modalCallbackData) + => await _interactionApi.CreateInteractionResponseAsync(_context.ID, _context.Token, + modalCallbackData.GetInteractionResponse()); + + public async Task SendFollowup(params Embed[] embeds) + { + var res = await _interactionApi.CreateFollowupMessageAsync( + new(_discordSettings.ApplicationId), + _context.Token, + embeds: embeds.ToList() + ); + return res.IsSuccess + ? Result.FromSuccess() + : Result.FromError(res); + } + + public async Task SendFollowup(string msg) + { + var res = await _interactionApi.CreateFollowupMessageAsync( + new(_discordSettings.ApplicationId), + _context.Token, + msg + ); + return res.IsSuccess + ? Result.FromSuccess() + : Result.FromError(res); + } + + public async Task SendErrorNotification(IResultError err) + { + var errorEmbed = _errorProcessingService.GetErrorEmbed(err); + + var sendErrorEmbed = await SendFollowup(errorEmbed); + return !sendErrorEmbed.IsSuccess + ? Result.FromError(sendErrorEmbed.Error) + : Result.FromSuccess(); + } +} \ No newline at end of file diff --git a/src/Mmcc.Bot.RemoraAbstractions/Services/InteractionResponder.cs b/src/Mmcc.Bot.RemoraAbstractions/Services/InteractionResponder.cs deleted file mode 100644 index ca75f22..0000000 --- a/src/Mmcc.Bot.RemoraAbstractions/Services/InteractionResponder.cs +++ /dev/null @@ -1,67 +0,0 @@ -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using Mmcc.Bot.Common.Models.Settings; -using Remora.Discord.API.Abstractions.Objects; -using Remora.Discord.API.Abstractions.Rest; -using Remora.Discord.API.Objects; -using Remora.Rest.Core; -using Remora.Results; - -namespace Mmcc.Bot.RemoraAbstractions.Services; - -public interface IInteractionResponder -{ - Task NotifyDeferredMessageIsComing( - Snowflake interactionId, - string interactionToken, - CancellationToken ct = default - ); - - Task SendFollowup(string interactionToken, params Embed[] embeds); - - Task SendFollowup(string interactionToken, string msg); -} - -public class InteractionResponder : IInteractionResponder -{ - private readonly DiscordSettings _discordSettings; - private readonly IDiscordRestInteractionAPI _interactionApi; - - public InteractionResponder(DiscordSettings discordSettings, IDiscordRestInteractionAPI interactionApi) - { - _discordSettings = discordSettings; - _interactionApi = interactionApi; - } - - public async Task NotifyDeferredMessageIsComing( - Snowflake interactionId, - string interactionToken, - CancellationToken ct = default - ) => await _interactionApi.CreateInteractionResponseAsync(interactionId, interactionToken, - new InteractionResponse(InteractionCallbackType.DeferredChannelMessageWithSource), ct: ct); - - public async Task SendFollowup(string interactionToken, params Embed[] embeds) - { - var res = await _interactionApi.CreateFollowupMessageAsync( - new(_discordSettings.ApplicationId), - interactionToken, - embeds: embeds.ToList() - ); - return res.IsSuccess - ? Result.FromSuccess() - : Result.FromError(res); - } - - public async Task SendFollowup(string interactionToken, string msg) - { - var res = await _interactionApi.CreateFollowupMessageAsync( - new(_discordSettings.ApplicationId), - interactionToken, - msg - ); - return res.IsSuccess - ? Result.FromSuccess() - : Result.FromError(res); - } -} \ No newline at end of file diff --git a/src/Mmcc.Bot.RemoraAbstractions/Services/MessageResponders/CommandMessageResponder.cs b/src/Mmcc.Bot.RemoraAbstractions/Services/MessageResponders/CommandMessageResponder.cs new file mode 100644 index 0000000..1a1ad9c --- /dev/null +++ b/src/Mmcc.Bot.RemoraAbstractions/Services/MessageResponders/CommandMessageResponder.cs @@ -0,0 +1,38 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using Remora.Discord.API.Abstractions.Objects; +using Remora.Discord.API.Abstractions.Rest; +using Remora.Discord.API.Objects; +using Remora.Discord.Commands.Contexts; +using Remora.Rest.Core; +using Remora.Results; + +namespace Mmcc.Bot.RemoraAbstractions.Services.MessageResponders; + +public class CommandMessageResponder : MessageResponderBase +{ + private readonly MessageContext _context; + + /// + /// Instantiates a new instance of the . + /// + /// The message context. + /// The channel API. + public CommandMessageResponder(MessageContext context, IDiscordRestChannelAPI channelApi) : base(channelApi) + => _context = context; + + /// + public override async Task Respond(string message) + => await Respond(_context.MessageID, _context.ChannelID, message); + + /// + public override async Task Respond(List embeds) + => await Respond(_context.MessageID, _context.ChannelID, embeds); + + /// + public override async Task RespondWithComponents( + IReadOnlyList components, + Optional content = new(), + params Embed[] embeds + ) => await RespondWithComponents(_context.MessageID, _context.ChannelID, components, content, embeds); +} diff --git a/src/Mmcc.Bot.RemoraAbstractions/Services/MessageResponders/InteractionMessageResponder.cs b/src/Mmcc.Bot.RemoraAbstractions/Services/MessageResponders/InteractionMessageResponder.cs new file mode 100644 index 0000000..b946d1e --- /dev/null +++ b/src/Mmcc.Bot.RemoraAbstractions/Services/MessageResponders/InteractionMessageResponder.cs @@ -0,0 +1,38 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using Remora.Discord.API.Abstractions.Objects; +using Remora.Discord.API.Abstractions.Rest; +using Remora.Discord.API.Objects; +using Remora.Discord.Commands.Contexts; +using Remora.Rest.Core; +using Remora.Results; + +namespace Mmcc.Bot.RemoraAbstractions.Services.MessageResponders; + +public class InteractionMessageResponder : MessageResponderBase +{ + private readonly InteractionContext _context; + + /// + /// Instantiates a new instance of the . + /// + /// The message context. + /// The channel API. + public InteractionMessageResponder(InteractionContext context, IDiscordRestChannelAPI channelApi) : base(channelApi) + => _context = context; + + /// + public override async Task Respond(string message) + => await Respond(_context.Message.Value.ID, _context.ChannelID, message); + + /// + public override async Task Respond(List embeds) + => await Respond(_context.Message.Value.ID, _context.ChannelID, embeds); + + /// + public override async Task RespondWithComponents( + IReadOnlyList components, + Optional content = new(), + params Embed[] embeds + ) => await RespondWithComponents(_context.Message.Value.ID, _context.ChannelID, components, content, embeds); +} \ No newline at end of file diff --git a/src/Mmcc.Bot.RemoraAbstractions/Services/MessageResponders/MessageResponderBase.cs b/src/Mmcc.Bot.RemoraAbstractions/Services/MessageResponders/MessageResponderBase.cs new file mode 100644 index 0000000..5801dcc --- /dev/null +++ b/src/Mmcc.Bot.RemoraAbstractions/Services/MessageResponders/MessageResponderBase.cs @@ -0,0 +1,79 @@ +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Remora.Discord.API.Abstractions.Objects; +using Remora.Discord.API.Abstractions.Rest; +using Remora.Discord.API.Objects; +using Remora.Rest.Core; +using Remora.Results; + +namespace Mmcc.Bot.RemoraAbstractions.Services.MessageResponders; + +public abstract class MessageResponderBase +{ + private readonly IDiscordRestChannelAPI _channelApi; + + protected MessageResponderBase(IDiscordRestChannelAPI channelApi) + { + _channelApi = channelApi; + } + + /// + /// Responds to the message the command was invoked by with a message with content. + /// + /// Content of the response. + /// Result of the asynchronous operation. + public abstract Task Respond(string message); + + /// + /// Responds to the message the command was invoked by with a message with s content. + /// + /// Content of the response. + /// Result of the asynchronous operation. + public async Task Respond(params Embed[] embeds) => + await Respond(embeds.ToList()); + + /// + /// Responds to the message the command was invoked by with a message with s content. + /// + /// Content of the response. + /// Result of the asynchronous operation. + public abstract Task Respond(List embeds); + + /// + /// Responds to the message the command was invoked by with a message with components. + /// + /// Components content of the response. + /// String content of the response. + /// Embeds content of the response. + /// + public abstract Task RespondWithComponents(IReadOnlyList components, Optional content = new(), params Embed[] embeds); + + protected async Task Respond(Snowflake parentMessageId, Snowflake parentMessageChannelId, string message) => + await _channelApi.CreateMessageAsync( + channelID: parentMessageChannelId, + content: message, + messageReference: new MessageReference(parentMessageId, FailIfNotExists: false) + ); + + protected async Task Respond(Snowflake parentMessageId, Snowflake parentMessageChannelId, List embeds) => + await _channelApi.CreateMessageAsync( + channelID: parentMessageChannelId, + embeds: embeds, + messageReference: new MessageReference(parentMessageId, FailIfNotExists: false) + ); + + protected async Task RespondWithComponents( + Snowflake parentMessageId, + Snowflake parentMessageChannelId, + IReadOnlyList components, + Optional content = new(), + params Embed[] embeds + ) => await _channelApi.CreateMessageAsync( + channelID: parentMessageChannelId, + content: content, + embeds: embeds, + components: new(components), + messageReference: new MessageReference(parentMessageId, FailIfNotExists: false) + ); +} \ No newline at end of file diff --git a/src/Mmcc.Bot.RemoraAbstractions/UI/ActionRowUtils.cs b/src/Mmcc.Bot.RemoraAbstractions/UI/ActionRowUtils.cs new file mode 100644 index 0000000..e8ca228 --- /dev/null +++ b/src/Mmcc.Bot.RemoraAbstractions/UI/ActionRowUtils.cs @@ -0,0 +1,11 @@ +using System.Collections.Generic; +using Remora.Discord.API.Abstractions.Objects; +using Remora.Discord.API.Objects; + +namespace Mmcc.Bot.RemoraAbstractions.UI; + +public static class ActionRowUtils +{ + public static ActionRowComponent CreateActionRowWithComponents(params IMessageComponent[] childComponents) + => new(childComponents); +} \ No newline at end of file diff --git a/src/Mmcc.Bot.RemoraAbstractions/UI/Extensions/InteractionModalCallbackDataExtensions.cs b/src/Mmcc.Bot.RemoraAbstractions/UI/Extensions/InteractionModalCallbackDataExtensions.cs new file mode 100644 index 0000000..430327f --- /dev/null +++ b/src/Mmcc.Bot.RemoraAbstractions/UI/Extensions/InteractionModalCallbackDataExtensions.cs @@ -0,0 +1,10 @@ +using Remora.Discord.API.Abstractions.Objects; +using Remora.Discord.API.Objects; + +namespace Mmcc.Bot.RemoraAbstractions.UI.Extensions; + +public static class InteractionModalCallbackDataExtensions +{ + public static InteractionResponse GetInteractionResponse(this InteractionModalCallbackData modalData) + => new(InteractionCallbackType.Modal, new(modalData)); +} \ No newline at end of file diff --git a/src/Mmcc.Bot.RemoraAbstractions/UI/Extensions/MessageComponentExtensions.cs b/src/Mmcc.Bot.RemoraAbstractions/UI/Extensions/MessageComponentExtensions.cs new file mode 100644 index 0000000..7f4b957 --- /dev/null +++ b/src/Mmcc.Bot.RemoraAbstractions/UI/Extensions/MessageComponentExtensions.cs @@ -0,0 +1,10 @@ +using System.Collections.Generic; +using Remora.Discord.API.Abstractions.Objects; +using Remora.Discord.API.Objects; + +namespace Mmcc.Bot.RemoraAbstractions.UI.Extensions; + +public static class MessageComponentExtensions +{ + public static List AsList(this IMessageComponent c) => new() {c}; +} \ No newline at end of file diff --git a/src/Mmcc.Bot.RemoraAbstractions/UI/FluentCallbackModalBuilder.cs b/src/Mmcc.Bot.RemoraAbstractions/UI/FluentCallbackModalBuilder.cs new file mode 100644 index 0000000..8a1b3ce --- /dev/null +++ b/src/Mmcc.Bot.RemoraAbstractions/UI/FluentCallbackModalBuilder.cs @@ -0,0 +1,65 @@ +using System.Collections.Generic; +using System.Linq; +using Remora.Discord.API.Objects; +using Remora.Discord.Interactivity; + +namespace Mmcc.Bot.RemoraAbstractions.UI; + +public class FluentCallbackModalBuilder : + FluentCallbackModalBuilder.ITitleSelectionStage, + FluentCallbackModalBuilder.IComponentsSelectionStage, + FluentCallbackModalBuilder.IBuildStage +{ + private readonly string? _customId; + + private string? _title; + private List? _components; + + private FluentCallbackModalBuilder() {} + + private FluentCallbackModalBuilder(string id) + => _customId = id; + + public static ITitleSelectionStage WithId(string id) + => new FluentCallbackModalBuilder(CustomIDHelpers.CreateModalID(id)); + + public static ITitleSelectionStage WithId(string id, params string[] path) + => new FluentCallbackModalBuilder(CustomIDHelpers.CreateModalID(id, path)); + + public IComponentsSelectionStage HasTitle(string title) + { + _title = title; + return this; + } + + public IBuildStage WithCustomActionRows(params ActionRowComponent[] actionRowComponents) + { + _components = actionRowComponents.ToList(); + return this; + } + + public IBuildStage WithActionRowFromTextInputs(params TextInputComponent[] textInputComponents) + { + _components = textInputComponents.Select(x => new ActionRowComponent(new[] {x})).ToList(); + return this; + } + + public InteractionModalCallbackData Build() + => new(_customId!, _title!, _components!); + + public interface ITitleSelectionStage + { + public IComponentsSelectionStage HasTitle(string title); + } + + public interface IComponentsSelectionStage + { + public IBuildStage WithCustomActionRows(params ActionRowComponent[] actionRowComponents); + public IBuildStage WithActionRowFromTextInputs(params TextInputComponent[] textInputComponents); + } + + public interface IBuildStage + { + public InteractionModalCallbackData Build(); + } +} \ No newline at end of file diff --git a/src/Mmcc.Bot.RemoraAbstractions/UI/FluentTextInputBuilder.cs b/src/Mmcc.Bot.RemoraAbstractions/UI/FluentTextInputBuilder.cs new file mode 100644 index 0000000..326588c --- /dev/null +++ b/src/Mmcc.Bot.RemoraAbstractions/UI/FluentTextInputBuilder.cs @@ -0,0 +1,101 @@ +using Remora.Discord.API.Abstractions.Objects; +using Remora.Discord.API.Objects; +using Remora.Rest.Core; + +namespace Mmcc.Bot.RemoraAbstractions.UI; + +public class FluentTextInputBuilder : + FluentTextInputBuilder.ITextInputStyleSelectionStage, + FluentTextInputBuilder.ILabelSelectionStage, + FluentTextInputBuilder.IRequiredSelectionStage, + FluentTextInputBuilder.IBuildStage +{ + private readonly string? _customId; + + private TextInputStyle? _textInputStyle; + private string? _label; + private bool? _isRequired; + + private Optional _minLength; + private Optional _maxLength; + private Optional _startingValue; + private Optional _placeholderValue; + + private FluentTextInputBuilder() {} + + private FluentTextInputBuilder(string id) + => _customId = id; + + public static ITextInputStyleSelectionStage WithId(string id) + => new FluentTextInputBuilder(id); + + public ILabelSelectionStage HasStyle(TextInputStyle style) + { + _textInputStyle = style; + return this; + } + + public IRequiredSelectionStage HasLabel(string label) + { + _label = label; + return this; + } + + public IBuildStage IsRequired(bool isRequired) + { + _isRequired = isRequired; + return this; + } + + public IBuildStage WithMinimumLength(int minLength) + { + _minLength = minLength; + return this; + } + + public IBuildStage WithMaximumLength(int maxLength) + { + _maxLength = maxLength; + return this; + } + + public IBuildStage HasStartingValue(string startingValue) + { + _startingValue = startingValue; + return this; + } + + public IBuildStage HasPlaceholderValue(string placeholderValue) + { + _placeholderValue = placeholderValue; + return this; + } + + public TextInputComponent Build() + => new(_customId!, _textInputStyle!.Value, _label!, _minLength, _maxLength, _isRequired!.Value, _startingValue, + _placeholderValue); + + public interface ITextInputStyleSelectionStage + { + public ILabelSelectionStage HasStyle(TextInputStyle style); + } + + public interface ILabelSelectionStage + { + public IRequiredSelectionStage HasLabel(string label); + } + + public interface IRequiredSelectionStage + { + public IBuildStage IsRequired(bool isRequired); + } + + public interface IBuildStage + { + public IBuildStage WithMinimumLength(int minLength); + public IBuildStage WithMaximumLength(int maxLength); + public IBuildStage HasStartingValue(string startingValue); + public IBuildStage HasPlaceholderValue(string placeholderValue); + public TextInputComponent Build(); + } +} \ No newline at end of file diff --git a/src/Mmcc.Bot.RemoraAbstractions/Ui/ActionRowUtils.cs b/src/Mmcc.Bot.RemoraAbstractions/Ui/ActionRowUtils.cs deleted file mode 100644 index 62ab8cd..0000000 --- a/src/Mmcc.Bot.RemoraAbstractions/Ui/ActionRowUtils.cs +++ /dev/null @@ -1,17 +0,0 @@ -using System.Collections.Generic; -using System.Linq; -using Mmcc.Bot.Caching.Entities; -using Remora.Discord.API.Abstractions.Objects; -using Remora.Discord.API.Objects; - -namespace Mmcc.Bot.RemoraAbstractions.Ui; - -// ReSharper disable once InconsistentNaming -public static class ActionRowUtils -{ - public static List FromButtons(params HandleableButton[] buttons) => - new() - { - new ActionRowComponent(buttons.Select(b => b.Component).ToList()) - }; -} \ No newline at end of file diff --git a/src/Mmcc.Bot.sln b/src/Mmcc.Bot.sln index 2a3d80e..57d7cb5 100644 --- a/src/Mmcc.Bot.sln +++ b/src/Mmcc.Bot.sln @@ -9,8 +9,6 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Mmcc.Bot.Database", "Mmcc.B EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Mmcc.Bot.Generators", "Mmcc.Bot.Generators\Mmcc.Bot.Generators.csproj", "{7E9B771E-2800-45F2-B201-DE51B33647B1}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Mmcc.Bot.Caching", "Mmcc.Bot.Caching\Mmcc.Bot.Caching.csproj", "{4680CB41-16E1-436B-9C4D-F83991B06AC6}" -EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Mmcc.Bot.RemoraAbstractions", "Mmcc.Bot.RemoraAbstractions\Mmcc.Bot.RemoraAbstractions.csproj", "{A5862FD7-B26A-4219-B31F-25256BEF4D75}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Mmcc.Bot.Polychat", "Mmcc.Bot.Polychat\Mmcc.Bot.Polychat.csproj", "{AD6B79E6-93AA-4F69-81FE-3228A29B680B}" @@ -23,6 +21,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Mmcc.Bot.Common.Extensions" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Mmcc.Bot.Common", "Mmcc.Bot.Common\Mmcc.Bot.Common.csproj", "{10014CD6-C13A-4245-A218-498066327B53}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Mmcc.Bot.InMemoryStore", "Mmcc.Bot.InMemoryStore\Mmcc.Bot.InMemoryStore.csproj", "{0BC009CA-0BA9-465A-B8A0-31E7F506F62F}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -41,10 +41,6 @@ Global {7E9B771E-2800-45F2-B201-DE51B33647B1}.Debug|Any CPU.Build.0 = Debug|Any CPU {7E9B771E-2800-45F2-B201-DE51B33647B1}.Release|Any CPU.ActiveCfg = Release|Any CPU {7E9B771E-2800-45F2-B201-DE51B33647B1}.Release|Any CPU.Build.0 = Release|Any CPU - {4680CB41-16E1-436B-9C4D-F83991B06AC6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {4680CB41-16E1-436B-9C4D-F83991B06AC6}.Debug|Any CPU.Build.0 = Debug|Any CPU - {4680CB41-16E1-436B-9C4D-F83991B06AC6}.Release|Any CPU.ActiveCfg = Release|Any CPU - {4680CB41-16E1-436B-9C4D-F83991B06AC6}.Release|Any CPU.Build.0 = Release|Any CPU {A5862FD7-B26A-4219-B31F-25256BEF4D75}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {A5862FD7-B26A-4219-B31F-25256BEF4D75}.Debug|Any CPU.Build.0 = Debug|Any CPU {A5862FD7-B26A-4219-B31F-25256BEF4D75}.Release|Any CPU.ActiveCfg = Release|Any CPU @@ -65,6 +61,10 @@ Global {10014CD6-C13A-4245-A218-498066327B53}.Debug|Any CPU.Build.0 = Debug|Any CPU {10014CD6-C13A-4245-A218-498066327B53}.Release|Any CPU.ActiveCfg = Release|Any CPU {10014CD6-C13A-4245-A218-498066327B53}.Release|Any CPU.Build.0 = Release|Any CPU + {0BC009CA-0BA9-465A-B8A0-31E7F506F62F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {0BC009CA-0BA9-465A-B8A0-31E7F506F62F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {0BC009CA-0BA9-465A-B8A0-31E7F506F62F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {0BC009CA-0BA9-465A-B8A0-31E7F506F62F}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/src/Mmcc.Bot/Commands/Core/Help/HelpCommands.cs b/src/Mmcc.Bot/Commands/Core/Help/HelpCommands.cs index f8464ed..e252f73 100644 --- a/src/Mmcc.Bot/Commands/Core/Help/HelpCommands.cs +++ b/src/Mmcc.Bot/Commands/Core/Help/HelpCommands.cs @@ -3,9 +3,11 @@ using System.Threading.Tasks; using MediatR; using Mmcc.Bot.RemoraAbstractions.Services; +using Mmcc.Bot.RemoraAbstractions.Services.MessageResponders; using Remora.Commands.Attributes; using Remora.Commands.Groups; using Remora.Discord.Commands.Contexts; +using Remora.Discord.Gateway.Responders; using Remora.Results; namespace Mmcc.Bot.Commands.Core.Help; @@ -16,7 +18,7 @@ namespace Mmcc.Bot.Commands.Core.Help; public class HelpCommands : CommandGroup { private readonly MessageContext _context; - private readonly ICommandResponder _responder; + private readonly CommandMessageResponder _responder; private readonly IDmSender _dmSender; private readonly IMediator _mediator; @@ -29,7 +31,7 @@ public class HelpCommands : CommandGroup /// The mediator. public HelpCommands( MessageContext context, - ICommandResponder responder, + CommandMessageResponder responder, IDmSender dmSender, IMediator mediator ) diff --git a/src/Mmcc.Bot/Commands/Core/MmccInfoCommands.cs b/src/Mmcc.Bot/Commands/Core/MmccInfoCommands.cs index 334aff7..84e6e7d 100644 --- a/src/Mmcc.Bot/Commands/Core/MmccInfoCommands.cs +++ b/src/Mmcc.Bot/Commands/Core/MmccInfoCommands.cs @@ -8,6 +8,7 @@ using Remora.Discord.API.Abstractions.Rest; using Remora.Discord.API.Objects; using Remora.Discord.Commands.Contexts; +using Remora.Discord.Interactivity; using Remora.Rest.Core; using Remora.Results; @@ -46,4 +47,29 @@ public async Task Mmcc() components: new(components) ); } + +#if DEBUG + [Command("demo")] + public async Task Demo() + { + var components = new List + { + new(new List + { + new + ( + ButtonComponentStyle.Primary, + Label: "Click me!", + CustomID: CustomIDHelpers.CreateButtonID("approve-btn") + ) + }) + }; + + return await _channelApi.CreateMessageAsync( + channelID: _context.ChannelID, + content: "DEMO", + components: new(components) + ); + } +#endif } \ No newline at end of file diff --git a/src/Mmcc.Bot/Commands/Diagnostics/DiagnosticsCommands.cs b/src/Mmcc.Bot/Commands/Diagnostics/DiagnosticsCommands.cs index dfb7bbd..5196cb1 100644 --- a/src/Mmcc.Bot/Commands/Diagnostics/DiagnosticsCommands.cs +++ b/src/Mmcc.Bot/Commands/Diagnostics/DiagnosticsCommands.cs @@ -8,7 +8,7 @@ using Mmcc.Bot.Common.Models.Colours; using Mmcc.Bot.Common.Statics; using Mmcc.Bot.RemoraAbstractions.Conditions.Attributes; -using Mmcc.Bot.RemoraAbstractions.Services; +using Mmcc.Bot.RemoraAbstractions.Services.MessageResponders; using Remora.Commands.Attributes; using Remora.Commands.Groups; using Remora.Discord.API.Abstractions.Objects; @@ -26,7 +26,7 @@ public class DiagnosticsCommands : CommandGroup { private readonly IColourPalette _colourPalette; private readonly IMediator _mediator; - private readonly ICommandResponder _responder; + private readonly CommandMessageResponder _responder; private readonly Dictionary _resourcesToCheck = new() { @@ -44,7 +44,7 @@ public class DiagnosticsCommands : CommandGroup public DiagnosticsCommands( IColourPalette colourPalette, IMediator mediator, - ICommandResponder responder + CommandMessageResponder responder ) { _colourPalette = colourPalette; diff --git a/src/Mmcc.Bot/Commands/Guilds/GuildCommands.cs b/src/Mmcc.Bot/Commands/Guilds/GuildCommands.cs index 5087f39..3d62509 100644 --- a/src/Mmcc.Bot/Commands/Guilds/GuildCommands.cs +++ b/src/Mmcc.Bot/Commands/Guilds/GuildCommands.cs @@ -6,7 +6,7 @@ using MediatR; using Mmcc.Bot.Common.Models.Colours; using Mmcc.Bot.RemoraAbstractions.Conditions.Attributes; -using Mmcc.Bot.RemoraAbstractions.Services; +using Mmcc.Bot.RemoraAbstractions.Services.MessageResponders; using Remora.Commands.Attributes; using Remora.Commands.Groups; using Remora.Discord.API.Abstractions.Objects; @@ -26,7 +26,7 @@ public class GuildCommands : CommandGroup private readonly MessageContext _context; private readonly IColourPalette _colourPalette; private readonly IMediator _mediator; - private readonly ICommandResponder _responder; + private readonly CommandMessageResponder _responder; /// /// Instantiates a new instance of class. @@ -39,7 +39,7 @@ public GuildCommands( MessageContext context, IColourPalette colourPalette, IMediator mediator, - ICommandResponder responder + CommandMessageResponder responder ) { _context = context; diff --git a/src/Mmcc.Bot/Commands/Minecraft/MinecraftServersCommands.cs b/src/Mmcc.Bot/Commands/Minecraft/MinecraftServersCommands.cs index 057010a..6d60bd3 100644 --- a/src/Mmcc.Bot/Commands/Minecraft/MinecraftServersCommands.cs +++ b/src/Mmcc.Bot/Commands/Minecraft/MinecraftServersCommands.cs @@ -10,7 +10,7 @@ using Mmcc.Bot.Polychat.MessageSenders; using Mmcc.Bot.Polychat.Services; using Mmcc.Bot.RemoraAbstractions.Conditions.Attributes; -using Mmcc.Bot.RemoraAbstractions.Services; +using Mmcc.Bot.RemoraAbstractions.Services.MessageResponders; using Remora.Commands.Attributes; using Remora.Commands.Groups; using Remora.Discord.API.Abstractions.Objects; @@ -32,7 +32,7 @@ public class MinecraftServersCommands : CommandGroup private readonly IMediator _mediator; private readonly IColourPalette _colourPalette; private readonly IPolychatService _polychatService; - private readonly ICommandResponder _responder; + private readonly CommandMessageResponder _responder; /// /// Instantiates a new instance of class. @@ -47,7 +47,7 @@ public MinecraftServersCommands( IMediator mediator, IColourPalette colourPalette, IPolychatService polychatService, - ICommandResponder responder + CommandMessageResponder responder ) { _context = context; diff --git a/src/Mmcc.Bot/Commands/Minecraft/Restarts/MinecraftAutoRestartsCommands.cs b/src/Mmcc.Bot/Commands/Minecraft/Restarts/MinecraftAutoRestartsCommands.cs index 8b37a2b..8900661 100644 --- a/src/Mmcc.Bot/Commands/Minecraft/Restarts/MinecraftAutoRestartsCommands.cs +++ b/src/Mmcc.Bot/Commands/Minecraft/Restarts/MinecraftAutoRestartsCommands.cs @@ -7,7 +7,7 @@ using Mmcc.Bot.Common.Statics; using Mmcc.Bot.Polychat.Jobs.Recurring.Restarts; using Mmcc.Bot.RemoraAbstractions.Conditions.Attributes; -using Mmcc.Bot.RemoraAbstractions.Services; +using Mmcc.Bot.RemoraAbstractions.Services.MessageResponders; using Mmcc.Bot.RemoraAbstractions.Timestamps; using Remora.Commands.Attributes; using Remora.Commands.Groups; @@ -23,11 +23,11 @@ public class MinecraftAutoRestartsCommands : CommandGroup { private readonly IMediator _mediator; private readonly IColourPalette _colourPalette; - private readonly ICommandResponder _responder; + private readonly CommandMessageResponder _responder; public MinecraftAutoRestartsCommands(IMediator mediator, IColourPalette colourPalette, - ICommandResponder responder + CommandMessageResponder responder ) { _mediator = mediator; diff --git a/src/Mmcc.Bot/Commands/Moderation/Bans/BanCommands.cs b/src/Mmcc.Bot/Commands/Moderation/Bans/BanCommands.cs index 4cb9568..f541c0c 100644 --- a/src/Mmcc.Bot/Commands/Moderation/Bans/BanCommands.cs +++ b/src/Mmcc.Bot/Commands/Moderation/Bans/BanCommands.cs @@ -5,7 +5,7 @@ using Mmcc.Bot.Common.Models; using Mmcc.Bot.Common.Models.Colours; using Mmcc.Bot.RemoraAbstractions.Conditions.Attributes; -using Mmcc.Bot.RemoraAbstractions.Services; +using Mmcc.Bot.RemoraAbstractions.Services.MessageResponders; using Remora.Commands.Attributes; using Remora.Commands.Groups; using Remora.Discord.API.Abstractions.Objects; @@ -28,7 +28,7 @@ public class BanCommands : CommandGroup private readonly MessageContext _context; private readonly IMediator _mediator; private readonly Embed _embedBase; - private readonly ICommandResponder _responder; + private readonly CommandMessageResponder _responder; /// /// Instantiates a new instance of class. @@ -41,7 +41,7 @@ public BanCommands( MessageContext context, IMediator mediator, IColourPalette colourPalette, - ICommandResponder responder + CommandMessageResponder responder ) { _context = context; diff --git a/src/Mmcc.Bot/Commands/Moderation/GeneralModerationCommands.cs b/src/Mmcc.Bot/Commands/Moderation/GeneralModerationCommands.cs index 6ca3c53..a55b409 100644 --- a/src/Mmcc.Bot/Commands/Moderation/GeneralModerationCommands.cs +++ b/src/Mmcc.Bot/Commands/Moderation/GeneralModerationCommands.cs @@ -9,7 +9,7 @@ using Mmcc.Bot.Common.Statics; using Mmcc.Bot.Database.Entities; using Mmcc.Bot.RemoraAbstractions.Conditions.Attributes; -using Mmcc.Bot.RemoraAbstractions.Services; +using Mmcc.Bot.RemoraAbstractions.Services.MessageResponders; using Remora.Commands.Attributes; using Remora.Commands.Groups; using Remora.Discord.API.Abstractions.Objects; @@ -31,7 +31,7 @@ public class GeneralModerationCommands : CommandGroup private readonly MessageContext _context; private readonly IMediator _mediator; private readonly IColourPalette _colourPalette; - private readonly ICommandResponder _responder; + private readonly CommandMessageResponder _responder; /// /// Instantiates a new instance of class. @@ -44,7 +44,7 @@ public GeneralModerationCommands( MessageContext context, IMediator mediator, IColourPalette colourPalette, - ICommandResponder responder + CommandMessageResponder responder ) { _context = context; diff --git a/src/Mmcc.Bot/Commands/Moderation/MemberApplications/MemberApplicationsCommands.cs b/src/Mmcc.Bot/Commands/Moderation/MemberApplications/MemberApplicationsCommands.cs index 21c75d9..67e5ce1 100644 --- a/src/Mmcc.Bot/Commands/Moderation/MemberApplications/MemberApplicationsCommands.cs +++ b/src/Mmcc.Bot/Commands/Moderation/MemberApplications/MemberApplicationsCommands.cs @@ -1,23 +1,28 @@ -using System; +using System; using System.Collections.Generic; using System.ComponentModel; using System.Linq; using System.Threading.Tasks; using MediatR; +using Mmcc.Bot.Common.Errors; using Mmcc.Bot.Common.Extensions.Database.Entities; using Mmcc.Bot.Common.Extensions.Remora.Discord.API.Abstractions.Rest; using Mmcc.Bot.Common.Models.Colours; using Mmcc.Bot.Common.Models.Settings; using Mmcc.Bot.Common.Statics; using Mmcc.Bot.Database.Entities; +using Mmcc.Bot.InMemoryStore.Stores; using Mmcc.Bot.RemoraAbstractions.Conditions.Attributes; -using Mmcc.Bot.RemoraAbstractions.Services; +using Mmcc.Bot.RemoraAbstractions.Services.MessageResponders; +using Mmcc.Bot.RemoraAbstractions.UI; +using Mmcc.Bot.RemoraAbstractions.UI.Extensions; using Remora.Commands.Attributes; using Remora.Commands.Groups; using Remora.Discord.API.Abstractions.Objects; using Remora.Discord.API.Abstractions.Rest; using Remora.Discord.API.Objects; using Remora.Discord.Commands.Contexts; +using Remora.Discord.Interactivity; using Remora.Rest.Core; using Remora.Results; @@ -37,7 +42,8 @@ public class MemberApplicationsCommands : CommandGroup private readonly IColourPalette _colourPalette; private readonly DiscordSettings _discordSettings; private readonly IDiscordRestGuildAPI _guildApi; - private readonly ICommandResponder _responder; + private readonly CommandMessageResponder _responder; + private readonly IMessageMemberAppContextStore _memberAppContextStore; /// /// Instantiates a new instance of . @@ -49,6 +55,7 @@ public class MemberApplicationsCommands : CommandGroup /// The Discord settings. /// The guild API. /// The command responder. + /// The app context store. public MemberApplicationsCommands( MessageContext context, IDiscordRestChannelAPI channelApi, @@ -56,7 +63,8 @@ public MemberApplicationsCommands( IColourPalette colourPalette, DiscordSettings discordSettings, IDiscordRestGuildAPI guildApi, - ICommandResponder responder + CommandMessageResponder responder, + IMessageMemberAppContextStore memberAppContextStore ) { _context = context; @@ -66,6 +74,7 @@ ICommandResponder responder _discordSettings = discordSettings; _guildApi = guildApi; _responder = responder; + _memberAppContextStore = memberAppContextStore; } [Command("info")] @@ -111,21 +120,33 @@ await _responder.Respond(new Embed /// Result of the operation. [Command("view", "v")] [Description("Views a member application by ID.")] - public async Task View(int id) => - await _mediator.Send(new GetById.Query - { - ApplicationId = id, - GuildId = _context.GuildID.Value - }) switch - { - { IsSuccess: true, Entity: { } e } => - await _responder.Respond(e.GetEmbed(_colourPalette)), + public async Task View(int id) + { + var getResult = await _mediator.Send(new GetById.Query + { + ApplicationId = id, + GuildId = _context.GuildID.Value + }); - { IsSuccess: true } => - Result.FromError(new NotFoundError($"Application with ID `{id}` could not be found.")), + if (getResult is {IsSuccess: true, Entity: { } app}) + { + _memberAppContextStore.Add(_context.MessageID.Value, app.MemberApplicationId); + + var approveButton = new ButtonComponent + ( + ButtonComponentStyle.Success, + Label: "Approve", + CustomID: CustomIDHelpers.CreateButtonID("approve-btn") + ); + var actionRows = ActionRowUtils.CreateActionRowWithComponents(approveButton).AsList(); - { IsSuccess: false } res => res - }; + return await _responder.RespondWithComponents(actionRows, new(), app.GetEmbed(_colourPalette)); + } + + return getResult is {IsSuccess: true} + ? Result.FromError(new NotFoundError($"Application with ID `{id}` could not be found.")) + : getResult; + } /// /// Views the next pending application in the queue. diff --git a/src/Mmcc.Bot/Commands/Moderation/PlayerInfo/PlayerInfoCommands.cs b/src/Mmcc.Bot/Commands/Moderation/PlayerInfo/PlayerInfoCommands.cs index 12f50ec..3bf5a6d 100644 --- a/src/Mmcc.Bot/Commands/Moderation/PlayerInfo/PlayerInfoCommands.cs +++ b/src/Mmcc.Bot/Commands/Moderation/PlayerInfo/PlayerInfoCommands.cs @@ -11,7 +11,7 @@ using Mmcc.Bot.Common.Statics; using Mmcc.Bot.Mojang; using Mmcc.Bot.RemoraAbstractions.Conditions.Attributes; -using Mmcc.Bot.RemoraAbstractions.Services; +using Mmcc.Bot.RemoraAbstractions.Services.MessageResponders; using Remora.Commands.Attributes; using Remora.Commands.Groups; using Remora.Discord.API; @@ -37,7 +37,7 @@ public class PlayerInfoCommands : CommandGroup private readonly IColourPalette _colourPalette; private readonly IMojangApiService _mojangApi; private readonly IDiscordRestGuildAPI _guildApi; - private readonly ICommandResponder _responder; + private readonly CommandMessageResponder _responder; /// /// Instantiates a new instance of . @@ -54,7 +54,7 @@ public PlayerInfoCommands( IColourPalette colourPalette, IMojangApiService mojangApi, IDiscordRestGuildAPI guildApi, - ICommandResponder responder + CommandMessageResponder responder ) { _context = context; diff --git a/src/Mmcc.Bot/Commands/Moderation/Warns/WarnCommands.cs b/src/Mmcc.Bot/Commands/Moderation/Warns/WarnCommands.cs index 3b22af6..b58fbb7 100644 --- a/src/Mmcc.Bot/Commands/Moderation/Warns/WarnCommands.cs +++ b/src/Mmcc.Bot/Commands/Moderation/Warns/WarnCommands.cs @@ -4,7 +4,7 @@ using MediatR; using Mmcc.Bot.Common.Models.Colours; using Mmcc.Bot.RemoraAbstractions.Conditions.Attributes; -using Mmcc.Bot.RemoraAbstractions.Services; +using Mmcc.Bot.RemoraAbstractions.Services.MessageResponders; using Remora.Commands.Attributes; using Remora.Commands.Groups; using Remora.Discord.API.Abstractions.Objects; @@ -26,7 +26,7 @@ public class WarnCommands : CommandGroup private readonly MessageContext _context; private readonly IMediator _mediator; private readonly Embed _embedBase; - private readonly ICommandResponder _responder; + private readonly CommandMessageResponder _responder; /// /// Instantiates a new instance of class. @@ -39,7 +39,7 @@ public WarnCommands( MessageContext context, IMediator mediator, IColourPalette colourPalette, - ICommandResponder responder + CommandMessageResponder responder ) { _context = context; diff --git a/src/Mmcc.Bot/Commands/Tags/Management/TagsManagementCommands.cs b/src/Mmcc.Bot/Commands/Tags/Management/TagsManagementCommands.cs index 3716173..99219ca 100644 --- a/src/Mmcc.Bot/Commands/Tags/Management/TagsManagementCommands.cs +++ b/src/Mmcc.Bot/Commands/Tags/Management/TagsManagementCommands.cs @@ -7,7 +7,7 @@ using Mmcc.Bot.Common.Models.Colours; using Mmcc.Bot.Common.Statics; using Mmcc.Bot.RemoraAbstractions.Conditions.Attributes; -using Mmcc.Bot.RemoraAbstractions.Services; +using Mmcc.Bot.RemoraAbstractions.Services.MessageResponders; using Remora.Commands.Attributes; using Remora.Commands.Groups; using Remora.Discord.API.Abstractions.Objects; @@ -28,7 +28,7 @@ public class TagsManagementCommands : CommandGroup private readonly MessageContext _context; private readonly IMediator _mediator; private readonly IColourPalette _colourPalette; - private readonly ICommandResponder _responder; + private readonly CommandMessageResponder _responder; /// /// Instantiates a new instance of class. @@ -41,7 +41,7 @@ public TagsManagementCommands( MessageContext context, IMediator mediator, IColourPalette colourPalette, - ICommandResponder responder + CommandMessageResponder responder ) { _context = context; diff --git a/src/Mmcc.Bot/Commands/Tags/Usage/TagsUsageCommands.cs b/src/Mmcc.Bot/Commands/Tags/Usage/TagsUsageCommands.cs index e8ed586..d37e6df 100644 --- a/src/Mmcc.Bot/Commands/Tags/Usage/TagsUsageCommands.cs +++ b/src/Mmcc.Bot/Commands/Tags/Usage/TagsUsageCommands.cs @@ -1,7 +1,7 @@ using System.ComponentModel; using System.Threading.Tasks; using MediatR; -using Mmcc.Bot.RemoraAbstractions.Services; +using Mmcc.Bot.RemoraAbstractions.Services.MessageResponders; using Remora.Commands.Attributes; using Remora.Commands.Groups; using Remora.Discord.Commands.Contexts; @@ -16,7 +16,7 @@ public class TagsUsageCommands : CommandGroup { private readonly MessageContext _context; private readonly IMediator _mediator; - private readonly ICommandResponder _responder; + private readonly CommandMessageResponder _responder; /// /// Instantiates a new instance of . @@ -27,7 +27,7 @@ public class TagsUsageCommands : CommandGroup public TagsUsageCommands( MessageContext context, IMediator mediator, - ICommandResponder responder + CommandMessageResponder responder ) { _context = context; diff --git a/src/Mmcc.Bot/Interactions/InteractionsSetup.cs b/src/Mmcc.Bot/Interactions/InteractionsSetup.cs new file mode 100644 index 0000000..9922ed4 --- /dev/null +++ b/src/Mmcc.Bot/Interactions/InteractionsSetup.cs @@ -0,0 +1,25 @@ +using Microsoft.Extensions.DependencyInjection; +using Mmcc.Bot.Interactions.Moderation.MemberApplications; +using Remora.Discord.Interactivity.Extensions; + +namespace Mmcc.Bot.Interactions; + +/// +/// Extension methods that register interactive functionality with the service collection. +/// +public static class InteractionsSetup +{ + /// + /// Registers interactive functionality with the service collection. + /// + /// The . + /// The . + public static IServiceCollection AddInteractions(this IServiceCollection services) + { + services.AddInteractivity(); + + services.AddInteractionGroup(); + + return services; + } +} \ No newline at end of file diff --git a/src/Mmcc.Bot/Interactions/Moderation/MemberApplications/MemberApplicationsInteractions.cs b/src/Mmcc.Bot/Interactions/Moderation/MemberApplications/MemberApplicationsInteractions.cs new file mode 100644 index 0000000..cc74728 --- /dev/null +++ b/src/Mmcc.Bot/Interactions/Moderation/MemberApplications/MemberApplicationsInteractions.cs @@ -0,0 +1,178 @@ +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using MediatR; +using Mmcc.Bot.Commands.Moderation.MemberApplications; +using Mmcc.Bot.Common.Errors; +using Mmcc.Bot.Common.Extensions.Remora.Discord.API.Abstractions.Rest; +using Mmcc.Bot.Common.Models.Colours; +using Mmcc.Bot.Common.Models.Settings; +using Mmcc.Bot.Common.Statics; +using Mmcc.Bot.InMemoryStore.Stores; +using Mmcc.Bot.RemoraAbstractions.Services; +using Mmcc.Bot.RemoraAbstractions.UI; +using Remora.Discord.API.Abstractions.Objects; +using Remora.Discord.API.Abstractions.Rest; +using Remora.Discord.API.Objects; +using Remora.Discord.Commands.Attributes; +using Remora.Discord.Commands.Contexts; +using Remora.Discord.Interactivity; +using Remora.Rest.Core; +using Remora.Results; + +namespace Mmcc.Bot.Interactions.Moderation.MemberApplications; + +public class MemberApplicationsInteractions : InteractionGroup +{ + private readonly InteractionContext _context; + private readonly IDiscordRestChannelAPI _channelApi; + private readonly IInteractionHelperService _interactionHelperService; + private readonly IDiscordRestGuildAPI _guildApi; + private readonly IMessageMemberAppContextStore _memberAppContextStore; + private readonly DiscordSettings _discordSettings; + private readonly IMediator _mediator; + private readonly IColourPalette _colourPalette; + private readonly IErrorProcessingService _errorProcessingService; + + public MemberApplicationsInteractions( + InteractionContext context, + IDiscordRestChannelAPI channelApi, + IInteractionHelperService interactionHelperService, + IDiscordRestGuildAPI guildApi, + IMessageMemberAppContextStore memberAppContextStore, + DiscordSettings discordSettings, + IMediator mediator, + IColourPalette colourPalette, + IErrorProcessingService errorProcessingService + ) + { + _channelApi = channelApi; + _interactionHelperService = interactionHelperService; + _guildApi = guildApi; + _memberAppContextStore = memberAppContextStore; + _discordSettings = discordSettings; + _mediator = mediator; + _colourPalette = colourPalette; + _errorProcessingService = errorProcessingService; + _context = context; + } + + [Button("approve-btn")] + [SuppressInteractionResponse(true)] + public async Task OnApproveButtonPressed() + { + var serverPrefixInput = FluentTextInputBuilder + .WithId("serverPrefix") + .HasStyle(TextInputStyle.Short) + .HasLabel("Server Prefix") + .IsRequired(true) + .Build(); + + var ignsListInput = FluentTextInputBuilder + .WithId("igns") + .HasStyle(TextInputStyle.Paragraph) + .HasLabel("IGNs List (use space to separate usernames).") + .IsRequired(true) + .Build(); + + var modal = FluentCallbackModalBuilder + .WithId("approve") + .HasTitle($"Approve member application") + .WithActionRowFromTextInputs(serverPrefixInput, ignsListInput) + .Build(); + + var res = await _interactionHelperService.RespondWithModal(modal); + return res; + } + + [Modal("approve")] + [SuppressInteractionResponse(true)] + public async Task OnApproveModal(string serverPrefix, string igns) + { + var notificationResult = await _interactionHelperService.NotifyDeferredMessageIsComing(); + if (!notificationResult.IsSuccess) + { + return notificationResult; + } + + var ignsList = igns.Split(' ').ToList(); + + var approveResult = await ApproveMemberApplication(serverPrefix, ignsList); + if (!approveResult.IsSuccess) + { + return await _interactionHelperService.SendErrorNotification(approveResult.Error); + } + + var sendSuccessEmbed = await _interactionHelperService.SendFollowup(approveResult.Entity); + return !sendSuccessEmbed.IsSuccess + ? Result.FromError(sendSuccessEmbed.Error) + : Result.FromSuccess(); + } + + private async Task> ApproveMemberApplication(string serverPrefix, List ignsList) + { + if (_context.Message is not {Value.MessageReference.Value.MessageID.HasValue: true}) + { + return Result.FromError(new PropertyMissingOrNullError( + "The message containing the button unexpectedly did not reference the original command message")); + } + + var messageReference = _context.Message.Value.MessageReference.Value.MessageID.Value; + + var memberAppId = _memberAppContextStore.GetOrDefault(messageReference.Value); + if (memberAppId is null) + { + return Result.FromError(new InteractionExpiredError( + $"Interaction has expired in the {typeof(IMessageMemberAppContextStore)} store. Please run the original command again and press the button in the new response.")); + } + + var getMembersChannelResult = await _guildApi.FindGuildChannelByName(_context.GuildID.Value, + _discordSettings.ChannelNames.MemberApps); + if (!getMembersChannelResult.IsSuccess) + { + return Result.FromError(getMembersChannelResult); + } + + var commandResult = await _mediator.Send(new ApproveAutomatically.Command + { + Id = memberAppId.Value, + GuildId = _context.GuildID.Value, + ChannelId = _context.ChannelID, + ServerPrefix = serverPrefix, + Igns = ignsList + }); + if (!commandResult.IsSuccess) + { + return Result.FromError(commandResult); + } + + var userNotificationEmbed = new Embed + { + Title = ":white_check_mark: Application approved.", + Description = "Your application has been approved.", + Thumbnail = EmbedProperties.MmccLogoThumbnail, + Colour = _colourPalette.Green, + Fields = new List + { + new("Approved by", $"<@{_context.User.ID}>", false) + } + }; + var sendUserNotificationEmbedResult = await _channelApi.CreateMessageAsync( + channelID: getMembersChannelResult.Entity.ID, + embeds: new[] { userNotificationEmbed }, + messageReference: new MessageReference(new Snowflake(commandResult.Entity.MessageId))); + + if (!sendUserNotificationEmbedResult.IsSuccess) + { + return Result.FromError(sendUserNotificationEmbedResult); + } + + return new Embed + { + Title = ":white_check_mark: Approved the application successfully", + Description = $"Application with ID `{memberAppId.Value}` has been :white_check_mark: *approved*.", + Thumbnail = EmbedProperties.MmccLogoThumbnail, + Colour = _colourPalette.Green + }; + } +} \ No newline at end of file diff --git a/src/Mmcc.Bot/Middleware/ErrorNotificationMiddleware.cs b/src/Mmcc.Bot/Middleware/ErrorNotificationMiddleware.cs index 0a90b1e..75c523f 100644 --- a/src/Mmcc.Bot/Middleware/ErrorNotificationMiddleware.cs +++ b/src/Mmcc.Bot/Middleware/ErrorNotificationMiddleware.cs @@ -1,16 +1,8 @@ -using System; -using System.Collections.Generic; -using System.Threading; +using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.Logging; -using Mmcc.Bot.Common.Errors; -using Mmcc.Bot.Common.Extensions.FluentValidation.Results; -using Mmcc.Bot.Common.Models.Colours; -using Mmcc.Bot.Common.Models.Settings; -using Mmcc.Bot.Common.Statics; -using Remora.Commands.Results; +using Mmcc.Bot.RemoraAbstractions.Services; using Remora.Discord.API.Abstractions.Rest; -using Remora.Discord.API.Objects; using Remora.Discord.Commands.Contexts; using Remora.Discord.Commands.Services; using Remora.Results; @@ -21,20 +13,17 @@ public class ErrorNotificationMiddleware : IPostExecutionEvent { private readonly ILogger _logger; private readonly IDiscordRestChannelAPI _channelApi; - private readonly IColourPalette _colourPalette; - private readonly DiscordSettings _discordSettings; - + private readonly IErrorProcessingService _errorProcessingService; + public ErrorNotificationMiddleware( ILogger logger, - IDiscordRestChannelAPI channelApi, - IColourPalette colourPalette, - DiscordSettings discordSettings + IDiscordRestChannelAPI channelApi, + IErrorProcessingService errorProcessingService ) { _logger = logger; _channelApi = channelApi; - _colourPalette = colourPalette; - _discordSettings = discordSettings; + _errorProcessingService = errorProcessingService; } public async Task AfterExecutionAsync( @@ -49,41 +38,7 @@ CancellationToken ct } var err = executionResult.Error; - var errorEmbed = new Embed - { - Thumbnail = EmbedProperties.MmccLogoThumbnail, - Colour = _colourPalette.Red, - Timestamp = DateTimeOffset.UtcNow - }; - errorEmbed = err switch - { - CommandNotFoundError cnfe => errorEmbed with - { - Title = ":exclamation: Command not found", - Description = $"Could not find a matching command for `{_discordSettings.Prefix}{cnfe.OriginalInput}`." - }, - ValidationError(var message, var readOnlyList, _) => errorEmbed with - { - Title = ":exclamation: Validation error.", - Description = message.Replace('\'', '`'), - Fields = new List {readOnlyList.ToEmbedField()} - }, - NotFoundError => errorEmbed with - { - Title = ":x: Resource not found.", - Description = err.Message - }, - null => errorEmbed with - { - Title = ":exclamation: Error.", - Description = "Unknown error." - }, - _ => errorEmbed with - { - Title = $":x: {err.GetType()}.", - Description = err.Message - } - }; + var errorEmbed = _errorProcessingService.GetErrorEmbed(err); var sendEmbedResult = await _channelApi.CreateMessageAsync(context.ChannelID, embeds: new[] { errorEmbed }, ct: ct); diff --git a/src/Mmcc.Bot/Mmcc.Bot.csproj b/src/Mmcc.Bot/Mmcc.Bot.csproj index 3a64747..818d7f1 100644 --- a/src/Mmcc.Bot/Mmcc.Bot.csproj +++ b/src/Mmcc.Bot/Mmcc.Bot.csproj @@ -28,12 +28,16 @@ - + + + + + diff --git a/src/Mmcc.Bot/Program.cs b/src/Mmcc.Bot/Program.cs index 2d29d70..347c6c1 100644 --- a/src/Mmcc.Bot/Program.cs +++ b/src/Mmcc.Bot/Program.cs @@ -6,7 +6,6 @@ using Microsoft.Extensions.Logging; using Mmcc.Bot; using Mmcc.Bot.Behaviours; -using Mmcc.Bot.Caching; using Mmcc.Bot.Commands; using Mmcc.Bot.Common.Extensions.Hosting; using Mmcc.Bot.Common.Models.Colours; @@ -18,6 +17,8 @@ using Mmcc.Bot.EventResponders.Moderation.MemberApplications; using Mmcc.Bot.Hosting; using Mmcc.Bot.Hosting.Moderation; +using Mmcc.Bot.InMemoryStore.Stores; +using Mmcc.Bot.Interactions; using Mmcc.Bot.Middleware; using Mmcc.Bot.Mojang; using Mmcc.Bot.Polychat; @@ -42,6 +43,7 @@ services.ConfigureBot(hostContext); // db; + services.AddInMemoryStores(); services.AddBotDatabaseContext(); services.AddSingleton(); @@ -58,7 +60,6 @@ services.AddTransient(typeof(IPipelineBehavior<,>), typeof(ValidationBehaviour<,>)); // Mmcc.Bot.X projects; - services.AddMmccCaching(); services.AddMojangApi(); services.AddPolychat(hostContext.Configuration.GetSection("Ssmp")); @@ -66,6 +67,7 @@ services.AddRemoraAbstractions(); services.AddBotMiddlewares(); services.AddBotCommands(); + services.AddInteractions(); services.AddBotGatewayEventResponders(); services.AddDiscordCaching(); services.AddBotBackgroundServices(); From 67fb479e38346b622bca40c90b167111face612f Mon Sep 17 00:00:00 2001 From: TraceLD Date: Sat, 22 Oct 2022 13:17:29 +0100 Subject: [PATCH 05/30] commit this work before uberest v2 --- .../Attributes/RequireGuildAttribute.cs | 10 - .../RequireUserGuildPermissionAttribute.cs | 24 -- .../RequireGuildCondition.cs | 18 +- .../RequireUserGuildPermissionCondition.cs | 28 +- .../InteractionRequireGuildCondition.cs | 27 ++ .../InteractionRequireUserGuildPermission.cs | 45 +++ .../Mmcc.Bot.RemoraAbstractions.csproj | 4 + .../Services/InteractionHelperService.cs | 11 - .../InteractionExecutionEventsRunner.cs | 55 ++++ .../Diagnostics/DiagnosticsCommands.cs | 3 +- src/Mmcc.Bot/Commands/Guilds/GuildCommands.cs | 3 +- .../Minecraft/MinecraftServersCommands.cs | 3 +- .../Restarts/MinecraftAutoRestartsCommands.cs | 3 +- .../Commands/Moderation/Bans/BanCommands.cs | 3 +- .../Moderation/GeneralModerationCommands.cs | 3 +- .../MemberApplicationsCommands.cs | 3 +- .../PlayerInfo/PlayerInfoCommands.cs | 3 +- .../Commands/Moderation/Warns/WarnCommands.cs | 3 +- .../Tags/Management/TagsManagementCommands.cs | 3 +- .../EventResponders/EventRespondersSetup.cs | 6 + .../Interactions/InteractivityResponder.cs | 298 ++++++++++++++++++ .../Interactions/InteractionsSetup.cs | 13 +- .../MemberApplicationsInteractions.cs | 19 +- .../InteractionErrorNotificationMiddleware.cs | 48 +++ src/Mmcc.Bot/Middleware/MiddlewareSetup.cs | 2 + src/Mmcc.Bot/Mmcc.Bot.csproj | 4 - 26 files changed, 561 insertions(+), 81 deletions(-) delete mode 100644 src/Mmcc.Bot.RemoraAbstractions/Conditions/Attributes/RequireGuildAttribute.cs delete mode 100644 src/Mmcc.Bot.RemoraAbstractions/Conditions/Attributes/RequireUserGuildPermissionAttribute.cs rename src/Mmcc.Bot.RemoraAbstractions/Conditions/{ => CommandSpecific}/RequireGuildCondition.cs (79%) rename src/Mmcc.Bot.RemoraAbstractions/Conditions/{ => CommandSpecific}/RequireUserGuildPermissionCondition.cs (59%) create mode 100644 src/Mmcc.Bot.RemoraAbstractions/Conditions/InteractionSpecific/InteractionRequireGuildCondition.cs create mode 100644 src/Mmcc.Bot.RemoraAbstractions/Conditions/InteractionSpecific/InteractionRequireUserGuildPermission.cs create mode 100644 src/Mmcc.Bot.RemoraAbstractions/Services/Interactions/InteractionExecutionEventsRunner.cs create mode 100644 src/Mmcc.Bot/EventResponders/Interactions/InteractivityResponder.cs create mode 100644 src/Mmcc.Bot/Middleware/InteractionErrorNotificationMiddleware.cs diff --git a/src/Mmcc.Bot.RemoraAbstractions/Conditions/Attributes/RequireGuildAttribute.cs b/src/Mmcc.Bot.RemoraAbstractions/Conditions/Attributes/RequireGuildAttribute.cs deleted file mode 100644 index 6dd643e..0000000 --- a/src/Mmcc.Bot.RemoraAbstractions/Conditions/Attributes/RequireGuildAttribute.cs +++ /dev/null @@ -1,10 +0,0 @@ -using Remora.Commands.Conditions; - -namespace Mmcc.Bot.RemoraAbstractions.Conditions.Attributes; - -/// -/// Marks a command as requiring to be executed within a guild. -/// -public class RequireGuildAttribute : ConditionAttribute -{ -} \ No newline at end of file diff --git a/src/Mmcc.Bot.RemoraAbstractions/Conditions/Attributes/RequireUserGuildPermissionAttribute.cs b/src/Mmcc.Bot.RemoraAbstractions/Conditions/Attributes/RequireUserGuildPermissionAttribute.cs deleted file mode 100644 index 6ae8828..0000000 --- a/src/Mmcc.Bot.RemoraAbstractions/Conditions/Attributes/RequireUserGuildPermissionAttribute.cs +++ /dev/null @@ -1,24 +0,0 @@ -using Remora.Commands.Conditions; -using Remora.Discord.API.Abstractions.Objects; - -namespace Mmcc.Bot.RemoraAbstractions.Conditions.Attributes; - -/// -/// Marks a command as requiring the requesting user to have a particular permission within the guild. -/// -public class RequireUserGuildPermissionAttribute : ConditionAttribute -{ - /// - /// Gets the permission. - /// - public DiscordPermission Permission { get; } - - /// - /// Initializes a new instance of the class. - /// - /// The permission. - public RequireUserGuildPermissionAttribute(DiscordPermission permission) - { - Permission = permission; - } -} \ No newline at end of file diff --git a/src/Mmcc.Bot.RemoraAbstractions/Conditions/RequireGuildCondition.cs b/src/Mmcc.Bot.RemoraAbstractions/Conditions/CommandSpecific/RequireGuildCondition.cs similarity index 79% rename from src/Mmcc.Bot.RemoraAbstractions/Conditions/RequireGuildCondition.cs rename to src/Mmcc.Bot.RemoraAbstractions/Conditions/CommandSpecific/RequireGuildCondition.cs index 36b073a..32d317d 100644 --- a/src/Mmcc.Bot.RemoraAbstractions/Conditions/RequireGuildCondition.cs +++ b/src/Mmcc.Bot.RemoraAbstractions/Conditions/CommandSpecific/RequireGuildCondition.cs @@ -1,11 +1,17 @@ using System.Threading; using System.Threading.Tasks; -using Mmcc.Bot.RemoraAbstractions.Conditions.Attributes; using Remora.Commands.Conditions; using Remora.Discord.Commands.Contexts; using Remora.Results; -namespace Mmcc.Bot.RemoraAbstractions.Conditions; +namespace Mmcc.Bot.RemoraAbstractions.Conditions.CommandSpecific; + +/// +/// Marks a command as requiring to be executed within a guild. +/// +public class RequireGuildAttribute : ConditionAttribute +{ +} /// /// Checks if the command was executed within a guild before allowing execution. @@ -13,16 +19,14 @@ namespace Mmcc.Bot.RemoraAbstractions.Conditions; public class RequireGuildCondition : ICondition { private readonly MessageContext _context; - + /// /// Instantiates a new instance of the class. /// /// The message context. public RequireGuildCondition(MessageContext context) - { - _context = context; - } - + => _context = context; + /// public ValueTask CheckAsync(RequireGuildAttribute attribute, CancellationToken ct) { diff --git a/src/Mmcc.Bot.RemoraAbstractions/Conditions/RequireUserGuildPermissionCondition.cs b/src/Mmcc.Bot.RemoraAbstractions/Conditions/CommandSpecific/RequireUserGuildPermissionCondition.cs similarity index 59% rename from src/Mmcc.Bot.RemoraAbstractions/Conditions/RequireUserGuildPermissionCondition.cs rename to src/Mmcc.Bot.RemoraAbstractions/Conditions/CommandSpecific/RequireUserGuildPermissionCondition.cs index bf6cff4..cdc5858 100644 --- a/src/Mmcc.Bot.RemoraAbstractions/Conditions/RequireUserGuildPermissionCondition.cs +++ b/src/Mmcc.Bot.RemoraAbstractions/Conditions/CommandSpecific/RequireUserGuildPermissionCondition.cs @@ -1,12 +1,32 @@ using System.Threading; using System.Threading.Tasks; -using Mmcc.Bot.RemoraAbstractions.Conditions.Attributes; using Mmcc.Bot.RemoraAbstractions.Services; using Remora.Commands.Conditions; +using Remora.Discord.API.Abstractions.Objects; using Remora.Discord.Commands.Contexts; using Remora.Results; -namespace Mmcc.Bot.RemoraAbstractions.Conditions; +namespace Mmcc.Bot.RemoraAbstractions.Conditions.CommandSpecific; + +/// +/// Marks a command as requiring the requesting user to have a particular permission within the guild. +/// +public class RequireUserGuildPermissionAttribute : ConditionAttribute +{ + /// + /// Gets the permission. + /// + public DiscordPermission Permission { get; } + + /// + /// Initializes a new instance of the class. + /// + /// The permission. + public RequireUserGuildPermissionAttribute(DiscordPermission permission) + { + Permission = permission; + } +} /// /// Checks required Guild permissions before allowing execution. @@ -30,8 +50,8 @@ public RequireUserGuildPermissionCondition(MessageContext context, IDiscordPermi } /// - public async ValueTask CheckAsync(RequireUserGuildPermissionAttribute attribute, CancellationToken ct) => - await _permissionsService.CheckHasRequiredPermission( + public async ValueTask CheckAsync(RequireUserGuildPermissionAttribute attribute, CancellationToken ct) + => await _permissionsService.CheckHasRequiredPermission( attribute.Permission, _context.ChannelID, _context.User, ct diff --git a/src/Mmcc.Bot.RemoraAbstractions/Conditions/InteractionSpecific/InteractionRequireGuildCondition.cs b/src/Mmcc.Bot.RemoraAbstractions/Conditions/InteractionSpecific/InteractionRequireGuildCondition.cs new file mode 100644 index 0000000..d98993d --- /dev/null +++ b/src/Mmcc.Bot.RemoraAbstractions/Conditions/InteractionSpecific/InteractionRequireGuildCondition.cs @@ -0,0 +1,27 @@ +using System.Threading; +using System.Threading.Tasks; +using Remora.Commands.Conditions; +using Remora.Discord.Commands.Contexts; +using Remora.Results; + +namespace Mmcc.Bot.RemoraAbstractions.Conditions.InteractionSpecific; + +public class InteractionRequireGuildAttribute : ConditionAttribute +{ +} + +public class InteractionRequireGuildCondition : ICondition +{ + private readonly InteractionContext _context; + + public InteractionRequireGuildCondition(InteractionContext context) + => _context = context; + + public ValueTask CheckAsync(InteractionRequireGuildAttribute attribute, CancellationToken ct = new CancellationToken()) + { + var guild = _context.GuildID; + return new(!guild.HasValue + ? new InvalidOperationError("Command that requires to be executed within a guild was executed outside of one") + : Result.FromSuccess()); + } +} \ No newline at end of file diff --git a/src/Mmcc.Bot.RemoraAbstractions/Conditions/InteractionSpecific/InteractionRequireUserGuildPermission.cs b/src/Mmcc.Bot.RemoraAbstractions/Conditions/InteractionSpecific/InteractionRequireUserGuildPermission.cs new file mode 100644 index 0000000..3947bb4 --- /dev/null +++ b/src/Mmcc.Bot.RemoraAbstractions/Conditions/InteractionSpecific/InteractionRequireUserGuildPermission.cs @@ -0,0 +1,45 @@ +using System.Threading; +using System.Threading.Tasks; +using Mmcc.Bot.RemoraAbstractions.Conditions.CommandSpecific; +using Mmcc.Bot.RemoraAbstractions.Services; +using Remora.Commands.Conditions; +using Remora.Discord.API.Abstractions.Objects; +using Remora.Discord.Commands.Contexts; +using Remora.Results; + +namespace Mmcc.Bot.RemoraAbstractions.Conditions.InteractionSpecific; + +public class InteractionRequireUserGuildPermissionAttribute : ConditionAttribute +{ + /// + /// Gets the permission. + /// + public DiscordPermission Permission { get; } + + /// + /// Initializes a new instance of the class. + /// + /// The permission. + public InteractionRequireUserGuildPermissionAttribute(DiscordPermission permission) + => Permission = permission; +} + +public class InteractionRequireUserGuildPermissionCondition : ICondition +{ + private readonly InteractionContext _context; + private readonly IDiscordPermissionsService _permissionsService; + + + public InteractionRequireUserGuildPermissionCondition(InteractionContext context, IDiscordPermissionsService permissionsService) + { + _context = context; + _permissionsService = permissionsService; + } + + public async ValueTask CheckAsync(InteractionRequireUserGuildPermissionAttribute attribute, CancellationToken ct) + => await _permissionsService.CheckHasRequiredPermission( + attribute.Permission, + _context.ChannelID, + _context.User, ct + ); +} \ No newline at end of file diff --git a/src/Mmcc.Bot.RemoraAbstractions/Mmcc.Bot.RemoraAbstractions.csproj b/src/Mmcc.Bot.RemoraAbstractions/Mmcc.Bot.RemoraAbstractions.csproj index bcee1b1..693b413 100644 --- a/src/Mmcc.Bot.RemoraAbstractions/Mmcc.Bot.RemoraAbstractions.csproj +++ b/src/Mmcc.Bot.RemoraAbstractions/Mmcc.Bot.RemoraAbstractions.csproj @@ -13,4 +13,8 @@ + + + + diff --git a/src/Mmcc.Bot.RemoraAbstractions/Services/InteractionHelperService.cs b/src/Mmcc.Bot.RemoraAbstractions/Services/InteractionHelperService.cs index ff754a0..a734103 100644 --- a/src/Mmcc.Bot.RemoraAbstractions/Services/InteractionHelperService.cs +++ b/src/Mmcc.Bot.RemoraAbstractions/Services/InteractionHelperService.cs @@ -16,7 +16,6 @@ public interface IInteractionHelperService Task RespondWithModal(InteractionModalCallbackData modalCallbackData); Task SendFollowup(params Embed[] embeds); Task SendFollowup(string msg); - Task SendErrorNotification(IResultError err); } public class InteractionHelperService : IInteractionHelperService @@ -70,14 +69,4 @@ public async Task SendFollowup(string msg) ? Result.FromSuccess() : Result.FromError(res); } - - public async Task SendErrorNotification(IResultError err) - { - var errorEmbed = _errorProcessingService.GetErrorEmbed(err); - - var sendErrorEmbed = await SendFollowup(errorEmbed); - return !sendErrorEmbed.IsSuccess - ? Result.FromError(sendErrorEmbed.Error) - : Result.FromSuccess(); - } } \ No newline at end of file diff --git a/src/Mmcc.Bot.RemoraAbstractions/Services/Interactions/InteractionExecutionEventsRunner.cs b/src/Mmcc.Bot.RemoraAbstractions/Services/Interactions/InteractionExecutionEventsRunner.cs new file mode 100644 index 0000000..9a96ddd --- /dev/null +++ b/src/Mmcc.Bot.RemoraAbstractions/Services/Interactions/InteractionExecutionEventsRunner.cs @@ -0,0 +1,55 @@ +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Remora.Discord.Commands.Contexts; +using Remora.Results; + +namespace Mmcc.Bot.RemoraAbstractions.Services.Interactions; + +public interface IInteractionExecutionEventsRunner +{ + Task RunPostExecutionEvents( + InteractionContext interactionContext, + Result interactionResult, + CancellationToken ct + ); +} + +public interface IInteractionPostExecutionEvent +{ + Task AfterExecutionAsync( + InteractionContext interactionContext, + Result interactionResult, + CancellationToken ct = default + ); +} + +public class InteractionExecutionEventsRunner : IInteractionExecutionEventsRunner +{ + private readonly IEnumerable _events; + + public InteractionExecutionEventsRunner(IEnumerable events) + => _events = events; + + public async Task RunPostExecutionEvents( + InteractionContext interactionContext, + Result interactionResult, + CancellationToken ct + ) + { + var results = await Task.WhenAll( + _events.Select(x => x.AfterExecutionAsync(interactionContext, interactionResult, ct)) + ); + + foreach (var result in results) + { + if (!result.IsSuccess) + { + return result; + } + } + + return Result.FromSuccess(); + } +} \ No newline at end of file diff --git a/src/Mmcc.Bot/Commands/Diagnostics/DiagnosticsCommands.cs b/src/Mmcc.Bot/Commands/Diagnostics/DiagnosticsCommands.cs index 5196cb1..e4ed3e8 100644 --- a/src/Mmcc.Bot/Commands/Diagnostics/DiagnosticsCommands.cs +++ b/src/Mmcc.Bot/Commands/Diagnostics/DiagnosticsCommands.cs @@ -7,7 +7,8 @@ using MediatR; using Mmcc.Bot.Common.Models.Colours; using Mmcc.Bot.Common.Statics; -using Mmcc.Bot.RemoraAbstractions.Conditions.Attributes; +using Mmcc.Bot.RemoraAbstractions.Conditions; +using Mmcc.Bot.RemoraAbstractions.Conditions.CommandSpecific; using Mmcc.Bot.RemoraAbstractions.Services.MessageResponders; using Remora.Commands.Attributes; using Remora.Commands.Groups; diff --git a/src/Mmcc.Bot/Commands/Guilds/GuildCommands.cs b/src/Mmcc.Bot/Commands/Guilds/GuildCommands.cs index 3d62509..0a7fffe 100644 --- a/src/Mmcc.Bot/Commands/Guilds/GuildCommands.cs +++ b/src/Mmcc.Bot/Commands/Guilds/GuildCommands.cs @@ -5,7 +5,8 @@ using System.Threading.Tasks; using MediatR; using Mmcc.Bot.Common.Models.Colours; -using Mmcc.Bot.RemoraAbstractions.Conditions.Attributes; +using Mmcc.Bot.RemoraAbstractions.Conditions; +using Mmcc.Bot.RemoraAbstractions.Conditions.CommandSpecific; using Mmcc.Bot.RemoraAbstractions.Services.MessageResponders; using Remora.Commands.Attributes; using Remora.Commands.Groups; diff --git a/src/Mmcc.Bot/Commands/Minecraft/MinecraftServersCommands.cs b/src/Mmcc.Bot/Commands/Minecraft/MinecraftServersCommands.cs index 6d60bd3..9a007a4 100644 --- a/src/Mmcc.Bot/Commands/Minecraft/MinecraftServersCommands.cs +++ b/src/Mmcc.Bot/Commands/Minecraft/MinecraftServersCommands.cs @@ -9,7 +9,8 @@ using Mmcc.Bot.Common.Statics; using Mmcc.Bot.Polychat.MessageSenders; using Mmcc.Bot.Polychat.Services; -using Mmcc.Bot.RemoraAbstractions.Conditions.Attributes; +using Mmcc.Bot.RemoraAbstractions.Conditions; +using Mmcc.Bot.RemoraAbstractions.Conditions.CommandSpecific; using Mmcc.Bot.RemoraAbstractions.Services.MessageResponders; using Remora.Commands.Attributes; using Remora.Commands.Groups; diff --git a/src/Mmcc.Bot/Commands/Minecraft/Restarts/MinecraftAutoRestartsCommands.cs b/src/Mmcc.Bot/Commands/Minecraft/Restarts/MinecraftAutoRestartsCommands.cs index 8900661..05f4c1c 100644 --- a/src/Mmcc.Bot/Commands/Minecraft/Restarts/MinecraftAutoRestartsCommands.cs +++ b/src/Mmcc.Bot/Commands/Minecraft/Restarts/MinecraftAutoRestartsCommands.cs @@ -6,7 +6,8 @@ using Mmcc.Bot.Common.Models.Colours; using Mmcc.Bot.Common.Statics; using Mmcc.Bot.Polychat.Jobs.Recurring.Restarts; -using Mmcc.Bot.RemoraAbstractions.Conditions.Attributes; +using Mmcc.Bot.RemoraAbstractions.Conditions; +using Mmcc.Bot.RemoraAbstractions.Conditions.CommandSpecific; using Mmcc.Bot.RemoraAbstractions.Services.MessageResponders; using Mmcc.Bot.RemoraAbstractions.Timestamps; using Remora.Commands.Attributes; diff --git a/src/Mmcc.Bot/Commands/Moderation/Bans/BanCommands.cs b/src/Mmcc.Bot/Commands/Moderation/Bans/BanCommands.cs index f541c0c..b2cb8aa 100644 --- a/src/Mmcc.Bot/Commands/Moderation/Bans/BanCommands.cs +++ b/src/Mmcc.Bot/Commands/Moderation/Bans/BanCommands.cs @@ -4,7 +4,8 @@ using MediatR; using Mmcc.Bot.Common.Models; using Mmcc.Bot.Common.Models.Colours; -using Mmcc.Bot.RemoraAbstractions.Conditions.Attributes; +using Mmcc.Bot.RemoraAbstractions.Conditions; +using Mmcc.Bot.RemoraAbstractions.Conditions.CommandSpecific; using Mmcc.Bot.RemoraAbstractions.Services.MessageResponders; using Remora.Commands.Attributes; using Remora.Commands.Groups; diff --git a/src/Mmcc.Bot/Commands/Moderation/GeneralModerationCommands.cs b/src/Mmcc.Bot/Commands/Moderation/GeneralModerationCommands.cs index a55b409..f76c5ad 100644 --- a/src/Mmcc.Bot/Commands/Moderation/GeneralModerationCommands.cs +++ b/src/Mmcc.Bot/Commands/Moderation/GeneralModerationCommands.cs @@ -8,7 +8,8 @@ using Mmcc.Bot.Common.Models.Colours; using Mmcc.Bot.Common.Statics; using Mmcc.Bot.Database.Entities; -using Mmcc.Bot.RemoraAbstractions.Conditions.Attributes; +using Mmcc.Bot.RemoraAbstractions.Conditions; +using Mmcc.Bot.RemoraAbstractions.Conditions.CommandSpecific; using Mmcc.Bot.RemoraAbstractions.Services.MessageResponders; using Remora.Commands.Attributes; using Remora.Commands.Groups; diff --git a/src/Mmcc.Bot/Commands/Moderation/MemberApplications/MemberApplicationsCommands.cs b/src/Mmcc.Bot/Commands/Moderation/MemberApplications/MemberApplicationsCommands.cs index 67e5ce1..8217e16 100644 --- a/src/Mmcc.Bot/Commands/Moderation/MemberApplications/MemberApplicationsCommands.cs +++ b/src/Mmcc.Bot/Commands/Moderation/MemberApplications/MemberApplicationsCommands.cs @@ -12,7 +12,8 @@ using Mmcc.Bot.Common.Statics; using Mmcc.Bot.Database.Entities; using Mmcc.Bot.InMemoryStore.Stores; -using Mmcc.Bot.RemoraAbstractions.Conditions.Attributes; +using Mmcc.Bot.RemoraAbstractions.Conditions; +using Mmcc.Bot.RemoraAbstractions.Conditions.CommandSpecific; using Mmcc.Bot.RemoraAbstractions.Services.MessageResponders; using Mmcc.Bot.RemoraAbstractions.UI; using Mmcc.Bot.RemoraAbstractions.UI.Extensions; diff --git a/src/Mmcc.Bot/Commands/Moderation/PlayerInfo/PlayerInfoCommands.cs b/src/Mmcc.Bot/Commands/Moderation/PlayerInfo/PlayerInfoCommands.cs index 3bf5a6d..a31e976 100644 --- a/src/Mmcc.Bot/Commands/Moderation/PlayerInfo/PlayerInfoCommands.cs +++ b/src/Mmcc.Bot/Commands/Moderation/PlayerInfo/PlayerInfoCommands.cs @@ -10,7 +10,8 @@ using Mmcc.Bot.Common.Models.Colours; using Mmcc.Bot.Common.Statics; using Mmcc.Bot.Mojang; -using Mmcc.Bot.RemoraAbstractions.Conditions.Attributes; +using Mmcc.Bot.RemoraAbstractions.Conditions; +using Mmcc.Bot.RemoraAbstractions.Conditions.CommandSpecific; using Mmcc.Bot.RemoraAbstractions.Services.MessageResponders; using Remora.Commands.Attributes; using Remora.Commands.Groups; diff --git a/src/Mmcc.Bot/Commands/Moderation/Warns/WarnCommands.cs b/src/Mmcc.Bot/Commands/Moderation/Warns/WarnCommands.cs index b58fbb7..930b744 100644 --- a/src/Mmcc.Bot/Commands/Moderation/Warns/WarnCommands.cs +++ b/src/Mmcc.Bot/Commands/Moderation/Warns/WarnCommands.cs @@ -3,7 +3,8 @@ using System.Threading.Tasks; using MediatR; using Mmcc.Bot.Common.Models.Colours; -using Mmcc.Bot.RemoraAbstractions.Conditions.Attributes; +using Mmcc.Bot.RemoraAbstractions.Conditions; +using Mmcc.Bot.RemoraAbstractions.Conditions.CommandSpecific; using Mmcc.Bot.RemoraAbstractions.Services.MessageResponders; using Remora.Commands.Attributes; using Remora.Commands.Groups; diff --git a/src/Mmcc.Bot/Commands/Tags/Management/TagsManagementCommands.cs b/src/Mmcc.Bot/Commands/Tags/Management/TagsManagementCommands.cs index 99219ca..6fb3ddc 100644 --- a/src/Mmcc.Bot/Commands/Tags/Management/TagsManagementCommands.cs +++ b/src/Mmcc.Bot/Commands/Tags/Management/TagsManagementCommands.cs @@ -6,7 +6,8 @@ using MediatR; using Mmcc.Bot.Common.Models.Colours; using Mmcc.Bot.Common.Statics; -using Mmcc.Bot.RemoraAbstractions.Conditions.Attributes; +using Mmcc.Bot.RemoraAbstractions.Conditions; +using Mmcc.Bot.RemoraAbstractions.Conditions.CommandSpecific; using Mmcc.Bot.RemoraAbstractions.Services.MessageResponders; using Remora.Commands.Attributes; using Remora.Commands.Groups; diff --git a/src/Mmcc.Bot/EventResponders/EventRespondersSetup.cs b/src/Mmcc.Bot/EventResponders/EventRespondersSetup.cs index b7749b5..8ddeb64 100644 --- a/src/Mmcc.Bot/EventResponders/EventRespondersSetup.cs +++ b/src/Mmcc.Bot/EventResponders/EventRespondersSetup.cs @@ -1,9 +1,12 @@ using Microsoft.Extensions.DependencyInjection; using Mmcc.Bot.EventResponders.Feedback; using Mmcc.Bot.EventResponders.Guilds; +using Mmcc.Bot.EventResponders.Interactions; using Mmcc.Bot.EventResponders.Moderation.MemberApplications; using Mmcc.Bot.EventResponders.Users; using Remora.Discord.Gateway.Extensions; +using Remora.Discord.Interactivity; +using Remora.Extensions.Options.Immutable; namespace Mmcc.Bot.EventResponders; @@ -26,6 +29,9 @@ public static IServiceCollection AddBotGatewayEventResponders(this IServiceColle services.AddResponder(); services.AddResponder(); services.AddResponder(); + + services.AddResponder(); + services.Configure(() => new InteractivityResponderOptions()); return services; } diff --git a/src/Mmcc.Bot/EventResponders/Interactions/InteractivityResponder.cs b/src/Mmcc.Bot/EventResponders/Interactions/InteractivityResponder.cs new file mode 100644 index 0000000..dca025a --- /dev/null +++ b/src/Mmcc.Bot/EventResponders/Interactions/InteractivityResponder.cs @@ -0,0 +1,298 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Humanizer; +using Microsoft.Extensions.Options; +using Mmcc.Bot.RemoraAbstractions.Services.Interactions; +using Remora.Commands.Services; +using Remora.Commands.Tokenization; +using Remora.Commands.Trees; +using Remora.Discord.API.Abstractions.Gateway.Events; +using Remora.Discord.API.Abstractions.Objects; +using Remora.Discord.API.Abstractions.Rest; +using Remora.Discord.API.Objects; +using Remora.Discord.Commands.Attributes; +using Remora.Discord.Commands.Contexts; +using Remora.Discord.Commands.Extensions; +using Remora.Discord.Commands.Services; +using Remora.Discord.Gateway.Responders; +using Remora.Discord.Interactivity; +using Remora.Results; + +namespace Mmcc.Bot.EventResponders.Interactions; + +public sealed class InteractivityResponder : IResponder +{ + private readonly ContextInjectionService _contextInjectionService; + private readonly IDiscordRestInteractionAPI _interactionApi; + private readonly IServiceProvider _services; + private readonly InteractivityResponderOptions _options; + private readonly CommandService _commandService; + + private readonly TokenizerOptions _tokenizerOptions; + private readonly TreeSearchOptions _treeSearchOptions; + + private readonly IInteractionExecutionEventsRunner _eventsRunner; + + /// + /// Initializes a new instance of the class. + /// + /// The command service. + /// The responder options. + /// The interaction API. + /// The available services. + /// The context injection service. + /// The tokenizer options. + /// The tree search options. + /// The interaction execution events runner. + public InteractivityResponder + ( + CommandService commandService, + IOptions options, + IDiscordRestInteractionAPI interactionApi, + IServiceProvider services, + ContextInjectionService contextInjectionService, + IOptions tokenizerOptions, + IOptions treeSearchOptions, + IInteractionExecutionEventsRunner eventsRunner + ) + { + _services = services; + _contextInjectionService = contextInjectionService; + _eventsRunner = eventsRunner; + _interactionApi = interactionApi; + _commandService = commandService; + _options = options.Value; + + _tokenizerOptions = tokenizerOptions.Value; + _treeSearchOptions = treeSearchOptions.Value; + } + + /// + public async Task RespondAsync(IInteractionCreate gatewayEvent, CancellationToken ct = default) + { + if (gatewayEvent.Type is not (InteractionType.MessageComponent or InteractionType.ModalSubmit)) + { + return Result.FromSuccess(); + } + + if (!gatewayEvent.Data.IsDefined(out var data)) + { + return new InvalidOperationError("Component or modal interaction without data received. Bug?"); + } + + var createContext = gatewayEvent.CreateContext(); + if (!createContext.IsSuccess) + { + return (Result)createContext; + } + + var context = createContext.Entity; + _contextInjectionService.Context = context; + + return data.TryPickT1(out var componentData, out var remainder) + ? await HandleComponentInteractionAsync(context, componentData, ct) + : remainder.TryPickT1(out var modalSubmitData, out _) + ? await HandleModalInteractionAsync(context, modalSubmitData, ct) + : Result.FromSuccess(); + } + + private async Task HandleComponentInteractionAsync + ( + InteractionContext context, + IMessageComponentData data, + CancellationToken ct = default + ) + { + if (!data.CustomID.StartsWith(Constants.InteractionTree)) + { + // Not a component we handle + return Result.FromSuccess(); + } + + if (data.ComponentType is ComponentType.SelectMenu) + { + if (!data.Values.HasValue) + { + return new InvalidOperationError("The interaction did not contain any selected values."); + } + } + + var commandPath = data.CustomID[Constants.InteractionTree.Length..][2..] + .Split(' ', StringSplitOptions.RemoveEmptyEntries); + + var buildParameters = data.ComponentType switch + { + ComponentType.Button => new Dictionary>(), + ComponentType.SelectMenu => Result>>.FromSuccess + ( + new Dictionary> + { + { "values", data.Values.Value } + } + ), + _ => new InvalidOperationError("An unsupported component type was encountered.") + }; + + if (!buildParameters.IsSuccess) + { + return (Result)buildParameters; + } + + var parameters = buildParameters.Entity; + + return await TryExecuteInteractionCommandAsync(context, commandPath, parameters, ct); + } + + private async Task HandleModalInteractionAsync + ( + InteractionContext context, + IModalSubmitData data, + CancellationToken ct = default + ) + { + if (!data.CustomID.StartsWith(Constants.InteractionTree)) + { + // Not a component we handle + return Result.FromSuccess(); + } + + var commandPath = data.CustomID[Constants.InteractionTree.Length..][2..] + .Split(' ', StringSplitOptions.RemoveEmptyEntries); + + var parameters = ExtractParameters(data.Components); + + return await TryExecuteInteractionCommandAsync + ( + context, + commandPath, + parameters, + ct + ); + } + + private static IReadOnlyDictionary> ExtractParameters + ( + IEnumerable components + ) + { + var parameters = new Dictionary>(); + foreach (var component in components) + { + if (component is IPartialActionRowComponent actionRow) + { + if (!actionRow.Components.IsDefined(out var rowComponents)) + { + continue; + } + + var nestedComponents = ExtractParameters(rowComponents); + foreach (var nestedComponent in nestedComponents) + { + parameters.Add(nestedComponent.Key, nestedComponent.Value); + } + + continue; + } + + switch (component) + { + case IPartialTextInputComponent textInput: + { + if (!textInput.CustomID.IsDefined(out var id)) + { + continue; + } + + if (!textInput.Value.IsDefined(out var value)) + { + continue; + } + + parameters.Add(id.Replace('-', '_').Camelize(), new[] { value }); + break; + } + case IPartialSelectMenuComponent selectMenu: + { + if (!selectMenu.CustomID.IsDefined(out var id)) + { + continue; + } + + if (!selectMenu.Options.IsDefined(out var options)) + { + continue; + } + + var values = options.Where(op => op.Value.HasValue).Select(op => op.Value.Value).ToList(); + + parameters.Add(id.Replace('-', '_').Camelize(), values); + break; + } + } + } + + return parameters; + } + + private async Task TryExecuteInteractionCommandAsync + ( + InteractionContext context, + IReadOnlyList commandPath, + IReadOnlyDictionary> parameters, + CancellationToken ct + ) + { + var prepareCommand = await _commandService.TryPrepareCommandAsync + ( + commandPath, + parameters, + _services, + searchOptions: _treeSearchOptions, + tokenizerOptions: _tokenizerOptions, + treeName: Constants.InteractionTree, + ct: ct + ); + + if (!prepareCommand.IsSuccess) + { + return (Result)prepareCommand; + } + + var preparedCommand = prepareCommand.Entity; + + var suppressResponseAttribute = preparedCommand.Command.Node + .FindCustomAttributeOnLocalTree(); + + var shouldSendResponse = !(suppressResponseAttribute?.Suppress ?? _options.SuppressAutomaticResponses); + + // ReSharper disable once InvertIf + if (shouldSendResponse) + { + var response = new InteractionResponse(InteractionCallbackType.DeferredUpdateMessage); + var createResponse = await _interactionApi.CreateInteractionResponseAsync + ( + context.ID, + context.Token, + response, + ct: ct + ); + + if (!createResponse.IsSuccess) + { + return createResponse; + } + } + + // Run the actual command + var executionResult = (Result)await _commandService.TryExecuteAsync( + preparedCommand, + _services, + ct + ); + + return await _eventsRunner.RunPostExecutionEvents(context, executionResult, ct); + } +} \ No newline at end of file diff --git a/src/Mmcc.Bot/Interactions/InteractionsSetup.cs b/src/Mmcc.Bot/Interactions/InteractionsSetup.cs index 9922ed4..3ab3636 100644 --- a/src/Mmcc.Bot/Interactions/InteractionsSetup.cs +++ b/src/Mmcc.Bot/Interactions/InteractionsSetup.cs @@ -1,6 +1,11 @@ using Microsoft.Extensions.DependencyInjection; using Mmcc.Bot.Interactions.Moderation.MemberApplications; +using Remora.Discord.Gateway.Extensions; +using Remora.Discord.Interactivity; using Remora.Discord.Interactivity.Extensions; +using Remora.Extensions.Options.Immutable; + +using InteractivityResponder = Mmcc.Bot.EventResponders.Interactions.InteractivityResponder; namespace Mmcc.Bot.Interactions; @@ -16,10 +21,12 @@ public static class InteractionsSetup /// The . public static IServiceCollection AddInteractions(this IServiceCollection services) { - services.AddInteractivity(); - - services.AddInteractionGroup(); + //services.AddInteractivity(); <= default Remora interactivity. We don't use because we have a custom pipeline + services.AddMemoryCache(); + + services.AddInteractionGroup(); + return services; } } \ No newline at end of file diff --git a/src/Mmcc.Bot/Interactions/Moderation/MemberApplications/MemberApplicationsInteractions.cs b/src/Mmcc.Bot/Interactions/Moderation/MemberApplications/MemberApplicationsInteractions.cs index cc74728..d046ea1 100644 --- a/src/Mmcc.Bot/Interactions/Moderation/MemberApplications/MemberApplicationsInteractions.cs +++ b/src/Mmcc.Bot/Interactions/Moderation/MemberApplications/MemberApplicationsInteractions.cs @@ -9,6 +9,7 @@ using Mmcc.Bot.Common.Models.Settings; using Mmcc.Bot.Common.Statics; using Mmcc.Bot.InMemoryStore.Stores; +using Mmcc.Bot.RemoraAbstractions.Conditions.InteractionSpecific; using Mmcc.Bot.RemoraAbstractions.Services; using Mmcc.Bot.RemoraAbstractions.UI; using Remora.Discord.API.Abstractions.Objects; @@ -32,7 +33,6 @@ public class MemberApplicationsInteractions : InteractionGroup private readonly DiscordSettings _discordSettings; private readonly IMediator _mediator; private readonly IColourPalette _colourPalette; - private readonly IErrorProcessingService _errorProcessingService; public MemberApplicationsInteractions( InteractionContext context, @@ -42,8 +42,7 @@ public MemberApplicationsInteractions( IMessageMemberAppContextStore memberAppContextStore, DiscordSettings discordSettings, IMediator mediator, - IColourPalette colourPalette, - IErrorProcessingService errorProcessingService + IColourPalette colourPalette ) { _channelApi = channelApi; @@ -53,12 +52,13 @@ IErrorProcessingService errorProcessingService _discordSettings = discordSettings; _mediator = mediator; _colourPalette = colourPalette; - _errorProcessingService = errorProcessingService; _context = context; } [Button("approve-btn")] [SuppressInteractionResponse(true)] + [InteractionRequireGuild] + [InteractionRequireUserGuildPermission(DiscordPermission.BanMembers)] public async Task OnApproveButtonPressed() { var serverPrefixInput = FluentTextInputBuilder @@ -77,16 +77,17 @@ public async Task OnApproveButtonPressed() var modal = FluentCallbackModalBuilder .WithId("approve") - .HasTitle($"Approve member application") + .HasTitle("Approve member application") .WithActionRowFromTextInputs(serverPrefixInput, ignsListInput) .Build(); - var res = await _interactionHelperService.RespondWithModal(modal); - return res; + return await _interactionHelperService.RespondWithModal(modal); } [Modal("approve")] [SuppressInteractionResponse(true)] + [InteractionRequireGuild] + [InteractionRequireUserGuildPermission(DiscordPermission.BanMembers)] public async Task OnApproveModal(string serverPrefix, string igns) { var notificationResult = await _interactionHelperService.NotifyDeferredMessageIsComing(); @@ -94,13 +95,15 @@ public async Task OnApproveModal(string serverPrefix, string igns) { return notificationResult; } + + return Result.FromError(new GenericError("Hi")); var ignsList = igns.Split(' ').ToList(); var approveResult = await ApproveMemberApplication(serverPrefix, ignsList); if (!approveResult.IsSuccess) { - return await _interactionHelperService.SendErrorNotification(approveResult.Error); + return Result.FromError(approveResult); } var sendSuccessEmbed = await _interactionHelperService.SendFollowup(approveResult.Entity); diff --git a/src/Mmcc.Bot/Middleware/InteractionErrorNotificationMiddleware.cs b/src/Mmcc.Bot/Middleware/InteractionErrorNotificationMiddleware.cs new file mode 100644 index 0000000..6f1f956 --- /dev/null +++ b/src/Mmcc.Bot/Middleware/InteractionErrorNotificationMiddleware.cs @@ -0,0 +1,48 @@ +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using Mmcc.Bot.RemoraAbstractions.Services; +using Mmcc.Bot.RemoraAbstractions.Services.Interactions; +using Remora.Discord.Commands.Contexts; +using Remora.Results; + +namespace Mmcc.Bot.Middleware; + +public class InteractionErrorNotificationMiddleware : IInteractionPostExecutionEvent +{ + private readonly ILogger _logger; + private readonly IErrorProcessingService _errorProcessingService; + private readonly IInteractionHelperService _interactionHelper; + + public InteractionErrorNotificationMiddleware( + ILogger logger, + IErrorProcessingService errorProcessingService, + IInteractionHelperService interactionHelper + ) + { + _logger = logger; + _errorProcessingService = errorProcessingService; + _interactionHelper = interactionHelper; + } + + public async Task AfterExecutionAsync( + InteractionContext interactionContext, + Result interactionResult, + CancellationToken ct + ) + { + if (interactionResult.IsSuccess) + { + return Result.FromSuccess(); + } + + var err = interactionResult.Error; + var errorEmbed = _errorProcessingService.GetErrorEmbed(err); + + // var sendEmbedResult = await _channelApi.CreateMessageAsync(interactionContext.ChannelID, embeds: new[] { errorEmbed }, ct: ct); + var sendErrorEmbed = await _interactionHelper.SendFollowup(errorEmbed); + return !sendErrorEmbed.IsSuccess + ? Result.FromError(sendErrorEmbed.Error) + : Result.FromSuccess(); + } +} \ No newline at end of file diff --git a/src/Mmcc.Bot/Middleware/MiddlewareSetup.cs b/src/Mmcc.Bot/Middleware/MiddlewareSetup.cs index 21ec04a..316250a 100644 --- a/src/Mmcc.Bot/Middleware/MiddlewareSetup.cs +++ b/src/Mmcc.Bot/Middleware/MiddlewareSetup.cs @@ -1,4 +1,5 @@ using Microsoft.Extensions.DependencyInjection; +using Mmcc.Bot.RemoraAbstractions.Services.Interactions; using Remora.Discord.Commands.Services; namespace Mmcc.Bot.Middleware; @@ -16,6 +17,7 @@ public static class MiddlewareSetup public static IServiceCollection AddBotMiddlewares(this IServiceCollection services) { services.AddScoped(); + services.AddScoped(); return services; } diff --git a/src/Mmcc.Bot/Mmcc.Bot.csproj b/src/Mmcc.Bot/Mmcc.Bot.csproj index 818d7f1..2a40c51 100644 --- a/src/Mmcc.Bot/Mmcc.Bot.csproj +++ b/src/Mmcc.Bot/Mmcc.Bot.csproj @@ -36,8 +36,4 @@ - - - - From 1b288bbc5259ba403b0cce544b078f15daa00f67 Mon Sep 17 00:00:00 2001 From: Lukasz Dabrowski Date: Mon, 7 Nov 2022 03:23:01 +0000 Subject: [PATCH 06/30] fixing missing interaction conditions --- src/Mmcc.Bot.RemoraAbstractions/AbstractionsSetup.cs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/Mmcc.Bot.RemoraAbstractions/AbstractionsSetup.cs b/src/Mmcc.Bot.RemoraAbstractions/AbstractionsSetup.cs index 43a6c0c..05ce82d 100644 --- a/src/Mmcc.Bot.RemoraAbstractions/AbstractionsSetup.cs +++ b/src/Mmcc.Bot.RemoraAbstractions/AbstractionsSetup.cs @@ -1,5 +1,6 @@ using Microsoft.Extensions.DependencyInjection; -using Mmcc.Bot.RemoraAbstractions.Conditions; +using Mmcc.Bot.RemoraAbstractions.Conditions.CommandSpecific; +using Mmcc.Bot.RemoraAbstractions.Conditions.InteractionSpecific; using Mmcc.Bot.RemoraAbstractions.Parsers; using Mmcc.Bot.RemoraAbstractions.Services; using Mmcc.Bot.RemoraAbstractions.Services.MessageResponders; @@ -31,6 +32,9 @@ public static IServiceCollection AddRemoraAbstractions(this IServiceCollection s services.AddCondition(); services.AddCondition(); + services.AddCondition(); + services.AddCondition(); + services.AddParser(); services.AddParser(); From 9bca4443740f3c204f170987a0add2b70770b240 Mon Sep 17 00:00:00 2001 From: Lukasz Dabrowski Date: Mon, 7 Nov 2022 03:23:51 +0000 Subject: [PATCH 07/30] rework the help system --- .../AbstractionsSetup.cs | 1 + .../CommandTreeWalker.cs | 62 ++++++ .../Mmcc.Bot.RemoraAbstractions.csproj | 4 - .../Services/HelpService.cs | 187 ++++++++++-------- src/Mmcc.Bot/Commands/CommandsSetup.cs | 1 - src/Mmcc.Bot/Commands/Core/Help/GetForAll.cs | 53 ----- .../Commands/Core/Help/GetHelpForCategory.cs | 60 ------ .../Commands/Core/Help/HelpCommands.cs | 84 -------- src/Mmcc.Bot/Commands/Core/HelpCommands.cs | 46 +++++ .../Commands/Core/MmccInfoCommands.cs | 6 +- .../MemberApplicationsInteractions.cs | 2 - 11 files changed, 214 insertions(+), 292 deletions(-) create mode 100644 src/Mmcc.Bot.RemoraAbstractions/CommandTreeWalker.cs delete mode 100644 src/Mmcc.Bot/Commands/Core/Help/GetForAll.cs delete mode 100644 src/Mmcc.Bot/Commands/Core/Help/GetHelpForCategory.cs delete mode 100644 src/Mmcc.Bot/Commands/Core/Help/HelpCommands.cs create mode 100644 src/Mmcc.Bot/Commands/Core/HelpCommands.cs diff --git a/src/Mmcc.Bot.RemoraAbstractions/AbstractionsSetup.cs b/src/Mmcc.Bot.RemoraAbstractions/AbstractionsSetup.cs index 05ce82d..2b2fbd1 100644 --- a/src/Mmcc.Bot.RemoraAbstractions/AbstractionsSetup.cs +++ b/src/Mmcc.Bot.RemoraAbstractions/AbstractionsSetup.cs @@ -28,6 +28,7 @@ public static IServiceCollection AddRemoraAbstractions(this IServiceCollection s services.AddScoped(); services.AddScoped(); + services.AddScoped(); services.AddCondition(); services.AddCondition(); diff --git a/src/Mmcc.Bot.RemoraAbstractions/CommandTreeWalker.cs b/src/Mmcc.Bot.RemoraAbstractions/CommandTreeWalker.cs new file mode 100644 index 0000000..802f163 --- /dev/null +++ b/src/Mmcc.Bot.RemoraAbstractions/CommandTreeWalker.cs @@ -0,0 +1,62 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Remora.Commands.Trees; +using Remora.Commands.Trees.Nodes; + +namespace Mmcc.Bot.RemoraAbstractions; + +public class CommandTreeWalker +{ + private readonly CommandTree _commandTree; + + public CommandTreeWalker(CommandTree commandTree) + => _commandTree = commandTree; + + public void PreOrderTraverseParentNodes(Action onParentNode) + => PreOrderTraverseParentNodes(_commandTree.Root, onParentNode); + + public GroupNode? GetGroupNodeByPath(List path) + { + var root = _commandTree.Root; + + var currPathIndex = 0; + var children = root.Children.OfType().ToList(); + while (true) + { + var matchedChild = children.FirstOrDefault(c => c.Key.Equals(path[currPathIndex])); + + if (matchedChild is null) + return null; + + if (currPathIndex == path.Count - 1) + return matchedChild; + + currPathIndex++; + children = matchedChild.Children.OfType().ToList(); + } + } + + public List CollectPath(GroupNode node) + { + IEnumerable res = new List { node.Key }; + var parent = node.Parent; + while (parent is GroupNode groupNode) + { + res = res.Prepend(groupNode.Key); + parent = groupNode.Parent; + } + + return res.ToList(); + } + + private void PreOrderTraverseParentNodes(IParentNode parentNode, Action onNode) + { + onNode(parentNode); + + foreach (var childNode in parentNode.Children.OfType()) + { + PreOrderTraverseParentNodes(childNode, onNode); + } + } +} \ No newline at end of file diff --git a/src/Mmcc.Bot.RemoraAbstractions/Mmcc.Bot.RemoraAbstractions.csproj b/src/Mmcc.Bot.RemoraAbstractions/Mmcc.Bot.RemoraAbstractions.csproj index 693b413..bcee1b1 100644 --- a/src/Mmcc.Bot.RemoraAbstractions/Mmcc.Bot.RemoraAbstractions.csproj +++ b/src/Mmcc.Bot.RemoraAbstractions/Mmcc.Bot.RemoraAbstractions.csproj @@ -13,8 +13,4 @@ - - - - diff --git a/src/Mmcc.Bot.RemoraAbstractions/Services/HelpService.cs b/src/Mmcc.Bot.RemoraAbstractions/Services/HelpService.cs index 8414465..477934e 100644 --- a/src/Mmcc.Bot.RemoraAbstractions/Services/HelpService.cs +++ b/src/Mmcc.Bot.RemoraAbstractions/Services/HelpService.cs @@ -1,118 +1,131 @@ using System.Collections.Generic; using System.Linq; -using System.Text; using Mmcc.Bot.Common.Models.Colours; +using Mmcc.Bot.Common.Models.Settings; using Mmcc.Bot.Common.Statics; using Remora.Commands.Trees.Nodes; using Remora.Discord.API.Objects; +using Remora.Discord.Extensions.Formatting; +using Remora.Results; namespace Mmcc.Bot.RemoraAbstractions.Services; -/// -/// Service for obtaining help embeds. -/// public interface IHelpService { - /// - /// Traverses a command tree and produces help embeds. - /// - /// The nodes to traverse. - /// The embeds output. - void TraverseAndGetHelpEmbeds(IList nodes, IList embeds); + Embed GetHelpForAll(); + Result GetHelpForCategory(List pathToCategory); } - -/// + public class HelpService : IHelpService { + private const string CategoryIcon = ":file_folder:"; + private const string CommandIcon = "❯"; + private readonly IColourPalette _colourPalette; + private readonly DiscordSettings _discordSettings; + private readonly CommandTreeWalker _cmdTreeWalker; - /// - /// Instantiates a new instance of . - /// - /// The colour palette. - public HelpService(IColourPalette colourPalette) + public HelpService( + IColourPalette colourPalette, + CommandTreeWalker cmdTreeWalker, + DiscordSettings discordSettings + ) { _colourPalette = colourPalette; + _cmdTreeWalker = cmdTreeWalker; + _discordSettings = discordSettings; } - /// - public void TraverseAndGetHelpEmbeds(IList nodes, IList embeds) + public Embed GetHelpForAll() { - var orphans = nodes - .OfType() - .ToList(); - var normals = nodes - .OfType() - .ToList(); - var fields = new List(); - - foreach (var orphan in orphans) + var categoryEmbedFields = new List(); + _cmdTreeWalker.PreOrderTraverseParentNodes(node => { - var nameString = new StringBuilder(); - var orphanParams = orphan.Shape.Parameters; - - if (orphanParams.Any()) - { - var paramsString = new StringBuilder(); - - for (var i = 0; i < orphanParams.Count; i++) - { - paramsString.Append(i != orphanParams.Count - 1 - ? $"<{orphanParams[i].HintName}> " - : $"<{orphanParams[i].HintName}>"); - } - - nameString.AppendLine($"❯ {orphan.Key} {paramsString}"); - } - else - { - nameString.AppendLine($"❯ {orphan.Key}"); - } - - var fieldValueSb = new StringBuilder(); - if (orphan.Aliases.Any()) - { - fieldValueSb.Append("**Aliases:** "); - for (var i = 0; i < orphan.Aliases.Count; i++) - { - fieldValueSb.Append(i != orphan.Aliases.Count - 1 - ? $"\"{orphan.Aliases[i]}\", " - : $"\"{orphan.Aliases[i]}\""); - } - } - fieldValueSb.Append("\n" + orphan.Shape.Description); - - fields.Add(new EmbedField(nameString.ToString(), $"{fieldValueSb}", false)); - } - - var parent = orphans.FirstOrDefault()?.Parent; - var embed = parent switch + if (node is not GroupNode groupNode) + return; + + var embedFieldForCategory = GetEmbedFieldForCategory(groupNode); + categoryEmbedFields.Add(embedFieldForCategory); + }); + + var helpEmbed = new Embed { - GroupNode g => new Embed - { - // what the fuck?? - Title = $":arrow_right: {g.Description} " + - $"[`!{g.Key}`{(g.Aliases.Any() ? "/" + string.Join("/", g.Aliases.Select(a => $"`!{a}`")) : "")}]", - Description = $"Usage: `!{g.Key} `." - }, - _ => new Embed - { - Title = ":arrow_right: General commands [`!`]", - Description = "Usage: `! `." - } + Title = ":information_source: Help", + Description = "Shows available categories. To see commands for a given category use `!help `.", + Fields = categoryEmbedFields, + Colour = _colourPalette.Blue, + Thumbnail = EmbedProperties.MmccLogoThumbnail }; - embed = embed with + + return helpEmbed; + } + + public Result GetHelpForCategory(List pathToCategory) + { + var category = _cmdTreeWalker.GetGroupNodeByPath(pathToCategory); + if (category is null) + return Result.FromError( + new NotFoundError($"No category matches {Markdown.InlineCode(string.Join(" ", category))}") + ); + + var formattedPath = GetFormattedPathForCategory(category); + + var embedTitle = $"{CategoryIcon} {category.Description} [{formattedPath}]"; + var embedDescription = $"Usage: {formattedPath[..^1]} `"; + var embedFields = GetCommandsEmbedFieldsForCategory(category); + + var embed = new Embed { - Fields = fields, + Title = embedTitle, + Description = embedDescription, + Fields = embedFields, Colour = _colourPalette.Blue, Thumbnail = EmbedProperties.MmccLogoThumbnail }; - - embeds.Add(embed); + + return embed; + } - foreach (var normal in normals) - { - TraverseAndGetHelpEmbeds(normal.Children.ToList(), embeds); - } + private List GetCommandsEmbedFieldsForCategory(GroupNode category) + => category.Children + .OfType() + .Select(GetEmbedFieldForCommand) + .ToList(); + + private EmbedField GetEmbedFieldForCommand(CommandNode cmd) + { + var cmdDescription = cmd.Shape.Description; + + var cmdArgs = cmd.Shape.Parameters; + var cmdArgsFormatted = string.Join(" ", cmdArgs.Select(x => $"<{x.HintName}>")); + + var aliasesFormatted = string.Join(", ", cmd.Aliases.Select(x => $"\"{x}\"")); + + var fieldName = $"{CommandIcon} {cmd.Key} {cmdArgsFormatted}"; + var fieldDescription = $"{Markdown.Underline("Aliases:")} {aliasesFormatted}\n{cmdDescription}"; + + return new EmbedField(fieldName, fieldDescription, false); + } + + private EmbedField GetEmbedFieldForCategory(GroupNode category) + { + var formattedPath = GetFormattedPathForCategory(category); + var fullHelpCmd = $"!help {formattedPath}"; + + var fieldName = $"{CategoryIcon} {category.Description} [{formattedPath}]"; + var fieldDesc = $"Full help: {Markdown.InlineCode(fullHelpCmd)}"; + + return new EmbedField(fieldName, fieldDesc, false); + } + + private string GetFormattedPathForCategory(GroupNode category) + { + var prefix = _discordSettings.Prefix; + var path = category.Parent is GroupNode + ? string.Join(" ", _cmdTreeWalker.CollectPath(category)) + : category.Key; + var formattedPath = Markdown.InlineCode($"{prefix}{path}"); + + return formattedPath; } } \ No newline at end of file diff --git a/src/Mmcc.Bot/Commands/CommandsSetup.cs b/src/Mmcc.Bot/Commands/CommandsSetup.cs index 7806023..ccc3017 100644 --- a/src/Mmcc.Bot/Commands/CommandsSetup.cs +++ b/src/Mmcc.Bot/Commands/CommandsSetup.cs @@ -1,6 +1,5 @@ using Microsoft.Extensions.DependencyInjection; using Mmcc.Bot.Commands.Core; -using Mmcc.Bot.Commands.Core.Help; using Mmcc.Bot.Commands.Diagnostics; using Mmcc.Bot.Commands.Guilds; using Mmcc.Bot.Commands.Minecraft; diff --git a/src/Mmcc.Bot/Commands/Core/Help/GetForAll.cs b/src/Mmcc.Bot/Commands/Core/Help/GetForAll.cs deleted file mode 100644 index bb9f149..0000000 --- a/src/Mmcc.Bot/Commands/Core/Help/GetForAll.cs +++ /dev/null @@ -1,53 +0,0 @@ -using System.Collections.Generic; -using System.Linq; -using MediatR; -using Mmcc.Bot.Common.Models.Colours; -using Mmcc.Bot.Common.Statics; -using Mmcc.Bot.RemoraAbstractions.Services; -using Remora.Commands.Trees; -using Remora.Discord.API.Objects; -using Remora.Results; - -namespace Mmcc.Bot.Commands.Core.Help; - -/// -/// Gets help for all available commands, maintaining the grouped structure. -/// -public class GetForAll -{ - /// - /// Query to get all available commands, maintaining the grouped structure. - /// - public record Query : IRequest>>; - - public class Handler : RequestHandler>> - { - private readonly CommandTree _commandTree; - private readonly IColourPalette _colourPalette; - private readonly IHelpService _helpService; - - public Handler(CommandTree commandTree, IColourPalette colourPalette, IHelpService helpService) - { - _commandTree = commandTree; - _colourPalette = colourPalette; - _helpService = helpService; - } - - protected override Result> Handle(Query request) - { - var embeds = new List - { - new() - { - Title = ":information_source: Help", - Description = "Shows available commands by category", - Colour = _colourPalette.Blue, - Thumbnail = EmbedProperties.MmccLogoThumbnail - } - }; - - _helpService.TraverseAndGetHelpEmbeds(_commandTree.Root.Children.ToList(), embeds); - return embeds; - } - } -} \ No newline at end of file diff --git a/src/Mmcc.Bot/Commands/Core/Help/GetHelpForCategory.cs b/src/Mmcc.Bot/Commands/Core/Help/GetHelpForCategory.cs deleted file mode 100644 index 830d210..0000000 --- a/src/Mmcc.Bot/Commands/Core/Help/GetHelpForCategory.cs +++ /dev/null @@ -1,60 +0,0 @@ -using System.Collections.Generic; -using System.Linq; -using FluentValidation; -using MediatR; -using Mmcc.Bot.RemoraAbstractions.Services; -using Remora.Commands.Trees; -using Remora.Commands.Trees.Nodes; -using Remora.Discord.API.Objects; -using Remora.Results; - -namespace Mmcc.Bot.Commands.Core.Help; - -/// -/// Gets help for all available commands within a category. -/// -public class GetHelpForCategory -{ - /// - /// Query to get help embeds for all available commands within a category. - /// - /// The name/alias of the category. - public record Query(string CategoryName) : IRequest>; - - public class Validator : AbstractValidator - { - public Validator() => - RuleFor(q => q.CategoryName) - .NotEmpty(); - } - - public class Handler : RequestHandler> - { - private readonly CommandTree _commandTree; - private readonly IHelpService _helpService; - - public Handler(CommandTree commandTree, IHelpService helpService) - { - _commandTree = commandTree; - _helpService = helpService; - } - - protected override Result Handle(Query request) - { - var categoryName = request.CategoryName; - var categories = _commandTree.Root.Children.ToList() - .OfType() - .FirstOrDefault(gn => gn.Key.Equals(categoryName) || gn.Aliases.Contains(categoryName)); - var embeds = new List(); - - if (categories is null) - { - return Result.FromSuccess(null); - } - - _helpService.TraverseAndGetHelpEmbeds(categories.Children.ToList(), embeds); - - return Result.FromSuccess(embeds.FirstOrDefault()); - } - } -} \ No newline at end of file diff --git a/src/Mmcc.Bot/Commands/Core/Help/HelpCommands.cs b/src/Mmcc.Bot/Commands/Core/Help/HelpCommands.cs deleted file mode 100644 index e252f73..0000000 --- a/src/Mmcc.Bot/Commands/Core/Help/HelpCommands.cs +++ /dev/null @@ -1,84 +0,0 @@ -using System.ComponentModel; -using System.Linq; -using System.Threading.Tasks; -using MediatR; -using Mmcc.Bot.RemoraAbstractions.Services; -using Mmcc.Bot.RemoraAbstractions.Services.MessageResponders; -using Remora.Commands.Attributes; -using Remora.Commands.Groups; -using Remora.Discord.Commands.Contexts; -using Remora.Discord.Gateway.Responders; -using Remora.Results; - -namespace Mmcc.Bot.Commands.Core.Help; - -/// -/// Help commands. -/// -public class HelpCommands : CommandGroup -{ - private readonly MessageContext _context; - private readonly CommandMessageResponder _responder; - private readonly IDmSender _dmSender; - private readonly IMediator _mediator; - - /// - /// Instantiates a new instance of class. - /// - /// The message context. - /// The command responder. - /// The DM sender. - /// The mediator. - public HelpCommands( - MessageContext context, - CommandMessageResponder responder, - IDmSender dmSender, - IMediator mediator - ) - { - _context = context; - _responder = responder; - _mediator = mediator; - _dmSender = dmSender; - } - - [Command("help")] - [Description("Shows available commands")] - public async Task Help() - { - var getEmbedsResult = await _mediator.Send(new GetForAll.Query()); - - if (!getEmbedsResult.IsSuccess) - { - return getEmbedsResult; - } - - var embedChunks = getEmbedsResult.Entity.Chunk(10); - - foreach (var embeds in embedChunks) - { - var sendDmChunkRes = await _dmSender.Send(_context.User.ID, embeds); - - if (!sendDmChunkRes.IsSuccess) - { - return sendDmChunkRes; - } - } - - return await _responder.Respond("Help has been sent to your DMs :smile:."); - } - - [Command("help")] - [Description("Shows help for a given category")] - public async Task Help(string categoryName) => - await _mediator.Send(new GetHelpForCategory.Query(categoryName)) switch - { - { IsSuccess: true, Entity: { } embed } => - await _responder.Respond(embed), - - { IsSuccess: true } => - Result.FromError(new NotFoundError($"Could not find a category with name `{categoryName}`.")), - - { IsSuccess: false } res => res - }; -} \ No newline at end of file diff --git a/src/Mmcc.Bot/Commands/Core/HelpCommands.cs b/src/Mmcc.Bot/Commands/Core/HelpCommands.cs new file mode 100644 index 0000000..89284a8 --- /dev/null +++ b/src/Mmcc.Bot/Commands/Core/HelpCommands.cs @@ -0,0 +1,46 @@ +using System.Collections.Generic; +using System.ComponentModel; +using System.Linq; +using System.Threading.Tasks; +using Mmcc.Bot.RemoraAbstractions.Services; +using Mmcc.Bot.RemoraAbstractions.Services.MessageResponders; +using Remora.Commands.Attributes; +using Remora.Commands.Groups; +using Remora.Results; + +namespace Mmcc.Bot.Commands.Core; + +/// +/// Help commands. +/// +public class HelpCommands : CommandGroup +{ + private readonly CommandMessageResponder _responder; + private readonly HelpService _helpService; + + public HelpCommands(CommandMessageResponder responder, HelpService helpService) + { + _responder = responder; + _helpService = helpService; + } + + [Command("help")] + [Description("Shows available commands")] + public async Task Help() + { + var helpEmbed = _helpService.GetHelpForAll(); + + return await _responder.Respond(helpEmbed); + } + + [Command("help")] + [Description("Shows help for a given category")] + public async Task Help([Greedy] IEnumerable path) => + _helpService.GetHelpForCategory(path.ToList()) switch + { + { IsSuccess: true, Entity: { } embed } => + await _responder.Respond(embed), + + { IsSuccess: false } res => res + }; +} \ No newline at end of file diff --git a/src/Mmcc.Bot/Commands/Core/MmccInfoCommands.cs b/src/Mmcc.Bot/Commands/Core/MmccInfoCommands.cs index 84e6e7d..9fead44 100644 --- a/src/Mmcc.Bot/Commands/Core/MmccInfoCommands.cs +++ b/src/Mmcc.Bot/Commands/Core/MmccInfoCommands.cs @@ -1,7 +1,9 @@ using System.Collections.Generic; using System.ComponentModel; +using System.Linq; using System.Threading.Tasks; using Mmcc.Bot.Common.Statics; +using Mmcc.Bot.RemoraAbstractions.Services; using Remora.Commands.Attributes; using Remora.Commands.Groups; using Remora.Discord.API.Abstractions.Objects; @@ -18,11 +20,13 @@ public class MmccInfoCommands : CommandGroup { private readonly IDiscordRestChannelAPI _channelApi; private readonly MessageContext _context; + private readonly HelpService _helpService; - public MmccInfoCommands(IDiscordRestChannelAPI channelApi, MessageContext context) + public MmccInfoCommands(IDiscordRestChannelAPI channelApi, MessageContext context, HelpService helpService) { _channelApi = channelApi; _context = context; + _helpService = helpService; } [Command("mmcc")] diff --git a/src/Mmcc.Bot/Interactions/Moderation/MemberApplications/MemberApplicationsInteractions.cs b/src/Mmcc.Bot/Interactions/Moderation/MemberApplications/MemberApplicationsInteractions.cs index d046ea1..4737651 100644 --- a/src/Mmcc.Bot/Interactions/Moderation/MemberApplications/MemberApplicationsInteractions.cs +++ b/src/Mmcc.Bot/Interactions/Moderation/MemberApplications/MemberApplicationsInteractions.cs @@ -95,8 +95,6 @@ public async Task OnApproveModal(string serverPrefix, string igns) { return notificationResult; } - - return Result.FromError(new GenericError("Hi")); var ignsList = igns.Split(' ').ToList(); From 422512d892d9d7ee62c79a7f85096497f10f50d9 Mon Sep 17 00:00:00 2001 From: Lukasz Dabrowski Date: Mon, 7 Nov 2022 03:26:51 +0000 Subject: [PATCH 08/30] fix System.InvalidOperationException: Unable to resolve service for type 'Mmcc.Bot.RemoraAbstractions.Services.HelpService' --- src/Mmcc.Bot/Commands/Core/HelpCommands.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Mmcc.Bot/Commands/Core/HelpCommands.cs b/src/Mmcc.Bot/Commands/Core/HelpCommands.cs index 89284a8..e7b0060 100644 --- a/src/Mmcc.Bot/Commands/Core/HelpCommands.cs +++ b/src/Mmcc.Bot/Commands/Core/HelpCommands.cs @@ -16,9 +16,9 @@ namespace Mmcc.Bot.Commands.Core; public class HelpCommands : CommandGroup { private readonly CommandMessageResponder _responder; - private readonly HelpService _helpService; + private readonly IHelpService _helpService; - public HelpCommands(CommandMessageResponder responder, HelpService helpService) + public HelpCommands(CommandMessageResponder responder, IHelpService helpService) { _responder = responder; _helpService = helpService; From c137f9328ac0c7ee6e7087c6beebef23dc15e98b Mon Sep 17 00:00:00 2001 From: Lukasz Dabrowski Date: Mon, 7 Nov 2022 03:28:43 +0000 Subject: [PATCH 09/30] fix messed up error in !help when a category could not be matched --- src/Mmcc.Bot.RemoraAbstractions/Services/HelpService.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Mmcc.Bot.RemoraAbstractions/Services/HelpService.cs b/src/Mmcc.Bot.RemoraAbstractions/Services/HelpService.cs index 477934e..b9f570a 100644 --- a/src/Mmcc.Bot.RemoraAbstractions/Services/HelpService.cs +++ b/src/Mmcc.Bot.RemoraAbstractions/Services/HelpService.cs @@ -65,7 +65,7 @@ public Result GetHelpForCategory(List pathToCategory) var category = _cmdTreeWalker.GetGroupNodeByPath(pathToCategory); if (category is null) return Result.FromError( - new NotFoundError($"No category matches {Markdown.InlineCode(string.Join(" ", category))}") + new NotFoundError($"No category matches {Markdown.InlineCode(string.Join(" ", pathToCategory))}") ); var formattedPath = GetFormattedPathForCategory(category); From 9f3d94c17bed38c0823240793ca719a94bef33b2 Mon Sep 17 00:00:00 2001 From: TraceLD Date: Mon, 7 Nov 2022 04:22:35 +0000 Subject: [PATCH 10/30] fix circular dependency mud --- .../Entities/MemberApplicationExtensions.cs | 79 ------------------- .../Mmcc.Bot.Common.Extensions.csproj | 5 -- .../System/{String.cs => StringExtensions.cs} | 4 +- .../Mmcc.Bot.RemoraAbstractions.csproj | 1 + .../Services/HelpService.cs | 18 ++++- .../MemberApplicationsCommands.cs | 32 ++++---- src/Mmcc.Bot/Program.cs | 3 + .../ICommonEmbedFieldsProvider.cs | 13 +++ .../MemberApplicationsEmbedFieldProvider.cs | 20 +++++ .../ICommonEmbedProvider.cs | 11 +++ .../MemberApplicationEmbedProvider.cs | 67 ++++++++++++++++ src/Mmcc.Bot/Providers/ProvidersSetup.cs | 33 ++++++++ 12 files changed, 180 insertions(+), 106 deletions(-) delete mode 100644 src/Mmcc.Bot.Common.Extensions/Database/Entities/MemberApplicationExtensions.cs rename src/Mmcc.Bot.Common.Extensions/System/{String.cs => StringExtensions.cs} (69%) create mode 100644 src/Mmcc.Bot/Providers/CommonEmbedFieldsProviders/ICommonEmbedFieldsProvider.cs create mode 100644 src/Mmcc.Bot/Providers/CommonEmbedFieldsProviders/MemberApplicationsEmbedFieldProvider.cs create mode 100644 src/Mmcc.Bot/Providers/CommonEmbedProviders/ICommonEmbedProvider.cs create mode 100644 src/Mmcc.Bot/Providers/CommonEmbedProviders/MemberApplicationEmbedProvider.cs create mode 100644 src/Mmcc.Bot/Providers/ProvidersSetup.cs diff --git a/src/Mmcc.Bot.Common.Extensions/Database/Entities/MemberApplicationExtensions.cs b/src/Mmcc.Bot.Common.Extensions/Database/Entities/MemberApplicationExtensions.cs deleted file mode 100644 index 62977bd..0000000 --- a/src/Mmcc.Bot.Common.Extensions/Database/Entities/MemberApplicationExtensions.cs +++ /dev/null @@ -1,79 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using Mmcc.Bot.Common.Models.Colours; -using Mmcc.Bot.Database.Entities; -using Mmcc.Bot.RemoraAbstractions.Timestamps; -using Remora.Discord.API.Objects; - -namespace Mmcc.Bot.Common.Extensions.Database.Entities -{ - /// - /// Extensions for . - /// - public static class MemberApplicationExtensions - { - /// - /// Get an embed representation of a member application. - /// - /// Member application. - /// Colour palette for the embed to use. - /// Embed representing the member application. - public static Embed GetEmbed(this MemberApplication memberApplication, IColourPalette colourPalette) - { - var statusStr = memberApplication.AppStatus.ToString(); - var embedConditionalAttributes = memberApplication.AppStatus switch - { - ApplicationStatus.Pending => new - { - Colour = colourPalette.Blue, - StatusFieldValue = $":clock1: {statusStr}" - }, - ApplicationStatus.Approved => new - { - Colour = colourPalette.Green, - StatusFieldValue = $":white_check_mark: {statusStr}" - }, - ApplicationStatus.Rejected => new - { - Colour = colourPalette.Red, - StatusFieldValue = $":no_entry: {statusStr}" - }, - _ => throw new ArgumentOutOfRangeException(nameof(memberApplication)) - }; - return new Embed - { - Title = $"Member Application #{memberApplication.MemberApplicationId}", - Description = - $"Submitted at {new DiscordTimestamp(memberApplication.AppTime).AsStyled(DiscordTimestampStyle.ShortDateTime)}.", - Fields = new List - { - new("Author", $"{memberApplication.AuthorDiscordName} (ID: `{memberApplication.AuthorDiscordId}`)", - false), - new("Status", embedConditionalAttributes.StatusFieldValue, false), - new( - "Provided details", - $"{memberApplication.MessageContent}\n" + - $"**[Original message (click here)](https://discord.com/channels/{memberApplication.GuildId}/{memberApplication.ChannelId}/{memberApplication.MessageId})**", - false - ) - }, - Colour = embedConditionalAttributes.Colour, - Thumbnail = new EmbedThumbnail(memberApplication.ImageUrl) - }; - } - - /// - /// Gets an enumerable of formatted embed fields that represent the member applications. - /// - /// Enumerable of member applications. - /// Enumerable of formatted embed fields that represent the member applications. - public static IEnumerable GetEmbedFields(this IEnumerable memberApplications) => - memberApplications.Select(app => new EmbedField - ( - $"[{app.MemberApplicationId}] {app.AuthorDiscordName}", - $"*Submitted at:* {new DiscordTimestamp(app.AppTime).AsStyled(DiscordTimestampStyle.ShortDateTime)}.", - false - )); - } -} \ No newline at end of file diff --git a/src/Mmcc.Bot.Common.Extensions/Mmcc.Bot.Common.Extensions.csproj b/src/Mmcc.Bot.Common.Extensions/Mmcc.Bot.Common.Extensions.csproj index 23df98a..817e645 100644 --- a/src/Mmcc.Bot.Common.Extensions/Mmcc.Bot.Common.Extensions.csproj +++ b/src/Mmcc.Bot.Common.Extensions/Mmcc.Bot.Common.Extensions.csproj @@ -10,7 +10,6 @@ - @@ -18,8 +17,4 @@ - - - - diff --git a/src/Mmcc.Bot.Common.Extensions/System/String.cs b/src/Mmcc.Bot.Common.Extensions/System/StringExtensions.cs similarity index 69% rename from src/Mmcc.Bot.Common.Extensions/System/String.cs rename to src/Mmcc.Bot.Common.Extensions/System/StringExtensions.cs index cbc0b17..3e0ea4a 100644 --- a/src/Mmcc.Bot.Common.Extensions/System/String.cs +++ b/src/Mmcc.Bot.Common.Extensions/System/StringExtensions.cs @@ -2,12 +2,14 @@ namespace Mmcc.Bot.Common.Extensions.System { - public static class String + public static class StringExtensions { public static string[] SplitByNewLine(this string s) => s.Split( new[] {"\r\n", "\r", "\n"}, StringSplitOptions.None ); + + public static string DoubleQuotes(this string s) => $"\"{s}\""; } } \ No newline at end of file diff --git a/src/Mmcc.Bot.RemoraAbstractions/Mmcc.Bot.RemoraAbstractions.csproj b/src/Mmcc.Bot.RemoraAbstractions/Mmcc.Bot.RemoraAbstractions.csproj index bcee1b1..13c2ac8 100644 --- a/src/Mmcc.Bot.RemoraAbstractions/Mmcc.Bot.RemoraAbstractions.csproj +++ b/src/Mmcc.Bot.RemoraAbstractions/Mmcc.Bot.RemoraAbstractions.csproj @@ -11,6 +11,7 @@ + diff --git a/src/Mmcc.Bot.RemoraAbstractions/Services/HelpService.cs b/src/Mmcc.Bot.RemoraAbstractions/Services/HelpService.cs index b9f570a..36a0fa0 100644 --- a/src/Mmcc.Bot.RemoraAbstractions/Services/HelpService.cs +++ b/src/Mmcc.Bot.RemoraAbstractions/Services/HelpService.cs @@ -1,8 +1,11 @@ using System.Collections.Generic; using System.Linq; +using System.Reflection; +using Mmcc.Bot.Common.Extensions.System; using Mmcc.Bot.Common.Models.Colours; using Mmcc.Bot.Common.Models.Settings; using Mmcc.Bot.Common.Statics; +using Mmcc.Bot.RemoraAbstractions.Conditions.CommandSpecific; using Remora.Commands.Trees.Nodes; using Remora.Discord.API.Objects; using Remora.Discord.Extensions.Formatting; @@ -95,14 +98,21 @@ private List GetCommandsEmbedFieldsForCategory(GroupNode category) private EmbedField GetEmbedFieldForCommand(CommandNode cmd) { var cmdDescription = cmd.Shape.Description; - var cmdArgs = cmd.Shape.Parameters; var cmdArgsFormatted = string.Join(" ", cmdArgs.Select(x => $"<{x.HintName}>")); - - var aliasesFormatted = string.Join(", ", cmd.Aliases.Select(x => $"\"{x}\"")); var fieldName = $"{CommandIcon} {cmd.Key} {cmdArgsFormatted}"; - var fieldDescription = $"{Markdown.Underline("Aliases:")} {aliasesFormatted}\n{cmdDescription}"; + + var fieldDescAliasesLine = cmd.Aliases.Any() + ? $"{Markdown.Underline("Aliases:")} {string.Join(", ", cmd.Aliases.Select(x => x.DoubleQuotes()))}\n" + : ""; + + var requiredPermission = cmd.CommandMethod.GetCustomAttribute(typeof(RequireUserGuildPermissionAttribute)); + var requiredPermissionLine = requiredPermission is RequireUserGuildPermissionAttribute r + ? $"{Markdown.Underline("Required user permission:")} {r.Permission}\n" + : ""; + + var fieldDescription = $"{fieldDescAliasesLine}{requiredPermissionLine}{cmdDescription}"; return new EmbedField(fieldName, fieldDescription, false); } diff --git a/src/Mmcc.Bot/Commands/Moderation/MemberApplications/MemberApplicationsCommands.cs b/src/Mmcc.Bot/Commands/Moderation/MemberApplications/MemberApplicationsCommands.cs index 8217e16..5ebf5be 100644 --- a/src/Mmcc.Bot/Commands/Moderation/MemberApplications/MemberApplicationsCommands.cs +++ b/src/Mmcc.Bot/Commands/Moderation/MemberApplications/MemberApplicationsCommands.cs @@ -10,8 +10,10 @@ using Mmcc.Bot.Common.Models.Colours; using Mmcc.Bot.Common.Models.Settings; using Mmcc.Bot.Common.Statics; +using Mmcc.Bot.CommonEmbedProviders; using Mmcc.Bot.Database.Entities; using Mmcc.Bot.InMemoryStore.Stores; +using Mmcc.Bot.Providers.CommonEmbedFieldsProviders; using Mmcc.Bot.RemoraAbstractions.Conditions; using Mmcc.Bot.RemoraAbstractions.Conditions.CommandSpecific; using Mmcc.Bot.RemoraAbstractions.Services.MessageResponders; @@ -46,17 +48,9 @@ public class MemberApplicationsCommands : CommandGroup private readonly CommandMessageResponder _responder; private readonly IMessageMemberAppContextStore _memberAppContextStore; - /// - /// Instantiates a new instance of . - /// - /// The message context. - /// The channel API. - /// The mediator. - /// The colour palette. - /// The Discord settings. - /// The guild API. - /// The command responder. - /// The app context store. + private readonly ICommonEmbedProvider _memberAppEmbedProvider; + private readonly ICommonEmbedFieldsProvider> _memberAppsEmbedFieldsProvider; + public MemberApplicationsCommands( MessageContext context, IDiscordRestChannelAPI channelApi, @@ -65,7 +59,9 @@ public MemberApplicationsCommands( DiscordSettings discordSettings, IDiscordRestGuildAPI guildApi, CommandMessageResponder responder, - IMessageMemberAppContextStore memberAppContextStore + IMessageMemberAppContextStore memberAppContextStore, + ICommonEmbedProvider memberAppEmbedProvider, + ICommonEmbedFieldsProvider> memberAppsEmbedFieldsProvider ) { _context = context; @@ -76,6 +72,8 @@ IMessageMemberAppContextStore memberAppContextStore _guildApi = guildApi; _responder = responder; _memberAppContextStore = memberAppContextStore; + _memberAppEmbedProvider = memberAppEmbedProvider; + _memberAppsEmbedFieldsProvider = memberAppsEmbedFieldsProvider; } [Command("info")] @@ -141,7 +139,7 @@ public async Task View(int id) ); var actionRows = ActionRowUtils.CreateActionRowWithComponents(approveButton).AsList(); - return await _responder.RespondWithComponents(actionRows, new(), app.GetEmbed(_colourPalette)); + return await _responder.RespondWithComponents(actionRows, new(), _memberAppEmbedProvider.GetEmbed(app)); } return getResult is {IsSuccess: true} @@ -159,7 +157,7 @@ public async Task ViewNextPending() => await _mediator.Send(new GetNextPending.Query { GuildId = _context.GuildID.Value }) switch { { IsSuccess: true, Entity: { } e } => - await _responder.Respond(e.GetEmbed(_colourPalette)), + await _responder.Respond(_memberAppEmbedProvider.GetEmbed(e)), { IsSuccess: true } => await _responder.Respond(new Embed @@ -200,7 +198,7 @@ public async Task ViewPending() await _responder.Respond( !e.Any() ? embedBase with { Description = "There are no pending applications at the moment." } - : embedBase with { Fields = e.GetEmbedFields().ToList() } + : embedBase with { Fields = _memberAppsEmbedFieldsProvider.GetEmbedFields(e).ToList() } ), { IsSuccess: false } res => res @@ -234,7 +232,7 @@ public async Task ViewApproved() await _responder.Respond( !e.Any() ? embedBase with { Description = "You have not approved any applications yet." } - : embedBase with { Fields = e.GetEmbedFields().ToList() } + : embedBase with { Fields = _memberAppsEmbedFieldsProvider.GetEmbedFields(e).ToList() } ), { IsSuccess: false } res => res @@ -268,7 +266,7 @@ public async Task ViewRejected() await _responder.Respond( !e.Any() ? embedBase with { Description = "You have not rejected any applications yet." } - : embedBase with { Fields = e.GetEmbedFields().ToList() } + : embedBase with { Fields = _memberAppsEmbedFieldsProvider.GetEmbedFields(e).ToList() } ), { IsSuccess: false } res => res diff --git a/src/Mmcc.Bot/Program.cs b/src/Mmcc.Bot/Program.cs index 347c6c1..010c816 100644 --- a/src/Mmcc.Bot/Program.cs +++ b/src/Mmcc.Bot/Program.cs @@ -23,6 +23,7 @@ using Mmcc.Bot.Mojang; using Mmcc.Bot.Polychat; using Mmcc.Bot.Polychat.Networking; +using Mmcc.Bot.Providers; using Mmcc.Bot.RemoraAbstractions; using Remora.Discord.Caching.Extensions; using Remora.Discord.Hosting.Extensions; @@ -63,6 +64,8 @@ services.AddMojangApi(); services.AddPolychat(hostContext.Configuration.GetSection("Ssmp")); + services.AddProviders(); + // Remora.Discord bot setup; services.AddRemoraAbstractions(); services.AddBotMiddlewares(); diff --git a/src/Mmcc.Bot/Providers/CommonEmbedFieldsProviders/ICommonEmbedFieldsProvider.cs b/src/Mmcc.Bot/Providers/CommonEmbedFieldsProviders/ICommonEmbedFieldsProvider.cs new file mode 100644 index 0000000..621847a --- /dev/null +++ b/src/Mmcc.Bot/Providers/CommonEmbedFieldsProviders/ICommonEmbedFieldsProvider.cs @@ -0,0 +1,13 @@ +using System.Collections; +using System.Collections.Generic; +using Remora.Discord.API.Objects; + +namespace Mmcc.Bot.Providers.CommonEmbedFieldsProviders; + +public interface ICommonEmbedFieldsProvider where T : IEnumerable +{ + /// + /// Gets an that represent the . + /// + IEnumerable GetEmbedFields(T objs); +} \ No newline at end of file diff --git a/src/Mmcc.Bot/Providers/CommonEmbedFieldsProviders/MemberApplicationsEmbedFieldProvider.cs b/src/Mmcc.Bot/Providers/CommonEmbedFieldsProviders/MemberApplicationsEmbedFieldProvider.cs new file mode 100644 index 0000000..fd5fbb4 --- /dev/null +++ b/src/Mmcc.Bot/Providers/CommonEmbedFieldsProviders/MemberApplicationsEmbedFieldProvider.cs @@ -0,0 +1,20 @@ +using System.Collections.Generic; +using System.Linq; +using Mmcc.Bot.Database.Entities; +using Mmcc.Bot.RemoraAbstractions.Timestamps; +using Remora.Discord.API.Objects; + +namespace Mmcc.Bot.Providers.CommonEmbedFieldsProviders; + +public class MemberApplicationsEmbedFieldProvider : ICommonEmbedFieldsProvider> +{ + public IEnumerable GetEmbedFields(IEnumerable memberApplications) + { + return memberApplications.Select(app => new EmbedField + ( + $"[{app.MemberApplicationId}] {app.AuthorDiscordName}", + $"*Submitted at:* {new DiscordTimestamp(app.AppTime).AsStyled(DiscordTimestampStyle.ShortDateTime)}.", + false + )); + } +} \ No newline at end of file diff --git a/src/Mmcc.Bot/Providers/CommonEmbedProviders/ICommonEmbedProvider.cs b/src/Mmcc.Bot/Providers/CommonEmbedProviders/ICommonEmbedProvider.cs new file mode 100644 index 0000000..1693e02 --- /dev/null +++ b/src/Mmcc.Bot/Providers/CommonEmbedProviders/ICommonEmbedProvider.cs @@ -0,0 +1,11 @@ +using Remora.Discord.API.Objects; + +namespace Mmcc.Bot.CommonEmbedProviders; + +public interface ICommonEmbedProvider +{ + /// + /// Gets an embed representation of . + /// + Embed GetEmbed(T obj); +} \ No newline at end of file diff --git a/src/Mmcc.Bot/Providers/CommonEmbedProviders/MemberApplicationEmbedProvider.cs b/src/Mmcc.Bot/Providers/CommonEmbedProviders/MemberApplicationEmbedProvider.cs new file mode 100644 index 0000000..a46ec74 --- /dev/null +++ b/src/Mmcc.Bot/Providers/CommonEmbedProviders/MemberApplicationEmbedProvider.cs @@ -0,0 +1,67 @@ +using System; +using System.Collections.Generic; +using Mmcc.Bot.Common.Models.Colours; +using Mmcc.Bot.CommonEmbedProviders; +using Mmcc.Bot.Database.Entities; +using Mmcc.Bot.RemoraAbstractions.Timestamps; +using Remora.Discord.API.Objects; + +namespace Mmcc.Bot.Providers.CommonEmbedProviders; + +public class MemberApplicationEmbedProvider : ICommonEmbedProvider +{ + private readonly IColourPalette _colourPalette; + + public MemberApplicationEmbedProvider(IColourPalette colourPalette) + => _colourPalette = colourPalette; + + public Embed GetEmbed(MemberApplication memberApplication) + { + var statusStr = memberApplication.AppStatus.ToString(); + var embedConditionalAttributes = memberApplication.AppStatus switch + { + ApplicationStatus.Pending => new + { + Colour = _colourPalette.Blue, + StatusFieldValue = $":clock1: {statusStr}" + }, + ApplicationStatus.Approved => new + { + Colour = _colourPalette.Green, + StatusFieldValue = $":white_check_mark: {statusStr}" + }, + ApplicationStatus.Rejected => new + { + Colour = _colourPalette.Red, + StatusFieldValue = $":no_entry: {statusStr}" + }, + _ => throw new ArgumentOutOfRangeException(nameof(memberApplication)) + }; + + return new Embed + { + Title = $"Member Application #{memberApplication.MemberApplicationId}", + Description = + $"Submitted at {new DiscordTimestamp(memberApplication.AppTime).AsStyled(DiscordTimestampStyle.ShortDateTime)}.", + Fields = new List + { + new + ( + "Author", + $"{memberApplication.AuthorDiscordName} (ID: `{memberApplication.AuthorDiscordId}`)", + false + ), + new("Status", embedConditionalAttributes.StatusFieldValue, false), + new + ( + "Provided details", + $"{memberApplication.MessageContent}\n" + + $"**[Original message (click here)](https://discord.com/channels/{memberApplication.GuildId}/{memberApplication.ChannelId}/{memberApplication.MessageId})**", + false + ) + }, + Colour = embedConditionalAttributes.Colour, + Thumbnail = new EmbedThumbnail(memberApplication.ImageUrl) + }; + } +} \ No newline at end of file diff --git a/src/Mmcc.Bot/Providers/ProvidersSetup.cs b/src/Mmcc.Bot/Providers/ProvidersSetup.cs new file mode 100644 index 0000000..fcd11bb --- /dev/null +++ b/src/Mmcc.Bot/Providers/ProvidersSetup.cs @@ -0,0 +1,33 @@ +using System.Collections.Generic; +using Microsoft.Extensions.DependencyInjection; +using Mmcc.Bot.CommonEmbedProviders; +using Mmcc.Bot.Database.Entities; +using Mmcc.Bot.Providers.CommonEmbedFieldsProviders; +using Mmcc.Bot.Providers.CommonEmbedProviders; + +namespace Mmcc.Bot.Providers; + +/// +/// Extension methods that register Mmcc.Bot.Providers with the service collection. +/// +public static class ProvidersSetup +{ + /// + /// Registers Mmcc.Bot.Providers classes with the service collection. + /// + /// The . + /// The . + public static IServiceCollection AddProviders(this IServiceCollection services) + { + services.AddSingleton, MemberApplicationEmbedProvider>(); + + services + .AddSingleton + < + ICommonEmbedFieldsProvider>, + MemberApplicationsEmbedFieldProvider + >(); + + return services; + } +} \ No newline at end of file From b3898642b083bcc896deef646dd3f4647f4e6b07 Mon Sep 17 00:00:00 2001 From: TraceLD Date: Sat, 12 Nov 2022 02:13:46 +0000 Subject: [PATCH 11/30] C# 11 --- .../Mmcc.Bot.Common.Extensions.csproj | 2 +- src/Mmcc.Bot.Common/Mmcc.Bot.Common.csproj | 2 +- src/Mmcc.Bot.Generators/Mmcc.Bot.Generators.csproj | 2 +- src/Mmcc.Bot.InMemoryStore/Mmcc.Bot.InMemoryStore.csproj | 1 + src/Mmcc.Bot/Commands/Core/MmccInfoCommands.cs | 5 ++++- src/Mmcc.Bot/Mmcc.Bot.csproj | 2 +- 6 files changed, 9 insertions(+), 5 deletions(-) diff --git a/src/Mmcc.Bot.Common.Extensions/Mmcc.Bot.Common.Extensions.csproj b/src/Mmcc.Bot.Common.Extensions/Mmcc.Bot.Common.Extensions.csproj index 817e645..662737d 100644 --- a/src/Mmcc.Bot.Common.Extensions/Mmcc.Bot.Common.Extensions.csproj +++ b/src/Mmcc.Bot.Common.Extensions/Mmcc.Bot.Common.Extensions.csproj @@ -3,7 +3,7 @@ net7.0 enable - 10 + 11 diff --git a/src/Mmcc.Bot.Common/Mmcc.Bot.Common.csproj b/src/Mmcc.Bot.Common/Mmcc.Bot.Common.csproj index 362e5d5..a8bc80c 100644 --- a/src/Mmcc.Bot.Common/Mmcc.Bot.Common.csproj +++ b/src/Mmcc.Bot.Common/Mmcc.Bot.Common.csproj @@ -3,7 +3,7 @@ net7.0 enable - 10 + 11 diff --git a/src/Mmcc.Bot.Generators/Mmcc.Bot.Generators.csproj b/src/Mmcc.Bot.Generators/Mmcc.Bot.Generators.csproj index 2449501..54b236f 100644 --- a/src/Mmcc.Bot.Generators/Mmcc.Bot.Generators.csproj +++ b/src/Mmcc.Bot.Generators/Mmcc.Bot.Generators.csproj @@ -2,7 +2,7 @@ netstandard2.0 - 9.0 + 11 enable diff --git a/src/Mmcc.Bot.InMemoryStore/Mmcc.Bot.InMemoryStore.csproj b/src/Mmcc.Bot.InMemoryStore/Mmcc.Bot.InMemoryStore.csproj index 90a8dec..a02f229 100644 --- a/src/Mmcc.Bot.InMemoryStore/Mmcc.Bot.InMemoryStore.csproj +++ b/src/Mmcc.Bot.InMemoryStore/Mmcc.Bot.InMemoryStore.csproj @@ -4,6 +4,7 @@ net6.0 enable enable + 11 diff --git a/src/Mmcc.Bot/Commands/Core/MmccInfoCommands.cs b/src/Mmcc.Bot/Commands/Core/MmccInfoCommands.cs index 9fead44..d26afcd 100644 --- a/src/Mmcc.Bot/Commands/Core/MmccInfoCommands.cs +++ b/src/Mmcc.Bot/Commands/Core/MmccInfoCommands.cs @@ -51,7 +51,10 @@ public async Task Mmcc() components: new(components) ); } - + + public record WebsiteButton() : ButtonComponent(ButtonComponentStyle.Link, "Website", + new PartialEmoji(new Snowflake(863798570602856469)), URL: MmccUrls.Website); + #if DEBUG [Command("demo")] public async Task Demo() diff --git a/src/Mmcc.Bot/Mmcc.Bot.csproj b/src/Mmcc.Bot/Mmcc.Bot.csproj index 2a40c51..9a2db6e 100644 --- a/src/Mmcc.Bot/Mmcc.Bot.csproj +++ b/src/Mmcc.Bot/Mmcc.Bot.csproj @@ -4,7 +4,7 @@ net7.0 dotnet-Mmcc.Bot-4BA5AD38-B3B4-456A-853F-080768D88F42 enable - 10 + 11 From 9c9e5f43c0f12229d58c574caeccc1b466e07484 Mon Sep 17 00:00:00 2001 From: TraceLD Date: Thu, 22 Dec 2022 10:45:58 +0000 Subject: [PATCH 12/30] stash changes before going to Poland --- .../UI/Buttons/DonateButton.cs | 12 +++ src/Mmcc.Bot.Common/UI/Buttons/ForumButton.cs | 12 +++ .../UI/Buttons/MmccGithubOrgButton.cs | 13 +++ .../UI/Buttons/MmccWebsiteButton.cs | 13 +++ src/Mmcc.Bot.Common/UI/Buttons/WikiButton.cs | 12 +++ ...rs.PolychatRequestResolverGenerator.csproj | 13 +++ .../RequestResolverGenerator.cs | 6 +- .../TemplateGeneratorBase.cs | 5 +- .../Mmcc.Bot.Generators.csproj | 12 --- .../Mmcc.Bot.Polychat.csproj | 5 +- src/Mmcc.Bot.sln | 15 ++-- src/Mmcc.Bot/Commands/CommandsSetup.cs | 1 + .../Commands/Core/MmccInfoCommands.cs | 82 ------------------- .../Commands/MmccInfo/MmccInfo.view.cs | 27 ++++++ .../Commands/MmccInfo/MmccInfoCommands.cs | 21 +++++ src/Mmcc.Bot/Mmcc.Bot.csproj | 3 + src/Mmcc.Bot/Program.cs | 2 + 17 files changed, 146 insertions(+), 108 deletions(-) create mode 100644 src/Mmcc.Bot.Common/UI/Buttons/DonateButton.cs create mode 100644 src/Mmcc.Bot.Common/UI/Buttons/ForumButton.cs create mode 100644 src/Mmcc.Bot.Common/UI/Buttons/MmccGithubOrgButton.cs create mode 100644 src/Mmcc.Bot.Common/UI/Buttons/MmccWebsiteButton.cs create mode 100644 src/Mmcc.Bot.Common/UI/Buttons/WikiButton.cs create mode 100644 src/Mmcc.Bot.Generators/Mmcc.Bot.Generators.PolychatRequestResolverGenerator/Mmcc.Bot.Generators.PolychatRequestResolverGenerator.csproj rename src/Mmcc.Bot.Generators/{ => Mmcc.Bot.Generators.PolychatRequestResolverGenerator}/RequestResolverGenerator.cs (98%) rename src/Mmcc.Bot.Generators/{ => Mmcc.Bot.Generators.PolychatRequestResolverGenerator}/TemplateGeneratorBase.cs (95%) delete mode 100644 src/Mmcc.Bot.Generators/Mmcc.Bot.Generators.csproj delete mode 100644 src/Mmcc.Bot/Commands/Core/MmccInfoCommands.cs create mode 100644 src/Mmcc.Bot/Commands/MmccInfo/MmccInfo.view.cs create mode 100644 src/Mmcc.Bot/Commands/MmccInfo/MmccInfoCommands.cs diff --git a/src/Mmcc.Bot.Common/UI/Buttons/DonateButton.cs b/src/Mmcc.Bot.Common/UI/Buttons/DonateButton.cs new file mode 100644 index 0000000..739b907 --- /dev/null +++ b/src/Mmcc.Bot.Common/UI/Buttons/DonateButton.cs @@ -0,0 +1,12 @@ +using Mmcc.Bot.Common.Statics; +using Remora.Discord.API.Abstractions.Objects; +using Remora.Discord.API.Objects; + +namespace Mmcc.Bot.Common.UI.Buttons; + +public record DonateButton() : ButtonComponent( + ButtonComponentStyle.Link, + "Donate", + new PartialEmoji(Name: "❤️"), + URL: MmccUrls.Donations +); \ No newline at end of file diff --git a/src/Mmcc.Bot.Common/UI/Buttons/ForumButton.cs b/src/Mmcc.Bot.Common/UI/Buttons/ForumButton.cs new file mode 100644 index 0000000..f1ea7cb --- /dev/null +++ b/src/Mmcc.Bot.Common/UI/Buttons/ForumButton.cs @@ -0,0 +1,12 @@ +using Mmcc.Bot.Common.Statics; +using Remora.Discord.API.Abstractions.Objects; +using Remora.Discord.API.Objects; + +namespace Mmcc.Bot.Common.UI.Buttons; + +public record ForumButton() : ButtonComponent( + ButtonComponentStyle.Link, + "Forum", + new PartialEmoji(Name: "🗣️"), + URL: MmccUrls.Forum +); \ No newline at end of file diff --git a/src/Mmcc.Bot.Common/UI/Buttons/MmccGithubOrgButton.cs b/src/Mmcc.Bot.Common/UI/Buttons/MmccGithubOrgButton.cs new file mode 100644 index 0000000..597d929 --- /dev/null +++ b/src/Mmcc.Bot.Common/UI/Buttons/MmccGithubOrgButton.cs @@ -0,0 +1,13 @@ +using Mmcc.Bot.Common.Statics; +using Remora.Discord.API.Abstractions.Objects; +using Remora.Discord.API.Objects; +using Remora.Rest.Core; + +namespace Mmcc.Bot.Common.UI.Buttons; + +public record MmccGithubOrgButton() : ButtonComponent( + ButtonComponentStyle.Link, + "GitHub", + new PartialEmoji(new Snowflake(453413238638641163)), + URL: MmccUrls.GitHub +); \ No newline at end of file diff --git a/src/Mmcc.Bot.Common/UI/Buttons/MmccWebsiteButton.cs b/src/Mmcc.Bot.Common/UI/Buttons/MmccWebsiteButton.cs new file mode 100644 index 0000000..af159f8 --- /dev/null +++ b/src/Mmcc.Bot.Common/UI/Buttons/MmccWebsiteButton.cs @@ -0,0 +1,13 @@ +using Mmcc.Bot.Common.Statics; +using Remora.Discord.API.Abstractions.Objects; +using Remora.Discord.API.Objects; +using Remora.Rest.Core; + +namespace Mmcc.Bot.Common.UI.Buttons; + +public record MmccWebsiteButton() : ButtonComponent( + ButtonComponentStyle.Link, + "Website", + new PartialEmoji(new Snowflake(863798570602856469)), + URL: MmccUrls.Website +); \ No newline at end of file diff --git a/src/Mmcc.Bot.Common/UI/Buttons/WikiButton.cs b/src/Mmcc.Bot.Common/UI/Buttons/WikiButton.cs new file mode 100644 index 0000000..77cdcc5 --- /dev/null +++ b/src/Mmcc.Bot.Common/UI/Buttons/WikiButton.cs @@ -0,0 +1,12 @@ +using Mmcc.Bot.Common.Statics; +using Remora.Discord.API.Abstractions.Objects; +using Remora.Discord.API.Objects; + +namespace Mmcc.Bot.Common.UI.Buttons; + +public record WikiButton() : ButtonComponent( + ButtonComponentStyle.Link, + "Wiki", + new PartialEmoji(Name: "📖"), + URL: MmccUrls.Wiki +); \ No newline at end of file diff --git a/src/Mmcc.Bot.Generators/Mmcc.Bot.Generators.PolychatRequestResolverGenerator/Mmcc.Bot.Generators.PolychatRequestResolverGenerator.csproj b/src/Mmcc.Bot.Generators/Mmcc.Bot.Generators.PolychatRequestResolverGenerator/Mmcc.Bot.Generators.PolychatRequestResolverGenerator.csproj new file mode 100644 index 0000000..e4fac13 --- /dev/null +++ b/src/Mmcc.Bot.Generators/Mmcc.Bot.Generators.PolychatRequestResolverGenerator/Mmcc.Bot.Generators.PolychatRequestResolverGenerator.csproj @@ -0,0 +1,13 @@ + + + + netstandard2.0 + enable + enable + 11 + + + + + + diff --git a/src/Mmcc.Bot.Generators/RequestResolverGenerator.cs b/src/Mmcc.Bot.Generators/Mmcc.Bot.Generators.PolychatRequestResolverGenerator/RequestResolverGenerator.cs similarity index 98% rename from src/Mmcc.Bot.Generators/RequestResolverGenerator.cs rename to src/Mmcc.Bot.Generators/Mmcc.Bot.Generators.PolychatRequestResolverGenerator/RequestResolverGenerator.cs index 98e70e3..4bc3c62 100644 --- a/src/Mmcc.Bot.Generators/RequestResolverGenerator.cs +++ b/src/Mmcc.Bot.Generators/Mmcc.Bot.Generators.PolychatRequestResolverGenerator/RequestResolverGenerator.cs @@ -1,10 +1,8 @@ -using System.Collections.Generic; -using System.Linq; -using System.Text; +using System.Text; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp.Syntax; -namespace Mmcc.Bot.Generators +namespace Mmcc.Bot.Generators.PolychatRequestResolverGenerator { [Generator] public class RequestResolverGenerator : ISourceGenerator diff --git a/src/Mmcc.Bot.Generators/TemplateGeneratorBase.cs b/src/Mmcc.Bot.Generators/Mmcc.Bot.Generators.PolychatRequestResolverGenerator/TemplateGeneratorBase.cs similarity index 95% rename from src/Mmcc.Bot.Generators/TemplateGeneratorBase.cs rename to src/Mmcc.Bot.Generators/Mmcc.Bot.Generators.PolychatRequestResolverGenerator/TemplateGeneratorBase.cs index 4761ff5..ab65194 100644 --- a/src/Mmcc.Bot.Generators/TemplateGeneratorBase.cs +++ b/src/Mmcc.Bot.Generators/Mmcc.Bot.Generators.PolychatRequestResolverGenerator/TemplateGeneratorBase.cs @@ -1,7 +1,6 @@ -using System.Linq; -using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis; -namespace Mmcc.Bot.Generators +namespace Mmcc.Bot.Generators.PolychatRequestResolverGenerator { /// /// Represents a base class to be inherited by template generators. diff --git a/src/Mmcc.Bot.Generators/Mmcc.Bot.Generators.csproj b/src/Mmcc.Bot.Generators/Mmcc.Bot.Generators.csproj deleted file mode 100644 index 54b236f..0000000 --- a/src/Mmcc.Bot.Generators/Mmcc.Bot.Generators.csproj +++ /dev/null @@ -1,12 +0,0 @@ - - - - netstandard2.0 - 11 - enable - - - - - - diff --git a/src/Mmcc.Bot.Polychat/Mmcc.Bot.Polychat.csproj b/src/Mmcc.Bot.Polychat/Mmcc.Bot.Polychat.csproj index 3b12f81..c2b7272 100644 --- a/src/Mmcc.Bot.Polychat/Mmcc.Bot.Polychat.csproj +++ b/src/Mmcc.Bot.Polychat/Mmcc.Bot.Polychat.csproj @@ -29,7 +29,10 @@ - + diff --git a/src/Mmcc.Bot.sln b/src/Mmcc.Bot.sln index 57d7cb5..1390c55 100644 --- a/src/Mmcc.Bot.sln +++ b/src/Mmcc.Bot.sln @@ -7,8 +7,6 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Mmcc.Bot", "Mmcc.Bot\Mmcc.B EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Mmcc.Bot.Database", "Mmcc.Bot.Database\Mmcc.Bot.Database.csproj", "{7AA2B74C-B606-4D5F-A6A2-0CBD7213F204}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Mmcc.Bot.Generators", "Mmcc.Bot.Generators\Mmcc.Bot.Generators.csproj", "{7E9B771E-2800-45F2-B201-DE51B33647B1}" -EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Mmcc.Bot.RemoraAbstractions", "Mmcc.Bot.RemoraAbstractions\Mmcc.Bot.RemoraAbstractions.csproj", "{A5862FD7-B26A-4219-B31F-25256BEF4D75}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Mmcc.Bot.Polychat", "Mmcc.Bot.Polychat\Mmcc.Bot.Polychat.csproj", "{AD6B79E6-93AA-4F69-81FE-3228A29B680B}" @@ -23,6 +21,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Mmcc.Bot.Common", "Mmcc.Bot EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Mmcc.Bot.InMemoryStore", "Mmcc.Bot.InMemoryStore\Mmcc.Bot.InMemoryStore.csproj", "{0BC009CA-0BA9-465A-B8A0-31E7F506F62F}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Mmcc.Bot.Generators", "Mmcc.Bot.Generators", "{A6ABA01C-861E-414D-B6D4-4AEE8A843490}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Mmcc.Bot.Generators.PolychatRequestResolverGenerator", "Mmcc.Bot.Generators\Mmcc.Bot.Generators.PolychatRequestResolverGenerator\Mmcc.Bot.Generators.PolychatRequestResolverGenerator.csproj", "{9FDC16C5-DBB4-4B53-9894-A3FDD5351922}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -37,10 +39,6 @@ Global {7AA2B74C-B606-4D5F-A6A2-0CBD7213F204}.Debug|Any CPU.Build.0 = Debug|Any CPU {7AA2B74C-B606-4D5F-A6A2-0CBD7213F204}.Release|Any CPU.ActiveCfg = Release|Any CPU {7AA2B74C-B606-4D5F-A6A2-0CBD7213F204}.Release|Any CPU.Build.0 = Release|Any CPU - {7E9B771E-2800-45F2-B201-DE51B33647B1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {7E9B771E-2800-45F2-B201-DE51B33647B1}.Debug|Any CPU.Build.0 = Debug|Any CPU - {7E9B771E-2800-45F2-B201-DE51B33647B1}.Release|Any CPU.ActiveCfg = Release|Any CPU - {7E9B771E-2800-45F2-B201-DE51B33647B1}.Release|Any CPU.Build.0 = Release|Any CPU {A5862FD7-B26A-4219-B31F-25256BEF4D75}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {A5862FD7-B26A-4219-B31F-25256BEF4D75}.Debug|Any CPU.Build.0 = Debug|Any CPU {A5862FD7-B26A-4219-B31F-25256BEF4D75}.Release|Any CPU.ActiveCfg = Release|Any CPU @@ -65,6 +63,10 @@ Global {0BC009CA-0BA9-465A-B8A0-31E7F506F62F}.Debug|Any CPU.Build.0 = Debug|Any CPU {0BC009CA-0BA9-465A-B8A0-31E7F506F62F}.Release|Any CPU.ActiveCfg = Release|Any CPU {0BC009CA-0BA9-465A-B8A0-31E7F506F62F}.Release|Any CPU.Build.0 = Release|Any CPU + {9FDC16C5-DBB4-4B53-9894-A3FDD5351922}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {9FDC16C5-DBB4-4B53-9894-A3FDD5351922}.Debug|Any CPU.Build.0 = Debug|Any CPU + {9FDC16C5-DBB4-4B53-9894-A3FDD5351922}.Release|Any CPU.ActiveCfg = Release|Any CPU + {9FDC16C5-DBB4-4B53-9894-A3FDD5351922}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -75,5 +77,6 @@ Global GlobalSection(NestedProjects) = preSolution {A11CD52A-365B-4E2F-AF61-3352C6D52032} = {194115DD-FBE6-4E5C-9DED-28F15B9011B6} {10014CD6-C13A-4245-A218-498066327B53} = {194115DD-FBE6-4E5C-9DED-28F15B9011B6} + {9FDC16C5-DBB4-4B53-9894-A3FDD5351922} = {A6ABA01C-861E-414D-B6D4-4AEE8A843490} EndGlobalSection EndGlobal diff --git a/src/Mmcc.Bot/Commands/CommandsSetup.cs b/src/Mmcc.Bot/Commands/CommandsSetup.cs index ccc3017..80914a1 100644 --- a/src/Mmcc.Bot/Commands/CommandsSetup.cs +++ b/src/Mmcc.Bot/Commands/CommandsSetup.cs @@ -4,6 +4,7 @@ using Mmcc.Bot.Commands.Guilds; using Mmcc.Bot.Commands.Minecraft; using Mmcc.Bot.Commands.Minecraft.Restarts; +using Mmcc.Bot.Commands.MmccInfo; using Mmcc.Bot.Commands.Moderation; using Mmcc.Bot.Commands.Moderation.Bans; using Mmcc.Bot.Commands.Moderation.MemberApplications; diff --git a/src/Mmcc.Bot/Commands/Core/MmccInfoCommands.cs b/src/Mmcc.Bot/Commands/Core/MmccInfoCommands.cs deleted file mode 100644 index d26afcd..0000000 --- a/src/Mmcc.Bot/Commands/Core/MmccInfoCommands.cs +++ /dev/null @@ -1,82 +0,0 @@ -using System.Collections.Generic; -using System.ComponentModel; -using System.Linq; -using System.Threading.Tasks; -using Mmcc.Bot.Common.Statics; -using Mmcc.Bot.RemoraAbstractions.Services; -using Remora.Commands.Attributes; -using Remora.Commands.Groups; -using Remora.Discord.API.Abstractions.Objects; -using Remora.Discord.API.Abstractions.Rest; -using Remora.Discord.API.Objects; -using Remora.Discord.Commands.Contexts; -using Remora.Discord.Interactivity; -using Remora.Rest.Core; -using Remora.Results; - -namespace Mmcc.Bot.Commands.Core; - -public class MmccInfoCommands : CommandGroup -{ - private readonly IDiscordRestChannelAPI _channelApi; - private readonly MessageContext _context; - private readonly HelpService _helpService; - - public MmccInfoCommands(IDiscordRestChannelAPI channelApi, MessageContext context, HelpService helpService) - { - _channelApi = channelApi; - _context = context; - _helpService = helpService; - } - - [Command("mmcc")] - [Description("Shows useful MMCC links")] - public async Task Mmcc() - { - var components = new List - { - new ActionRowComponent(new List - { - new(ButtonComponentStyle.Link, "Website", new PartialEmoji(new Snowflake(863798570602856469)), URL: MmccUrls.Website), - new(ButtonComponentStyle.Link, "Donate", new PartialEmoji(Name: "❤️"), URL: MmccUrls.Donations), - new(ButtonComponentStyle.Link, "Wiki", new PartialEmoji(Name: "📖"), URL: MmccUrls.Wiki), - new(ButtonComponentStyle.Link, "Forum", new PartialEmoji(Name: "🗣️"), URL: MmccUrls.Forum), - new(ButtonComponentStyle.Link, "GitHub", new PartialEmoji(new Snowflake(453413238638641163)), URL: MmccUrls.GitHub) - }) - }; - - return await _channelApi.CreateMessageAsync( - channelID: _context.ChannelID, - content: "Useful links", - components: new(components) - ); - } - - public record WebsiteButton() : ButtonComponent(ButtonComponentStyle.Link, "Website", - new PartialEmoji(new Snowflake(863798570602856469)), URL: MmccUrls.Website); - -#if DEBUG - [Command("demo")] - public async Task Demo() - { - var components = new List - { - new(new List - { - new - ( - ButtonComponentStyle.Primary, - Label: "Click me!", - CustomID: CustomIDHelpers.CreateButtonID("approve-btn") - ) - }) - }; - - return await _channelApi.CreateMessageAsync( - channelID: _context.ChannelID, - content: "DEMO", - components: new(components) - ); - } -#endif -} \ No newline at end of file diff --git a/src/Mmcc.Bot/Commands/MmccInfo/MmccInfo.view.cs b/src/Mmcc.Bot/Commands/MmccInfo/MmccInfo.view.cs new file mode 100644 index 0000000..dd46852 --- /dev/null +++ b/src/Mmcc.Bot/Commands/MmccInfo/MmccInfo.view.cs @@ -0,0 +1,27 @@ +using Mmcc.Bot.Common.UI.Buttons; +using Porbeagle; +using Porbeagle.Attributes; +using Remora.Rest.Core; + +namespace Mmcc.Bot.Commands.MmccInfo; + +[DiscordView] +public partial record MmccInfoView : IMessageView +{ + public Optional Text { get; init; } = "Useful links"; + + [ActionRow(0)] + private MmccWebsiteButton MmccWebsiteButton { get; } = new(); + + [ActionRow(0)] + private DonateButton DonateButton { get; } = new(); + + [ActionRow(0)] + private WikiButton WikiButton { get; } = new(); + + [ActionRow(0)] + private ForumButton ForumButton { get; } = new(); + + [ActionRow(0)] + private MmccGithubOrgButton MmccGitHubOrgButton { get; } = new(); +} \ No newline at end of file diff --git a/src/Mmcc.Bot/Commands/MmccInfo/MmccInfoCommands.cs b/src/Mmcc.Bot/Commands/MmccInfo/MmccInfoCommands.cs new file mode 100644 index 0000000..50d88d0 --- /dev/null +++ b/src/Mmcc.Bot/Commands/MmccInfo/MmccInfoCommands.cs @@ -0,0 +1,21 @@ +using System.ComponentModel; +using System.Threading.Tasks; +using Porbeagle; +using Remora.Commands.Attributes; +using Remora.Commands.Groups; +using Remora.Results; + +namespace Mmcc.Bot.Commands.MmccInfo; + +public class MmccInfoCommands : CommandGroup +{ + private readonly IContextAwareViewManager _viewManager; + + public MmccInfoCommands(IContextAwareViewManager viewManager) => + _viewManager = viewManager; + + [Command("mmcc")] + [Description("Shows useful MMCC links")] + public async Task Mmcc() + => await _viewManager.RespondWithView(new MmccInfoView()); +} \ No newline at end of file diff --git a/src/Mmcc.Bot/Mmcc.Bot.csproj b/src/Mmcc.Bot/Mmcc.Bot.csproj index 9a2db6e..d967ef8 100644 --- a/src/Mmcc.Bot/Mmcc.Bot.csproj +++ b/src/Mmcc.Bot/Mmcc.Bot.csproj @@ -34,6 +34,9 @@ + + + diff --git a/src/Mmcc.Bot/Program.cs b/src/Mmcc.Bot/Program.cs index 010c816..23fc9f0 100644 --- a/src/Mmcc.Bot/Program.cs +++ b/src/Mmcc.Bot/Program.cs @@ -25,6 +25,7 @@ using Mmcc.Bot.Polychat.Networking; using Mmcc.Bot.Providers; using Mmcc.Bot.RemoraAbstractions; +using Porbeagle; using Remora.Discord.Caching.Extensions; using Remora.Discord.Hosting.Extensions; using Serilog; @@ -74,6 +75,7 @@ services.AddBotGatewayEventResponders(); services.AddDiscordCaching(); services.AddBotBackgroundServices(); + services.AddScoped(); services.AddHangfire(); }) From 88f75ffc24f43667c8e04390419769c38f07c95d Mon Sep 17 00:00:00 2001 From: TraceLD Date: Fri, 24 Mar 2023 15:23:24 +0000 Subject: [PATCH 13/30] stash work --- .../Entities/ModerationActionExtensions.cs | 16 +++++++ .../System/KeyValuePairDiscordExtensions.cs | 15 +++++++ .../Buttons/DonateButton.cs | 0 .../Buttons/ForumButton.cs | 0 .../Buttons/MmccGithubOrgButton.cs | 0 .../Buttons/MmccWebsiteButton.cs | 0 .../Buttons/WikiButton.cs | 0 .../Embeds/NotificationEmbed.cs | 19 ++++++++ .../Mmcc.Bot.Common.UI.csproj | 14 ++++++ src/Mmcc.Bot.Common/Mmcc.Bot.Common.csproj | 5 +++ src/Mmcc.Bot.Common/Models/Notification.cs | 12 ++++++ .../RequestResolverGenerator.cs | 43 ++++++++++--------- src/Mmcc.Bot.sln | 7 +++ .../Commands/Tags/Usage/TagsUsageCommands.cs | 13 ++++++ src/Mmcc.Bot/Mmcc.Bot.csproj | 1 + .../ModerationActionExpiredNotification.cs | 34 +++++++++++++++ src/Mmcc.Bot/Program.cs | 1 - 17 files changed, 158 insertions(+), 22 deletions(-) create mode 100644 src/Mmcc.Bot.Common.Extensions/System/KeyValuePairDiscordExtensions.cs rename src/{Mmcc.Bot.Common/UI => Mmcc.Bot.Common.UI}/Buttons/DonateButton.cs (100%) rename src/{Mmcc.Bot.Common/UI => Mmcc.Bot.Common.UI}/Buttons/ForumButton.cs (100%) rename src/{Mmcc.Bot.Common/UI => Mmcc.Bot.Common.UI}/Buttons/MmccGithubOrgButton.cs (100%) rename src/{Mmcc.Bot.Common/UI => Mmcc.Bot.Common.UI}/Buttons/MmccWebsiteButton.cs (100%) rename src/{Mmcc.Bot.Common/UI => Mmcc.Bot.Common.UI}/Buttons/WikiButton.cs (100%) create mode 100644 src/Mmcc.Bot.Common.UI/Embeds/NotificationEmbed.cs create mode 100644 src/Mmcc.Bot.Common.UI/Mmcc.Bot.Common.UI.csproj create mode 100644 src/Mmcc.Bot.Common/Models/Notification.cs create mode 100644 src/Mmcc.Bot/Notifications/Moderation/ModerationActionExpiredNotification.cs diff --git a/src/Mmcc.Bot.Common.Extensions/Database/Entities/ModerationActionExtensions.cs b/src/Mmcc.Bot.Common.Extensions/Database/Entities/ModerationActionExtensions.cs index c26e845..d3c16dc 100644 --- a/src/Mmcc.Bot.Common.Extensions/Database/Entities/ModerationActionExtensions.cs +++ b/src/Mmcc.Bot.Common.Extensions/Database/Entities/ModerationActionExtensions.cs @@ -29,6 +29,22 @@ public static IEnumerable GetEmbedFields(this IEnumerable ma switch + { + { UserDiscordId: { } dId, UserIgn: { } ign } => + $$""" + Discord user: <@{{dId}}> + IGN: `{{ign}}` + """, + + { UserDiscordId: { } dId } => $"Discord user: <@{dId}>", + + { UserIgn: { } ign } => $"IGN: `{ign}`", + + _ => "No Discord ID/IGN data." + }; + private static EmbedField GetEmbedFieldForActionsOfType(this IEnumerable moderationActions, ModerationActionType type, bool showAssociatedDiscord, bool showAssociatedIgn) { var list = moderationActions.Where(ma => ma.ModerationActionType == type).ToList(); diff --git a/src/Mmcc.Bot.Common.Extensions/System/KeyValuePairDiscordExtensions.cs b/src/Mmcc.Bot.Common.Extensions/System/KeyValuePairDiscordExtensions.cs new file mode 100644 index 0000000..1533c4b --- /dev/null +++ b/src/Mmcc.Bot.Common.Extensions/System/KeyValuePairDiscordExtensions.cs @@ -0,0 +1,15 @@ +using System.Collections.Generic; +using System.Linq; +using Remora.Discord.API.Abstractions.Objects; +using Remora.Discord.API.Objects; + +namespace Mmcc.Bot.Common.Extensions.System; + +public static class KeyValuePairDiscordExtensions +{ + public static IEnumerable ToEmbedFields(this IEnumerable> kvEnumerable) + => kvEnumerable.Select(x => new EmbedField(x.Key, x.Value, false)); + + public static EmbedField ToEmbedField(this KeyValuePair kv) + => new(kv.Key, kv.Value, false); +} \ No newline at end of file diff --git a/src/Mmcc.Bot.Common/UI/Buttons/DonateButton.cs b/src/Mmcc.Bot.Common.UI/Buttons/DonateButton.cs similarity index 100% rename from src/Mmcc.Bot.Common/UI/Buttons/DonateButton.cs rename to src/Mmcc.Bot.Common.UI/Buttons/DonateButton.cs diff --git a/src/Mmcc.Bot.Common/UI/Buttons/ForumButton.cs b/src/Mmcc.Bot.Common.UI/Buttons/ForumButton.cs similarity index 100% rename from src/Mmcc.Bot.Common/UI/Buttons/ForumButton.cs rename to src/Mmcc.Bot.Common.UI/Buttons/ForumButton.cs diff --git a/src/Mmcc.Bot.Common/UI/Buttons/MmccGithubOrgButton.cs b/src/Mmcc.Bot.Common.UI/Buttons/MmccGithubOrgButton.cs similarity index 100% rename from src/Mmcc.Bot.Common/UI/Buttons/MmccGithubOrgButton.cs rename to src/Mmcc.Bot.Common.UI/Buttons/MmccGithubOrgButton.cs diff --git a/src/Mmcc.Bot.Common/UI/Buttons/MmccWebsiteButton.cs b/src/Mmcc.Bot.Common.UI/Buttons/MmccWebsiteButton.cs similarity index 100% rename from src/Mmcc.Bot.Common/UI/Buttons/MmccWebsiteButton.cs rename to src/Mmcc.Bot.Common.UI/Buttons/MmccWebsiteButton.cs diff --git a/src/Mmcc.Bot.Common/UI/Buttons/WikiButton.cs b/src/Mmcc.Bot.Common.UI/Buttons/WikiButton.cs similarity index 100% rename from src/Mmcc.Bot.Common/UI/Buttons/WikiButton.cs rename to src/Mmcc.Bot.Common.UI/Buttons/WikiButton.cs diff --git a/src/Mmcc.Bot.Common.UI/Embeds/NotificationEmbed.cs b/src/Mmcc.Bot.Common.UI/Embeds/NotificationEmbed.cs new file mode 100644 index 0000000..1d83fd3 --- /dev/null +++ b/src/Mmcc.Bot.Common.UI/Embeds/NotificationEmbed.cs @@ -0,0 +1,19 @@ +using Mmcc.Bot.Common.Extensions.System; +using Mmcc.Bot.Common.Models; +using Remora.Discord.API.Abstractions.Objects; +using Remora.Discord.API.Objects; +using Remora.Rest.Core; + +namespace Mmcc.Bot.Common.UI.Embeds; + +public record NotificationEmbed : Embed +{ + public NotificationEmbed(Notification context) : base( + Title: context.Title, + Description: context.Description ?? new Optional(), + Timestamp: context.Timestamp ?? new Optional(), + Fields: context.CustomProperties?.ToEmbedFields().ToList() ?? new Optional>() + ) + { + } +} \ No newline at end of file diff --git a/src/Mmcc.Bot.Common.UI/Mmcc.Bot.Common.UI.csproj b/src/Mmcc.Bot.Common.UI/Mmcc.Bot.Common.UI.csproj new file mode 100644 index 0000000..363e860 --- /dev/null +++ b/src/Mmcc.Bot.Common.UI/Mmcc.Bot.Common.UI.csproj @@ -0,0 +1,14 @@ + + + + net7.0 + enable + enable + + + + + + + + diff --git a/src/Mmcc.Bot.Common/Mmcc.Bot.Common.csproj b/src/Mmcc.Bot.Common/Mmcc.Bot.Common.csproj index a8bc80c..2d0da02 100644 --- a/src/Mmcc.Bot.Common/Mmcc.Bot.Common.csproj +++ b/src/Mmcc.Bot.Common/Mmcc.Bot.Common.csproj @@ -10,6 +10,11 @@ + + + + + diff --git a/src/Mmcc.Bot.Common/Models/Notification.cs b/src/Mmcc.Bot.Common/Models/Notification.cs new file mode 100644 index 0000000..2647183 --- /dev/null +++ b/src/Mmcc.Bot.Common/Models/Notification.cs @@ -0,0 +1,12 @@ +using System; +using System.Collections.Generic; +using MediatR; + +namespace Mmcc.Bot.Common.Models; + +public record Notification( + string Title, + string? Description, + DateTimeOffset? Timestamp, + IReadOnlyList>? CustomProperties +) : INotification; diff --git a/src/Mmcc.Bot.Generators/Mmcc.Bot.Generators.PolychatRequestResolverGenerator/RequestResolverGenerator.cs b/src/Mmcc.Bot.Generators/Mmcc.Bot.Generators.PolychatRequestResolverGenerator/RequestResolverGenerator.cs index 4bc3c62..d6c8f4a 100644 --- a/src/Mmcc.Bot.Generators/Mmcc.Bot.Generators.PolychatRequestResolverGenerator/RequestResolverGenerator.cs +++ b/src/Mmcc.Bot.Generators/Mmcc.Bot.Generators.PolychatRequestResolverGenerator/RequestResolverGenerator.cs @@ -77,20 +77,21 @@ List polychatMessageClasses private string AnnotateMsgType(string type) => $"global::{MSG_NAMESPACE}.{type}"; - protected override string FillInStub(string generatedFiller) - => $@"// auto-generated -namespace {GENERATED_NAMESPACE}; - -#pragma warning disable CS0612 // type is obsolete -public partial class {GENERATED_CLASS} -{{ - public {AnnotateTypeWithGlobal(_polychatRequestInterfaceSymbol)}? Resolve() - {{ -{string.Join("\n", generatedFiller.Split('\n').Select(s => Indent(s, 2)))} - }} -}} -#pragma warning restore CS0612 -"; + protected override string FillInStub(string generatedFiller) => + $$""" + // auto-generated + namespace {{GENERATED_NAMESPACE}}; + + #pragma warning disable CS0612 // type is obsolete + public partial class {{GENERATED_CLASS}} + { + public {{AnnotateTypeWithGlobal(_polychatRequestInterfaceSymbol)}}? Resolve() + { + {{string.Join("\n", generatedFiller.Split('\n').Select(s => Indent(s, 2)))}} + } + } + #pragma warning restore CS0612 + """; protected override string GenerateFiller() { @@ -113,13 +114,13 @@ protected override string GenerateFiller() return sb.ToString(); } - private static string GenerateNoClassesStub() - => new StringBuilder() - .AppendLine("// no Polychat classes found;") - .AppendLine("// once classes are added to the Polychat project generated code will go here;") - .AppendLine() - .AppendLine("return null;") - .ToString(); + private static string GenerateNoClassesStub() => + """ + // no Polychat classes found; + // once classes are added to the Polychat project generated code will go here; + + return null; + """; private string GenerateIfForMsgClass(string messageType) => new StringBuilder() diff --git a/src/Mmcc.Bot.sln b/src/Mmcc.Bot.sln index 1390c55..2d9adea 100644 --- a/src/Mmcc.Bot.sln +++ b/src/Mmcc.Bot.sln @@ -25,6 +25,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Mmcc.Bot.Generators", "Mmcc EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Mmcc.Bot.Generators.PolychatRequestResolverGenerator", "Mmcc.Bot.Generators\Mmcc.Bot.Generators.PolychatRequestResolverGenerator\Mmcc.Bot.Generators.PolychatRequestResolverGenerator.csproj", "{9FDC16C5-DBB4-4B53-9894-A3FDD5351922}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Mmcc.Bot.Common.UI", "Mmcc.Bot.Common.UI\Mmcc.Bot.Common.UI.csproj", "{95A40405-B933-4C00-B13E-CA7B7362F160}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -67,6 +69,10 @@ Global {9FDC16C5-DBB4-4B53-9894-A3FDD5351922}.Debug|Any CPU.Build.0 = Debug|Any CPU {9FDC16C5-DBB4-4B53-9894-A3FDD5351922}.Release|Any CPU.ActiveCfg = Release|Any CPU {9FDC16C5-DBB4-4B53-9894-A3FDD5351922}.Release|Any CPU.Build.0 = Release|Any CPU + {95A40405-B933-4C00-B13E-CA7B7362F160}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {95A40405-B933-4C00-B13E-CA7B7362F160}.Debug|Any CPU.Build.0 = Debug|Any CPU + {95A40405-B933-4C00-B13E-CA7B7362F160}.Release|Any CPU.ActiveCfg = Release|Any CPU + {95A40405-B933-4C00-B13E-CA7B7362F160}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -78,5 +84,6 @@ Global {A11CD52A-365B-4E2F-AF61-3352C6D52032} = {194115DD-FBE6-4E5C-9DED-28F15B9011B6} {10014CD6-C13A-4245-A218-498066327B53} = {194115DD-FBE6-4E5C-9DED-28F15B9011B6} {9FDC16C5-DBB4-4B53-9894-A3FDD5351922} = {A6ABA01C-861E-414D-B6D4-4AEE8A843490} + {95A40405-B933-4C00-B13E-CA7B7362F160} = {194115DD-FBE6-4E5C-9DED-28F15B9011B6} EndGlobalSection EndGlobal diff --git a/src/Mmcc.Bot/Commands/Tags/Usage/TagsUsageCommands.cs b/src/Mmcc.Bot/Commands/Tags/Usage/TagsUsageCommands.cs index d37e6df..f0f27cb 100644 --- a/src/Mmcc.Bot/Commands/Tags/Usage/TagsUsageCommands.cs +++ b/src/Mmcc.Bot/Commands/Tags/Usage/TagsUsageCommands.cs @@ -1,6 +1,8 @@ using System.ComponentModel; using System.Threading.Tasks; using MediatR; +using Mmcc.Bot.Database.Entities; +using Mmcc.Bot.Notifications.Moderation; using Mmcc.Bot.RemoraAbstractions.Services.MessageResponders; using Remora.Commands.Attributes; using Remora.Commands.Groups; @@ -51,4 +53,15 @@ await _responder.Respond(e.Content), {IsSuccess: false} res => res }; } + + [Command("tester")] + [Description("Sends a given tag.")] + public async Task SendTagg(string tagName) + { + await _mediator.Publish( + new ModerationActionExpiredNotification(new ModerationAction(ModerationActionType.Ban, 0, true, "asas", 0, + null, "fasdfdsafsd"))); + + return Result.FromSuccess(); + } } \ No newline at end of file diff --git a/src/Mmcc.Bot/Mmcc.Bot.csproj b/src/Mmcc.Bot/Mmcc.Bot.csproj index d967ef8..c9494aa 100644 --- a/src/Mmcc.Bot/Mmcc.Bot.csproj +++ b/src/Mmcc.Bot/Mmcc.Bot.csproj @@ -29,6 +29,7 @@ + diff --git a/src/Mmcc.Bot/Notifications/Moderation/ModerationActionExpiredNotification.cs b/src/Mmcc.Bot/Notifications/Moderation/ModerationActionExpiredNotification.cs new file mode 100644 index 0000000..136b4cd --- /dev/null +++ b/src/Mmcc.Bot/Notifications/Moderation/ModerationActionExpiredNotification.cs @@ -0,0 +1,34 @@ +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using MediatR; +using Mmcc.Bot.Common.Extensions.Database.Entities; +using Mmcc.Bot.Common.Models; +using Mmcc.Bot.Database.Entities; + +namespace Mmcc.Bot.Notifications.Moderation; + +public record ModerationActionExpiredNotification : Notification +{ + public ModerationActionExpiredNotification(ModerationAction ma) : base( + $"Moderation action with ID: {ma.ModerationActionId} has expired.", + "Moderation action has expired and has therefore been deactivated.", + DateTimeOffset.UtcNow, + new List> + { + new("Action type", ma.ModerationActionType.ToStringWithEmoji()), + new("User info", ma.GetUserDataDisplayString()) + } + ) + { + } +} + +public class DiscordNotificationHandler : INotificationHandler +{ + public async Task Handle(ModerationActionExpiredNotification notification, CancellationToken cancellationToken) + { + Console.WriteLine("a"); + } +} \ No newline at end of file diff --git a/src/Mmcc.Bot/Program.cs b/src/Mmcc.Bot/Program.cs index 23fc9f0..9e66a12 100644 --- a/src/Mmcc.Bot/Program.cs +++ b/src/Mmcc.Bot/Program.cs @@ -62,7 +62,6 @@ services.AddTransient(typeof(IPipelineBehavior<,>), typeof(ValidationBehaviour<,>)); // Mmcc.Bot.X projects; - services.AddMojangApi(); services.AddPolychat(hostContext.Configuration.GetSection("Ssmp")); services.AddProviders(); From 109b26ce9beb20a2a0d217fac5e1fc63c8ac6449 Mon Sep 17 00:00:00 2001 From: TraceLD Date: Fri, 31 Mar 2023 21:48:53 +0100 Subject: [PATCH 14/30] add notifications system --- .../Embeds/NotificationEmbed.cs | 2 +- ...ExcludeFromMediatrAssemblyScanAttribute.cs | 8 +++ .../Models/IDiscordNotifiable.cs | 8 +++ .../Models/IMmccNotification.cs | 13 ++++ src/Mmcc.Bot.Common/Models/Notification.cs | 12 ---- .../Commands/Moderation/Bans/Unban.cs | 61 +++++++--------- .../Commands/Tags/Usage/TagsUsageCommands.cs | 6 +- .../Moderation/ModerationBackgroundService.cs | 69 ++----------------- .../DiscordNotificationHandler.cs | 65 +++++++++++++++++ .../ModerationActionExpiredNotification.cs | 42 ++++++----- src/Mmcc.Bot/Program.cs | 11 ++- 11 files changed, 154 insertions(+), 143 deletions(-) create mode 100644 src/Mmcc.Bot.Common/ExcludeFromMediatrAssemblyScanAttribute.cs create mode 100644 src/Mmcc.Bot.Common/Models/IDiscordNotifiable.cs create mode 100644 src/Mmcc.Bot.Common/Models/IMmccNotification.cs delete mode 100644 src/Mmcc.Bot.Common/Models/Notification.cs create mode 100644 src/Mmcc.Bot/Notifications/DiscordNotificationHandler.cs diff --git a/src/Mmcc.Bot.Common.UI/Embeds/NotificationEmbed.cs b/src/Mmcc.Bot.Common.UI/Embeds/NotificationEmbed.cs index 1d83fd3..7f5a65e 100644 --- a/src/Mmcc.Bot.Common.UI/Embeds/NotificationEmbed.cs +++ b/src/Mmcc.Bot.Common.UI/Embeds/NotificationEmbed.cs @@ -8,7 +8,7 @@ namespace Mmcc.Bot.Common.UI.Embeds; public record NotificationEmbed : Embed { - public NotificationEmbed(Notification context) : base( + public NotificationEmbed(IMmccNotification context) : base( Title: context.Title, Description: context.Description ?? new Optional(), Timestamp: context.Timestamp ?? new Optional(), diff --git a/src/Mmcc.Bot.Common/ExcludeFromMediatrAssemblyScanAttribute.cs b/src/Mmcc.Bot.Common/ExcludeFromMediatrAssemblyScanAttribute.cs new file mode 100644 index 0000000..89a3b44 --- /dev/null +++ b/src/Mmcc.Bot.Common/ExcludeFromMediatrAssemblyScanAttribute.cs @@ -0,0 +1,8 @@ +using System; + +namespace Mmcc.Bot.Common; + +[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Interface)] +public class ExcludeFromMediatrAssemblyScanAttribute : Attribute +{ +} diff --git a/src/Mmcc.Bot.Common/Models/IDiscordNotifiable.cs b/src/Mmcc.Bot.Common/Models/IDiscordNotifiable.cs new file mode 100644 index 0000000..d72cccf --- /dev/null +++ b/src/Mmcc.Bot.Common/Models/IDiscordNotifiable.cs @@ -0,0 +1,8 @@ +using Remora.Rest.Core; + +namespace Mmcc.Bot.Common.Models; + +public interface IDiscordNotifiable +{ + public Snowflake TargetGuildId { get; } +} \ No newline at end of file diff --git a/src/Mmcc.Bot.Common/Models/IMmccNotification.cs b/src/Mmcc.Bot.Common/Models/IMmccNotification.cs new file mode 100644 index 0000000..aca2049 --- /dev/null +++ b/src/Mmcc.Bot.Common/Models/IMmccNotification.cs @@ -0,0 +1,13 @@ +using System; +using System.Collections.Generic; +using MediatR; + +namespace Mmcc.Bot.Common.Models; + +public interface IMmccNotification : INotification +{ + public string Title { get; } + public string? Description { get; } + public DateTimeOffset? Timestamp { get; } + IReadOnlyList>? CustomProperties { get; } +} \ No newline at end of file diff --git a/src/Mmcc.Bot.Common/Models/Notification.cs b/src/Mmcc.Bot.Common/Models/Notification.cs deleted file mode 100644 index 2647183..0000000 --- a/src/Mmcc.Bot.Common/Models/Notification.cs +++ /dev/null @@ -1,12 +0,0 @@ -using System; -using System.Collections.Generic; -using MediatR; - -namespace Mmcc.Bot.Common.Models; - -public record Notification( - string Title, - string? Description, - DateTimeOffset? Timestamp, - IReadOnlyList>? CustomProperties -) : INotification; diff --git a/src/Mmcc.Bot/Commands/Moderation/Bans/Unban.cs b/src/Mmcc.Bot/Commands/Moderation/Bans/Unban.cs index 4e924e8..1d13cc3 100644 --- a/src/Mmcc.Bot/Commands/Moderation/Bans/Unban.cs +++ b/src/Mmcc.Bot/Commands/Moderation/Bans/Unban.cs @@ -4,7 +4,9 @@ using FluentValidation; using MediatR; using Microsoft.Extensions.Logging; +using Mmcc.Bot.Common.Extensions.Remora.Discord.API.Abstractions.Rest; using Mmcc.Bot.Common.Models.Colours; +using Mmcc.Bot.Common.Models.Settings; using Mmcc.Bot.Common.Statics; using Mmcc.Bot.Database; using Mmcc.Bot.Database.Entities; @@ -23,34 +25,17 @@ namespace Mmcc.Bot.Commands.Moderation.Bans; /// public class Unban { - /// - /// Command to unban a user. - /// public class Command : IRequest> { - /// - /// Moderation action. - /// public ModerationAction ModerationAction { get; set; } = null!; - - /// - /// ID of the channel to which polychat2 will send the confirmation message. - /// - public Snowflake ChannelId { get; set; } } - - /// - /// Validates the . - /// + public class Validator : AbstractValidator { public Validator() { RuleFor(c => c.ModerationAction) .NotNull(); - - RuleFor(c => c.ChannelId) - .NotNull(); } } @@ -64,17 +49,8 @@ public class Handler : IRequestHandler> private readonly IDiscordRestChannelAPI _channelApi; private readonly IColourPalette _colourPalette; private readonly ILogger _logger; - - /// - /// Instantiates a new instance of class. - /// - /// The DB context. - /// The polychat service. - /// The guild API. - /// The user API. - /// The channel API. - /// The colour palette. - /// The logger. + private readonly DiscordSettings _discordSettings; + public Handler( BotContext context, IPolychatService ps, @@ -82,7 +58,8 @@ public Handler( IDiscordRestUserAPI userApi, IDiscordRestChannelAPI channelApi, IColourPalette colourPalette, - ILogger logger + ILogger logger, + DiscordSettings discordSettings ) { _context = context; @@ -92,6 +69,7 @@ ILogger logger _channelApi = channelApi; _colourPalette = colourPalette; _logger = logger; + _discordSettings = discordSettings; } /// @@ -99,18 +77,25 @@ public async Task> Handle(Command request, Cancellation { var ma = request.ModerationAction; if (ma.ModerationActionType != ModerationActionType.Ban) - return new UnsupportedArgumentError( - $"Wrong moderation action type. Expected: {ModerationActionType.Ban}, got: {ma.ModerationActionType}"); - //if (!ma.IsActive) return new ValidationError("Moderation action is already inactive."); + return new UnsupportedArgumentError($"Wrong moderation action type. Expected: {ModerationActionType.Ban}, got: {ma.ModerationActionType}"); + if (!ma.IsActive) + return new UnsupportedArgumentError("Moderation action is already inactive."); + if (ma.UserIgn is not null) { + var getLogsChannel = await _guildApi.FindGuildChannelByName(new(ma.GuildId), _discordSettings.ChannelNames.ModerationLogs); + if (!getLogsChannel.IsSuccess) + { + _logger.LogError("An error has occurred while obtaining logs channel."); + } + var proto = new GenericCommand { - DefaultCommand = "ban", - DiscordCommandName = "ban", - DiscordChannelId = request.ChannelId.ToString(), - Args = {request.ModerationAction.UserIgn} + DiscordCommandName = "exec", + DefaultCommand = "$args", + Args = { "pardon", ma.UserIgn }, + DiscordChannelId = getLogsChannel.Entity?.ID.ToString() }; await _ps.BroadcastMessage(proto); } @@ -140,7 +125,7 @@ public async Task> Handle(Command request, Cancellation var createDmResult = await _userApi.CreateDMAsync(userDiscordIdSnowflake, cancellationToken); const string warningMsg = "Failed to send a DM notification to the user. It may be because they have blocked the bot or don't share any servers. This warning can in most cases be ignored."; - if (!createDmResult.IsSuccess || createDmResult.Entity is null) + if (!createDmResult.IsSuccess) { _logger.LogWarning(warningMsg); } diff --git a/src/Mmcc.Bot/Commands/Tags/Usage/TagsUsageCommands.cs b/src/Mmcc.Bot/Commands/Tags/Usage/TagsUsageCommands.cs index f0f27cb..4c19dbe 100644 --- a/src/Mmcc.Bot/Commands/Tags/Usage/TagsUsageCommands.cs +++ b/src/Mmcc.Bot/Commands/Tags/Usage/TagsUsageCommands.cs @@ -58,9 +58,9 @@ await _responder.Respond(e.Content), [Description("Sends a given tag.")] public async Task SendTagg(string tagName) { - await _mediator.Publish( - new ModerationActionExpiredNotification(new ModerationAction(ModerationActionType.Ban, 0, true, "asas", 0, - null, "fasdfdsafsd"))); + await _mediator.Publish(new ModerationActionExpiredNotification(new ModerationAction(ModerationActionType.Ban, + 0, true, "asas", 0, + null, "fasdfdsafsd"))); return Result.FromSuccess(); } diff --git a/src/Mmcc.Bot/Hosting/Moderation/ModerationBackgroundService.cs b/src/Mmcc.Bot/Hosting/Moderation/ModerationBackgroundService.cs index 91f7794..273498e 100644 --- a/src/Mmcc.Bot/Hosting/Moderation/ModerationBackgroundService.cs +++ b/src/Mmcc.Bot/Hosting/Moderation/ModerationBackgroundService.cs @@ -1,22 +1,15 @@ using System; -using System.Collections.Generic; -using System.Text; using System.Threading; using System.Threading.Tasks; using MediatR; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Mmcc.Bot.Commands.Moderation.Bans; -using Mmcc.Bot.Common.Extensions.Database.Entities; -using Mmcc.Bot.Common.Extensions.Remora.Discord.API.Abstractions.Rest; using Mmcc.Bot.Common.Hosting; using Mmcc.Bot.Common.Models.Colours; using Mmcc.Bot.Common.Models.Settings; using Mmcc.Bot.Database.Entities; -using Remora.Discord.API.Abstractions.Rest; -using Remora.Discord.API.Objects; using Remora.Discord.Commands.Results; -using Remora.Rest.Core; using Remora.Results; namespace Mmcc.Bot.Hosting.Moderation; @@ -53,15 +46,13 @@ protected override async Task OnExecute(CancellationToken ct) using var scope = _sp.CreateScope(); var provider = scope.ServiceProvider; - var guildApi = provider.GetRequiredService(); var mediator = provider.GetRequiredService(); - var channelApi = provider.GetRequiredService(); - var getAllPendingResult = await mediator.Send(new GetExpiredActions.Query(), ct); + var getAllPendingResult = await mediator.Send(new GetExpiredActions.Query(), ct); if (!getAllPendingResult.IsSuccess) { _logger.LogError( - "An error has occurred while running an iteration of the {Service} timed background service:\n{Error}", + "An error has occurred while getting expired modification actions as part of hosted service: {HostedServiceName}:\n{Error}", nameof(ModerationBackgroundService), getAllPendingResult.Error ); @@ -72,22 +63,9 @@ protected override async Task OnExecute(CancellationToken ct) foreach (var ma in actionsToDeactivate) { - var getLogsChannel = await guildApi.FindGuildChannelByName(new Snowflake(ma.GuildId), - _discordSettings.ChannelNames.ModerationLogs); - if (!getLogsChannel.IsSuccess) - { - _logger.LogError( - "An error has occurred while running an iteration of the {Service} timed background service:\n{Error}", - nameof(ModerationBackgroundService), - getLogsChannel.Error - ); - break; - } - - Result unbanResult = ma.ModerationActionType switch + var unbanResult = ma.ModerationActionType switch { - ModerationActionType.Ban => await mediator.Send(new Unban.Command - { ModerationAction = ma, ChannelId = getLogsChannel.Entity.ID }, ct), + ModerationActionType.Ban => await mediator.Send(new Unban.Command { ModerationAction = ma }, ct), _ => Result.FromError(new UnsupportedFeatureError("Unsupported moderation type.")) }; @@ -101,45 +79,6 @@ protected override async Task OnExecute(CancellationToken ct) break; } - var typeString = ma.ModerationActionType.ToStringWithEmoji(); - var userSb = new StringBuilder(); - - if (ma.UserDiscordId is not null) - { - userSb.AppendLine($"Discord user: <@{ma.UserDiscordId}>"); - } - - if (ma.UserIgn is not null) - { - userSb.AppendLine($"IGN: `{ma.UserIgn}`"); - } - - var notificationEmbed = new Embed - { - Title = $"Moderation action with ID: {ma.ModerationActionId} has expired.", - Description = "Moderation action has expired and has therefore been deactivated.", - Colour = _colourPalette.Green, - Fields = new List - { - new("Action type", typeString, false), - new("User info", userSb.ToString(), false) - }, - Timestamp = DateTimeOffset.UtcNow - }; - var sendNotificationResult = await channelApi.CreateMessageAsync(getLogsChannel.Entity.ID, - embeds: new[] { notificationEmbed }, ct: ct); - if (!sendNotificationResult.IsSuccess) - { - _logger.LogWarning( - "Successfully deactivated expired moderation action with ID: {Id} but failed to send a notification to the logs channel." + - "It may be because the bot doesn't have permissions in that channel or has since been removed from the guild. This warning can in most cases be ignored." + - "The error was:\n{Error}", - ma.ModerationActionId, - sendNotificationResult.Error - ); - break; - } - _logger.LogInformation( "Successfully deactivated expired moderation action with ID: {Id}", ma.ModerationActionId); } diff --git a/src/Mmcc.Bot/Notifications/DiscordNotificationHandler.cs b/src/Mmcc.Bot/Notifications/DiscordNotificationHandler.cs new file mode 100644 index 0000000..31bb99e --- /dev/null +++ b/src/Mmcc.Bot/Notifications/DiscordNotificationHandler.cs @@ -0,0 +1,65 @@ +using System.Threading; +using System.Threading.Tasks; +using MediatR; +using Microsoft.Extensions.Logging; +using Mmcc.Bot.Common; +using Mmcc.Bot.Common.Extensions.Remora.Discord.API.Abstractions.Rest; +using Mmcc.Bot.Common.Models; +using Mmcc.Bot.Common.Models.Settings; +using Mmcc.Bot.Common.UI.Embeds; +using Remora.Discord.API.Abstractions.Rest; + +namespace Mmcc.Bot.Notifications; + +[ExcludeFromMediatrAssemblyScan] +public class DiscordNotificationHandler : INotificationHandler + where TNotification : IMmccNotification, IDiscordNotifiable +{ + private readonly ILogger> _logger; + private readonly DiscordSettings _discordSettings; + private readonly IDiscordRestChannelAPI _channelApi; + private readonly IDiscordRestGuildAPI _guildApi; + + public DiscordNotificationHandler( + ILogger> logger, + DiscordSettings discordSettings, + IDiscordRestChannelAPI channelApi, + IDiscordRestGuildAPI guildApi + ) + { + _logger = logger; + _discordSettings = discordSettings; + _channelApi = channelApi; + _guildApi = guildApi; + } + + public async Task Handle(TNotification notification, CancellationToken cancellationToken) + { + // TODO: cache this; + var logsChannelName = _discordSettings.ChannelNames.ModerationLogs; + var getLogsChannel = await _guildApi.FindGuildChannelByName(notification.TargetGuildId, logsChannelName); + if (!getLogsChannel.IsSuccess) + { + _logger.LogError( + "An error has occurred while running an iteration of the {Service} timed background service:\n{Error}", + nameof(DiscordNotificationHandler), + getLogsChannel.Error + ); + } + + var notificationEmbed = new NotificationEmbed(notification); + var sendNotificationResult = await _channelApi.CreateMessageAsync(getLogsChannel.Entity.ID, + embeds: new[] { notificationEmbed }, ct: cancellationToken); + if (!sendNotificationResult.IsSuccess) + { + _logger.LogWarning( + "Successfully deactivated expired moderation action but failed to send a notification to the logs channel {LogsChannelName} in guild {GuildId}." + + "It may be because the bot doesn't have permissions in that channel or has since been removed from the guild. This warning can in most cases be ignored." + + "The error was:\n{Error}", + logsChannelName, + notification.TargetGuildId, + sendNotificationResult.Error + ); + } + } +} \ No newline at end of file diff --git a/src/Mmcc.Bot/Notifications/Moderation/ModerationActionExpiredNotification.cs b/src/Mmcc.Bot/Notifications/Moderation/ModerationActionExpiredNotification.cs index 136b4cd..5d60e12 100644 --- a/src/Mmcc.Bot/Notifications/Moderation/ModerationActionExpiredNotification.cs +++ b/src/Mmcc.Bot/Notifications/Moderation/ModerationActionExpiredNotification.cs @@ -1,34 +1,32 @@ using System; using System.Collections.Generic; -using System.Threading; -using System.Threading.Tasks; -using MediatR; using Mmcc.Bot.Common.Extensions.Database.Entities; using Mmcc.Bot.Common.Models; using Mmcc.Bot.Database.Entities; +using Remora.Rest.Core; namespace Mmcc.Bot.Notifications.Moderation; -public record ModerationActionExpiredNotification : Notification +public record ModerationActionExpiredNotification( + string Title, + string? Description, + DateTimeOffset? Timestamp, + IReadOnlyList>? CustomProperties, + Snowflake TargetGuildId +) : IMmccNotification, IDiscordNotifiable { - public ModerationActionExpiredNotification(ModerationAction ma) : base( - $"Moderation action with ID: {ma.ModerationActionId} has expired.", - "Moderation action has expired and has therefore been deactivated.", - DateTimeOffset.UtcNow, - new List> - { - new("Action type", ma.ModerationActionType.ToStringWithEmoji()), - new("User info", ma.GetUserDataDisplayString()) - } - ) + public ModerationActionExpiredNotification(ModerationAction ma) + : this( + Title: $"Moderation action with ID: {ma.ModerationActionId} has expired.", + Description: "Moderation action has expired and has therefore been deactivated.", + Timestamp: DateTimeOffset.UtcNow, + TargetGuildId: new(ma.GuildId), + CustomProperties: new List> + { + new("Action type", ma.ModerationActionType.ToStringWithEmoji()), + new("User info", ma.GetUserDataDisplayString()) + } + ) { } -} - -public class DiscordNotificationHandler : INotificationHandler -{ - public async Task Handle(ModerationActionExpiredNotification notification, CancellationToken cancellationToken) - { - Console.WriteLine("a"); - } } \ No newline at end of file diff --git a/src/Mmcc.Bot/Program.cs b/src/Mmcc.Bot/Program.cs index 9e66a12..a1d91c3 100644 --- a/src/Mmcc.Bot/Program.cs +++ b/src/Mmcc.Bot/Program.cs @@ -1,4 +1,5 @@ using System; +using System.Reflection; using FluentValidation; using MediatR; using Microsoft.Extensions.DependencyInjection; @@ -7,6 +8,7 @@ using Mmcc.Bot; using Mmcc.Bot.Behaviours; using Mmcc.Bot.Commands; +using Mmcc.Bot.Common; using Mmcc.Bot.Common.Extensions.Hosting; using Mmcc.Bot.Common.Models.Colours; using Mmcc.Bot.Common.Models.Settings; @@ -20,7 +22,7 @@ using Mmcc.Bot.InMemoryStore.Stores; using Mmcc.Bot.Interactions; using Mmcc.Bot.Middleware; -using Mmcc.Bot.Mojang; +using Mmcc.Bot.Notifications; using Mmcc.Bot.Polychat; using Mmcc.Bot.Polychat.Networking; using Mmcc.Bot.Providers; @@ -58,8 +60,13 @@ services.AddAppInsights(hostContext); // MediatR; - services.AddMediatR(typeof(CreateFromDiscordMessage), typeof(PolychatRequest<>)); + services.AddMediatR(new [] { typeof(CreateFromDiscordMessage), typeof(PolychatRequest<>) }, cfg => + { + cfg.WithEvaluator(t => t.GetCustomAttribute() is null); + }); + services.AddTransient(typeof(IPipelineBehavior<,>), typeof(ValidationBehaviour<,>)); + services.AddTransient(typeof(INotificationHandler<>), typeof(DiscordNotificationHandler<>)); // Mmcc.Bot.X projects; services.AddPolychat(hostContext.Configuration.GetSection("Ssmp")); From 04d3c90ec05214ad567eff382e09d7b8427ce6bf Mon Sep 17 00:00:00 2001 From: TraceLD Date: Fri, 31 Mar 2023 21:50:30 +0100 Subject: [PATCH 15/30] port GeneralModerationCommands to new notifications system --- src/Mmcc.Bot/Commands/Moderation/GeneralModerationCommands.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/Mmcc.Bot/Commands/Moderation/GeneralModerationCommands.cs b/src/Mmcc.Bot/Commands/Moderation/GeneralModerationCommands.cs index f76c5ad..2e8f19e 100644 --- a/src/Mmcc.Bot/Commands/Moderation/GeneralModerationCommands.cs +++ b/src/Mmcc.Bot/Commands/Moderation/GeneralModerationCommands.cs @@ -108,8 +108,7 @@ public async Task Deactivate(int id) Result deactivateResult = getAppResult.Entity.ModerationActionType switch { - ModerationActionType.Ban => await _mediator.Send(new Unban.Command - { ModerationAction = getAppResult.Entity, ChannelId = _context.ChannelID }), + ModerationActionType.Ban => await _mediator.Send(new Unban.Command { ModerationAction = getAppResult.Entity }), _ => Result.FromError(new UnsupportedFeatureError("Unsupported moderation type.")) }; From c5d38b12df1962c91931a7bf0ed675cf2348392a Mon Sep 17 00:00:00 2001 From: TraceLD Date: Mon, 3 Apr 2023 01:53:57 +0100 Subject: [PATCH 16/30] bump packages --- src/Mmcc.Bot.Database/Mmcc.Bot.Database.csproj | 12 ++++++------ .../Mmcc.Bot.InMemoryStore.csproj | 2 +- src/Mmcc.Bot.Polychat/Mmcc.Bot.Polychat.csproj | 2 +- src/Mmcc.Bot/Mmcc.Bot.csproj | 4 ++-- 4 files changed, 10 insertions(+), 10 deletions(-) diff --git a/src/Mmcc.Bot.Database/Mmcc.Bot.Database.csproj b/src/Mmcc.Bot.Database/Mmcc.Bot.Database.csproj index befe1a2..eb2a63e 100644 --- a/src/Mmcc.Bot.Database/Mmcc.Bot.Database.csproj +++ b/src/Mmcc.Bot.Database/Mmcc.Bot.Database.csproj @@ -7,15 +7,15 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive - - - - - + + + + + diff --git a/src/Mmcc.Bot.InMemoryStore/Mmcc.Bot.InMemoryStore.csproj b/src/Mmcc.Bot.InMemoryStore/Mmcc.Bot.InMemoryStore.csproj index a02f229..9702445 100644 --- a/src/Mmcc.Bot.InMemoryStore/Mmcc.Bot.InMemoryStore.csproj +++ b/src/Mmcc.Bot.InMemoryStore/Mmcc.Bot.InMemoryStore.csproj @@ -8,7 +8,7 @@ - + diff --git a/src/Mmcc.Bot.Polychat/Mmcc.Bot.Polychat.csproj b/src/Mmcc.Bot.Polychat/Mmcc.Bot.Polychat.csproj index c2b7272..63a278a 100644 --- a/src/Mmcc.Bot.Polychat/Mmcc.Bot.Polychat.csproj +++ b/src/Mmcc.Bot.Polychat/Mmcc.Bot.Polychat.csproj @@ -17,7 +17,7 @@ - + diff --git a/src/Mmcc.Bot/Mmcc.Bot.csproj b/src/Mmcc.Bot/Mmcc.Bot.csproj index c9494aa..28d050a 100644 --- a/src/Mmcc.Bot/Mmcc.Bot.csproj +++ b/src/Mmcc.Bot/Mmcc.Bot.csproj @@ -1,4 +1,4 @@ - + net7.0 @@ -15,7 +15,7 @@ - + From 9c20451cfe5b277fd9676e06674c50d28ae51273 Mon Sep 17 00:00:00 2001 From: TraceLD Date: Mon, 3 Apr 2023 03:45:15 +0100 Subject: [PATCH 17/30] Port Diagnostics and Guilds features to the new View-based GUI system --- .../Models/Colours/ColourPalette.cs | 17 +++ .../Models/Colours/TailwindColourPalette.cs | 18 --- .../Models/Settings/DiagnosticsSettings.cs | 21 +++ src/Mmcc.Bot/Commands/CommandsSetup.cs | 6 +- .../Diagnostics/DiagnosticsCommands.cs | 137 ------------------ .../Diagnostics/PingNetworkResource.cs | 85 ----------- src/Mmcc.Bot/CoreSetup/ConfigurationSetup.cs | 2 + .../Diagnostics/DiagnosticsCommands.cs | 52 +++++++ .../Diagnostics/GetDrivesDiagnostics.cs} | 12 +- .../PingAllNetworkResourcesToCheck.cs | 61 ++++++++ .../Diagnostics/Views/BotDiagnostics.view.cs | 68 +++++++++ .../Views/DrivesDiagnostics.view.cs | 58 ++++++++ .../Guilds/GetGuildInfo.cs | 2 +- .../Guilds/GetInviteLink.cs | 2 +- .../Guilds/GuildCommands.cs | 52 ++----- .../Features/Guilds/Views/GuildInfo.view.cs | 54 +++++++ .../Core => Features/Help}/HelpCommands.cs | 2 +- src/Mmcc.Bot/Mmcc.Bot.csproj | 2 +- src/Mmcc.Bot/Program.cs | 2 +- 19 files changed, 358 insertions(+), 295 deletions(-) create mode 100644 src/Mmcc.Bot.Common/Models/Colours/ColourPalette.cs delete mode 100644 src/Mmcc.Bot.Common/Models/Colours/TailwindColourPalette.cs create mode 100644 src/Mmcc.Bot.Common/Models/Settings/DiagnosticsSettings.cs delete mode 100644 src/Mmcc.Bot/Commands/Diagnostics/DiagnosticsCommands.cs delete mode 100644 src/Mmcc.Bot/Commands/Diagnostics/PingNetworkResource.cs create mode 100644 src/Mmcc.Bot/Features/Diagnostics/DiagnosticsCommands.cs rename src/Mmcc.Bot/{Commands/Diagnostics/GetDrives.cs => Features/Diagnostics/GetDrivesDiagnostics.cs} (89%) create mode 100644 src/Mmcc.Bot/Features/Diagnostics/PingAllNetworkResourcesToCheck.cs create mode 100644 src/Mmcc.Bot/Features/Diagnostics/Views/BotDiagnostics.view.cs create mode 100644 src/Mmcc.Bot/Features/Diagnostics/Views/DrivesDiagnostics.view.cs rename src/Mmcc.Bot/{Commands => Features}/Guilds/GetGuildInfo.cs (98%) rename src/Mmcc.Bot/{Commands => Features}/Guilds/GetInviteLink.cs (98%) rename src/Mmcc.Bot/{Commands => Features}/Guilds/GuildCommands.cs (50%) create mode 100644 src/Mmcc.Bot/Features/Guilds/Views/GuildInfo.view.cs rename src/Mmcc.Bot/{Commands/Core => Features/Help}/HelpCommands.cs (97%) diff --git a/src/Mmcc.Bot.Common/Models/Colours/ColourPalette.cs b/src/Mmcc.Bot.Common/Models/Colours/ColourPalette.cs new file mode 100644 index 0000000..448a4fb --- /dev/null +++ b/src/Mmcc.Bot.Common/Models/Colours/ColourPalette.cs @@ -0,0 +1,17 @@ +using System.Drawing; + +namespace Mmcc.Bot.Common.Models.Colours; + +// TODO: Remove all usages of IColourPalette; +public static class ColourPalette +{ + public static Color Black => ColorTranslator.FromHtml("#262626"); + public static Color Gray => ColorTranslator.FromHtml("#6B7280"); + public static Color Red => ColorTranslator.FromHtml("#EF4444"); + public static Color Yellow => ColorTranslator.FromHtml("#F59E0B"); + public static Color Green => ColorTranslator.FromHtml("#10B981"); + public static Color Blue => ColorTranslator.FromHtml("#3B82F6"); + public static Color Indigo => ColorTranslator.FromHtml("#6366F1"); + public static Color Purple => ColorTranslator.FromHtml("#8B5CF6"); + public static Color Pink => ColorTranslator.FromHtml("#EC4899"); +} \ No newline at end of file diff --git a/src/Mmcc.Bot.Common/Models/Colours/TailwindColourPalette.cs b/src/Mmcc.Bot.Common/Models/Colours/TailwindColourPalette.cs deleted file mode 100644 index baf2a5c..0000000 --- a/src/Mmcc.Bot.Common/Models/Colours/TailwindColourPalette.cs +++ /dev/null @@ -1,18 +0,0 @@ -using System.Drawing; - -namespace Mmcc.Bot.Common.Models.Colours -{ - /// - public class TailwindColourPalette : IColourPalette - { - public Color Black => ColorTranslator.FromHtml("#262626"); - public Color Gray => ColorTranslator.FromHtml("#6B7280"); - public Color Red => ColorTranslator.FromHtml("#EF4444"); - public Color Yellow => ColorTranslator.FromHtml("#F59E0B"); - public Color Green => ColorTranslator.FromHtml("#10B981"); - public Color Blue => ColorTranslator.FromHtml("#3B82F6"); - public Color Indigo => ColorTranslator.FromHtml("#6366F1"); - public Color Purple => ColorTranslator.FromHtml("#8B5CF6"); - public Color Pink => ColorTranslator.FromHtml("#EC4899"); - } -} \ No newline at end of file diff --git a/src/Mmcc.Bot.Common/Models/Settings/DiagnosticsSettings.cs b/src/Mmcc.Bot.Common/Models/Settings/DiagnosticsSettings.cs new file mode 100644 index 0000000..9548f67 --- /dev/null +++ b/src/Mmcc.Bot.Common/Models/Settings/DiagnosticsSettings.cs @@ -0,0 +1,21 @@ +using System.Collections.Generic; + +namespace Mmcc.Bot.Common.Models.Settings; + +public interface IDiagnosticsSettings +{ + int Timeout { get; } + IReadOnlyDictionary NetworkResourcesToCheck { get; } +} + +public class DiagnosticsSettings : IDiagnosticsSettings +{ + public int Timeout { get; } = 120; + + public IReadOnlyDictionary NetworkResourcesToCheck { get; } = new Dictionary + { + ["Discord"] = "discord.com", + ["Mojang API"] = "api.mojang.com", + ["MMCC"] = "s4.moddedminecraft.club" + }.AsReadOnly(); +} \ No newline at end of file diff --git a/src/Mmcc.Bot/Commands/CommandsSetup.cs b/src/Mmcc.Bot/Commands/CommandsSetup.cs index 80914a1..130afc6 100644 --- a/src/Mmcc.Bot/Commands/CommandsSetup.cs +++ b/src/Mmcc.Bot/Commands/CommandsSetup.cs @@ -1,7 +1,4 @@ using Microsoft.Extensions.DependencyInjection; -using Mmcc.Bot.Commands.Core; -using Mmcc.Bot.Commands.Diagnostics; -using Mmcc.Bot.Commands.Guilds; using Mmcc.Bot.Commands.Minecraft; using Mmcc.Bot.Commands.Minecraft.Restarts; using Mmcc.Bot.Commands.MmccInfo; @@ -12,6 +9,9 @@ using Mmcc.Bot.Commands.Moderation.Warns; using Mmcc.Bot.Commands.Tags.Management; using Mmcc.Bot.Commands.Tags.Usage; +using Mmcc.Bot.Features.Diagnostics; +using Mmcc.Bot.Features.Guilds; +using Mmcc.Bot.Features.Help; using Remora.Commands.Extensions; using Remora.Discord.Commands.Extensions; diff --git a/src/Mmcc.Bot/Commands/Diagnostics/DiagnosticsCommands.cs b/src/Mmcc.Bot/Commands/Diagnostics/DiagnosticsCommands.cs deleted file mode 100644 index e4ed3e8..0000000 --- a/src/Mmcc.Bot/Commands/Diagnostics/DiagnosticsCommands.cs +++ /dev/null @@ -1,137 +0,0 @@ -using System; -using System.Collections.Generic; -using System.ComponentModel; -using System.Net.NetworkInformation; -using System.Text; -using System.Threading.Tasks; -using MediatR; -using Mmcc.Bot.Common.Models.Colours; -using Mmcc.Bot.Common.Statics; -using Mmcc.Bot.RemoraAbstractions.Conditions; -using Mmcc.Bot.RemoraAbstractions.Conditions.CommandSpecific; -using Mmcc.Bot.RemoraAbstractions.Services.MessageResponders; -using Remora.Commands.Attributes; -using Remora.Commands.Groups; -using Remora.Discord.API.Abstractions.Objects; -using Remora.Discord.API.Objects; -using Remora.Results; - -namespace Mmcc.Bot.Commands.Diagnostics; - -/// -/// Diagnostics commands. -/// -[Group("diagnostics")] -[Description("Server and bot diagnostics")] -public class DiagnosticsCommands : CommandGroup -{ - private readonly IColourPalette _colourPalette; - private readonly IMediator _mediator; - private readonly CommandMessageResponder _responder; - - private readonly Dictionary _resourcesToCheck = new() - { - ["Discord"] = "discord.com", - ["Mojang API"] = "api.mojang.com", - ["MMCC"] = "s4.moddedminecraft.club" - }; - - /// - /// Instantiates a new instance of . - /// - /// The colour palette. - /// The mediator. - /// The command responder. - public DiagnosticsCommands( - IColourPalette colourPalette, - IMediator mediator, - CommandMessageResponder responder - ) - { - _colourPalette = colourPalette; - _mediator = mediator; - _responder = responder; - } - - /// - /// Show status of the bot and APIs it uses. - /// - /// Result of the operation. - [Command("bot")] - [Description("Show status of the bot and APIs it uses")] - public async Task BotDiagnostics() - { - var fields = new List - { - new("Bot status", ":green_circle: Operational", false) - }; - - foreach (var (name, address) in _resourcesToCheck) - { - var pingResult = await _mediator.Send(new PingNetworkResource.Query {Address = address}); - var fieldVal = !pingResult.IsSuccess || pingResult.Entity.Status != IPStatus.Success - ? ":x: Could not reach." - : pingResult.Entity.RoundtripTime switch - { - <= 50 => ":green_circle: ", - <= 120 => ":yellow_circle: ", - _ => ":red_circle: " - } + pingResult.Entity.RoundtripTime + " ms"; - - fields.Add(new($"{name} Status", fieldVal, false)); - } - - var embed = new Embed - { - Title = "Bot diagnostics", - Description = "Information about the status of the bot and the APIs it uses", - Fields = fields, - Timestamp = DateTimeOffset.UtcNow, - Colour = _colourPalette.Green - }; - return await _responder.Respond(embed); - } - - /// - /// Shows drives info - including free space. - /// - /// Result of the operation. - [Command("drives")] - [Description("Shows drives info (including free space)")] - [RequireGuild] - [RequireUserGuildPermission(DiscordPermission.BanMembers)] - public async Task DrivesDiagnostics() - { - var embedFields = new List(); - var drives = await _mediator.Send(new GetDrives.Query()); - - foreach (var d in drives) - { - var fieldValue = new StringBuilder(); - var spaceEmoji = d.PercentageUsed switch - { - <= 65 => ":green_circle:", - <= 85 => ":yellow_circle:", - _ => ":red_circle:" - }; - - fieldValue.AppendLine($"Volume label: {d.Label}"); - fieldValue.AppendLine($"File system: {d.DriveFormat}"); - fieldValue.AppendLine($"Available space: {spaceEmoji} {d.GigabytesFree:0.00} GB ({d.PercentageUsed:0.00}% used)"); - fieldValue.AppendLine($"Total size: {d.GigabytesTotalSize:0.00} GB"); - - embedFields.Add(new EmbedField(d.Name, fieldValue.ToString(), false)); - } - - var embed = new Embed - { - Title = "Drives diagnostics", - Colour = _colourPalette.Blue, - Thumbnail = EmbedProperties.MmccLogoThumbnail, - Footer = new EmbedFooter("Dedicated server"), - Timestamp = DateTimeOffset.UtcNow, - Fields = embedFields - }; - return await _responder.Respond(embed); - } -} \ No newline at end of file diff --git a/src/Mmcc.Bot/Commands/Diagnostics/PingNetworkResource.cs b/src/Mmcc.Bot/Commands/Diagnostics/PingNetworkResource.cs deleted file mode 100644 index 8a12d68..0000000 --- a/src/Mmcc.Bot/Commands/Diagnostics/PingNetworkResource.cs +++ /dev/null @@ -1,85 +0,0 @@ -using System; -using System.Net.NetworkInformation; -using System.Threading; -using System.Threading.Tasks; -using FluentValidation; -using MediatR; -using Remora.Results; - -namespace Mmcc.Bot.Commands.Diagnostics; - -/// -/// Pings a network resource. -/// -public class PingNetworkResource -{ - /// - /// Query to ping a network resource. - /// - public class Query : IRequest> - { - /// - /// Address of the network resource to ping. - /// - public string Address { get; set; } = null!; - } - - public class Validator : AbstractValidator - { - public Validator() - { - RuleFor(q => q.Address) - .NotEmpty(); - } - } - - public class QueryResult - { - /// - /// Address of the pinged network resource. - /// - public string Address { get; set; } = null!; - - /// - /// Status. - /// - public IPStatus Status { get; set; } - - /// - /// Roundtrip time in milliseconds. - /// - public long? RoundtripTime { get; set; } - } - - /// - public class Handler : IRequestHandler> - { - private const int Timeout = 120; - - /// - public async Task> Handle(Query request, CancellationToken cancellationToken) - { - try - { - var ping = new Ping(); - var options = new PingOptions - { - DontFragment = true - }; - var buffer = new byte[32]; - var reply = await ping.SendPingAsync(request.Address, Timeout, buffer, options); - - return Result.FromSuccess(new() - { - Address = reply.Address.ToString(), - Status = reply.Status, - RoundtripTime = reply.RoundtripTime, - }); - } - catch (Exception e) - { - return e; - } - } - } -} \ No newline at end of file diff --git a/src/Mmcc.Bot/CoreSetup/ConfigurationSetup.cs b/src/Mmcc.Bot/CoreSetup/ConfigurationSetup.cs index 510af7f..f6ad45f 100644 --- a/src/Mmcc.Bot/CoreSetup/ConfigurationSetup.cs +++ b/src/Mmcc.Bot/CoreSetup/ConfigurationSetup.cs @@ -25,6 +25,8 @@ public static IServiceCollection ConfigureBot( HostBuilderContext hostContext ) { + services.AddScoped(); + // add command line args config; services.AddSingleton(); diff --git a/src/Mmcc.Bot/Features/Diagnostics/DiagnosticsCommands.cs b/src/Mmcc.Bot/Features/Diagnostics/DiagnosticsCommands.cs new file mode 100644 index 0000000..38a05e0 --- /dev/null +++ b/src/Mmcc.Bot/Features/Diagnostics/DiagnosticsCommands.cs @@ -0,0 +1,52 @@ +using System.ComponentModel; +using System.Threading.Tasks; +using MediatR; +using Mmcc.Bot.Features.Diagnostics.Views; +using Mmcc.Bot.RemoraAbstractions.Conditions.CommandSpecific; +using Porbeagle; +using Remora.Commands.Attributes; +using Remora.Commands.Groups; +using Remora.Discord.API.Abstractions.Objects; +using Remora.Results; + +namespace Mmcc.Bot.Features.Diagnostics; + +[Group("diagnostics")] +[Description("Server and bot diagnostics")] +public class DiagnosticsCommands : CommandGroup +{ + private readonly IMediator _mediator; + private readonly IContextAwareViewManager _viewManager; + + public DiagnosticsCommands(IMediator mediator, IContextAwareViewManager viewManager) + { + _mediator = mediator; + _viewManager = viewManager; + } + + [Command("bot")] + [Description("Show status of the bot and APIs it uses")] + public async Task BotDiagnostics() + { + var result = await _mediator.Send(new PingAllNetworkResourcesToCheck.Query()); + + return result switch + { + { IsSuccess: true, Entity: { } pingResults } + => await _viewManager.RespondWithView(new BotDiagnosticsView(pingResults)), + + { IsSuccess: false } => result + }; + } + + [Command("drives")] + [Description("Shows drives info (including free space)")] + [RequireGuild] + [RequireUserGuildPermission(DiscordPermission.BanMembers)] + public async Task DrivesDiagnostics() + { + var result = await _mediator.Send(new GetDrivesDiagnostics.Query()); + + return await _viewManager.RespondWithView(new DrivesDiagnosticsView(result)); + } +} \ No newline at end of file diff --git a/src/Mmcc.Bot/Commands/Diagnostics/GetDrives.cs b/src/Mmcc.Bot/Features/Diagnostics/GetDrivesDiagnostics.cs similarity index 89% rename from src/Mmcc.Bot/Commands/Diagnostics/GetDrives.cs rename to src/Mmcc.Bot/Features/Diagnostics/GetDrivesDiagnostics.cs index 9867ec3..3214003 100644 --- a/src/Mmcc.Bot/Commands/Diagnostics/GetDrives.cs +++ b/src/Mmcc.Bot/Features/Diagnostics/GetDrivesDiagnostics.cs @@ -3,12 +3,12 @@ using System.Linq; using MediatR; -namespace Mmcc.Bot.Commands.Diagnostics; +namespace Mmcc.Bot.Features.Diagnostics; /// -/// Query to get drives. +/// Gets the drives diagnostics. /// -public class GetDrives +public class GetDrivesDiagnostics { /// /// Query to get drives. @@ -18,7 +18,7 @@ public class Query : IRequest> } /// - /// Result of the drive query. + /// Drive diagnostics. /// public class QueryResult { @@ -30,11 +30,9 @@ public class QueryResult public double PercentageUsed { get; set; } public float GigabytesTotalSize { get; set; } } - - /// + public class Handler : RequestHandler> { - /// protected override IList Handle(Query request) { var allDrives = DriveInfo.GetDrives(); diff --git a/src/Mmcc.Bot/Features/Diagnostics/PingAllNetworkResourcesToCheck.cs b/src/Mmcc.Bot/Features/Diagnostics/PingAllNetworkResourcesToCheck.cs new file mode 100644 index 0000000..f76a5e0 --- /dev/null +++ b/src/Mmcc.Bot/Features/Diagnostics/PingAllNetworkResourcesToCheck.cs @@ -0,0 +1,61 @@ +using System; +using System.Collections.Generic; +using System.Net.NetworkInformation; +using System.Threading; +using System.Threading.Tasks; +using MediatR; +using Mmcc.Bot.Common.Models.Settings; +using Remora.Results; + +namespace Mmcc.Bot.Features.Diagnostics; + +/// +/// Pings all network resources to check specified in . +/// +public class PingAllNetworkResourcesToCheck +{ + public record struct Query : IRequest>>; + + public record QueryResult(string Name, string Address, IPStatus Status, long? RoundtripTime); + + public class Handler : IRequestHandler>> + { + private readonly IDiagnosticsSettings _settings; + + public Handler(IDiagnosticsSettings settings) + => _settings = settings; + + public async Task>> Handle(Query request, CancellationToken cancellationToken) + { + try + { + var results = new List(_settings.NetworkResourcesToCheck.Count); + foreach (var (name, address) in _settings.NetworkResourcesToCheck) + { + var pingResult = await PingResource(name, address); + + results.Add(pingResult); + } + + return Result>.FromSuccess(results); + } + catch (Exception e) + { + return e; + } + } + + private async Task PingResource(string name, string address) + { + var ping = new Ping(); + var options = new PingOptions + { + DontFragment = true + }; + var buffer = new byte[32]; + var reply = await ping.SendPingAsync(address, _settings.Timeout, buffer, options); + + return new(Name: name, Address: reply.Address.ToString(), Status: reply.Status, RoundtripTime: reply.RoundtripTime); + } + } +} \ No newline at end of file diff --git a/src/Mmcc.Bot/Features/Diagnostics/Views/BotDiagnostics.view.cs b/src/Mmcc.Bot/Features/Diagnostics/Views/BotDiagnostics.view.cs new file mode 100644 index 0000000..20a98e8 --- /dev/null +++ b/src/Mmcc.Bot/Features/Diagnostics/Views/BotDiagnostics.view.cs @@ -0,0 +1,68 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net.NetworkInformation; +using Mmcc.Bot.Common.Models.Colours; +using Porbeagle; +using Porbeagle.Attributes; +using Remora.Discord.API.Abstractions.Objects; +using Remora.Discord.API.Objects; +using Remora.Rest.Core; + +namespace Mmcc.Bot.Features.Diagnostics.Views; + +[DiscordView] +public partial record BotDiagnosticsView : IMessageView +{ + public BotDiagnosticsView(IEnumerable results) + => Embed = new BotDiagnosticsEmbed(results); + + public Optional Text { get; init; } = new(); + + public Embed Embed { get; } +} + +public record BotDiagnosticsEmbed : Embed +{ + public BotDiagnosticsEmbed(IEnumerable results) : base( + Title: "Bot diagnostics", + Description: "Information about the status of the bot and the APIs it uses", + Timestamp: DateTimeOffset.UtcNow, + Colour: ColourPalette.Green, + Fields: GetFields(results) + ) + { + } + + private static Optional> GetFields(IEnumerable results) + { + var fields = new List + { + new EmbedField(Name: "Bot status", Value: ":green_circle: Operational", IsInline: false) + }; + + fields.AddRange(results.Select(x => x.ToEmbedField())); + + return fields; + } +} + +file static class PingResultMapperExtensions +{ + internal static IEmbedField ToEmbedField(this PingAllNetworkResourcesToCheck.QueryResult pingResult) + { + var fieldTextValue = pingResult.Status switch + { + IPStatus.Success => pingResult.RoundtripTime switch + { + <= 50 => ":green_circle: ", + <= 120 => ":yellow_circle: ", + _ => ":red_circle: " + } + pingResult.RoundtripTime + " ms", + + _ => ":x: Could not reach." + }; + + return new EmbedField(Name: $"{pingResult.Name} Status", Value: fieldTextValue, IsInline: false); + } +} \ No newline at end of file diff --git a/src/Mmcc.Bot/Features/Diagnostics/Views/DrivesDiagnostics.view.cs b/src/Mmcc.Bot/Features/Diagnostics/Views/DrivesDiagnostics.view.cs new file mode 100644 index 0000000..93c2cc3 --- /dev/null +++ b/src/Mmcc.Bot/Features/Diagnostics/Views/DrivesDiagnostics.view.cs @@ -0,0 +1,58 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Mmcc.Bot.Common.Models.Colours; +using Mmcc.Bot.Common.Statics; +using Porbeagle; +using Porbeagle.Attributes; +using Remora.Discord.API.Abstractions.Objects; +using Remora.Discord.API.Objects; +using Remora.Rest.Core; + +namespace Mmcc.Bot.Features.Diagnostics.Views; + +[DiscordView] +public partial record DrivesDiagnosticsView : IMessageView +{ + public DrivesDiagnosticsView(IEnumerable results) + => Embed = new DrivesDiagnosticsEmbed(results); + + public Optional Text { get; init; } = new(); + public Embed Embed { get; } +} + +public record DrivesDiagnosticsEmbed : Embed +{ + public DrivesDiagnosticsEmbed(IEnumerable results) : base( + Title: "Drives diagnostics", + Colour: ColourPalette.Blue, + Thumbnail: EmbedProperties.MmccLogoThumbnail, + Footer: new EmbedFooter("Dedicated server"), + Timestamp: DateTimeOffset.UtcNow, + Fields: results.Select(x => x.ToEmbedField()).ToList() + ) + { + } +} + +file static class DriveDiagnosticsMapperExtensions +{ + internal static IEmbedField ToEmbedField(this GetDrivesDiagnostics.QueryResult d) + { + var freeSpaceRemainingEmoji = d.PercentageUsed switch + { + <= 65 => ":green_circle:", + <= 85 => ":yellow_circle:", + _ => ":red_circle:" + }; + + var fieldTextValue = $""" + Volume label: {d.Label} + File system: {d.DriveFormat} + Available space: {freeSpaceRemainingEmoji} {d.GigabytesFree:0.00} GB ({d.PercentageUsed:0.00}% used) + Total size: {d.GigabytesTotalSize:0.00} GB + """; + + return new EmbedField(Name: d.Name, Value: fieldTextValue, IsInline: false); + } +} diff --git a/src/Mmcc.Bot/Commands/Guilds/GetGuildInfo.cs b/src/Mmcc.Bot/Features/Guilds/GetGuildInfo.cs similarity index 98% rename from src/Mmcc.Bot/Commands/Guilds/GetGuildInfo.cs rename to src/Mmcc.Bot/Features/Guilds/GetGuildInfo.cs index 09df9b0..a021de8 100644 --- a/src/Mmcc.Bot/Commands/Guilds/GetGuildInfo.cs +++ b/src/Mmcc.Bot/Features/Guilds/GetGuildInfo.cs @@ -11,7 +11,7 @@ using Remora.Rest.Core; using Remora.Results; -namespace Mmcc.Bot.Commands.Guilds; +namespace Mmcc.Bot.Features.Guilds; /// /// Gets guild info. diff --git a/src/Mmcc.Bot/Commands/Guilds/GetInviteLink.cs b/src/Mmcc.Bot/Features/Guilds/GetInviteLink.cs similarity index 98% rename from src/Mmcc.Bot/Commands/Guilds/GetInviteLink.cs rename to src/Mmcc.Bot/Features/Guilds/GetInviteLink.cs index 35c0757..f7656e9 100644 --- a/src/Mmcc.Bot/Commands/Guilds/GetInviteLink.cs +++ b/src/Mmcc.Bot/Features/Guilds/GetInviteLink.cs @@ -7,7 +7,7 @@ using Remora.Rest.Core; using Remora.Results; -namespace Mmcc.Bot.Commands.Guilds; +namespace Mmcc.Bot.Features.Guilds; /// /// Gets an invite link to a guild. diff --git a/src/Mmcc.Bot/Commands/Guilds/GuildCommands.cs b/src/Mmcc.Bot/Features/Guilds/GuildCommands.cs similarity index 50% rename from src/Mmcc.Bot/Commands/Guilds/GuildCommands.cs rename to src/Mmcc.Bot/Features/Guilds/GuildCommands.cs index 0a7fffe..f06774c 100644 --- a/src/Mmcc.Bot/Commands/Guilds/GuildCommands.cs +++ b/src/Mmcc.Bot/Features/Guilds/GuildCommands.cs @@ -1,26 +1,18 @@ -using System; -using System.Collections.Generic; -using System.ComponentModel; -using System.Linq; +using System.ComponentModel; using System.Threading.Tasks; using MediatR; using Mmcc.Bot.Common.Models.Colours; -using Mmcc.Bot.RemoraAbstractions.Conditions; +using Mmcc.Bot.Features.Guilds.Views; using Mmcc.Bot.RemoraAbstractions.Conditions.CommandSpecific; using Mmcc.Bot.RemoraAbstractions.Services.MessageResponders; +using Porbeagle; using Remora.Commands.Attributes; using Remora.Commands.Groups; -using Remora.Discord.API.Abstractions.Objects; -using Remora.Discord.API.Objects; using Remora.Discord.Commands.Contexts; -using Remora.Rest.Core; using Remora.Results; -namespace Mmcc.Bot.Commands.Guilds; +namespace Mmcc.Bot.Features.Guilds; -/// -/// Core commands. -/// [RequireGuild] public class GuildCommands : CommandGroup { @@ -28,25 +20,21 @@ public class GuildCommands : CommandGroup private readonly IColourPalette _colourPalette; private readonly IMediator _mediator; private readonly CommandMessageResponder _responder; - - /// - /// Instantiates a new instance of class. - /// - /// The message context. - /// The colour palette. - /// The mediator. - /// The command responder. + private readonly IContextAwareViewManager _viewManager; + public GuildCommands( MessageContext context, IColourPalette colourPalette, IMediator mediator, - CommandMessageResponder responder + CommandMessageResponder responder, + IContextAwareViewManager viewManager ) { _context = context; _colourPalette = colourPalette; _mediator = mediator; _responder = responder; + _viewManager = viewManager; } [Command("guild")] @@ -54,29 +42,13 @@ CommandMessageResponder responder public async Task GuildInfo() => await _mediator.Send(new GetGuildInfo.Query(_context.GuildID.Value)) switch { - { IsSuccess: true, Entity: { } e } => - await _responder.Respond(new Embed - { - Title = "Guild info", - Description = "Information about the current guild.", - Fields = new List - { - new("Name", e.GuildName, false), - new("Owner", $"<@{e.GuildOwnerId}>"), - new("Max members", e.GuildMaxMembers.ToString() ?? "Unavailable", false), - new("Available roles", string.Join(", ", e.GuildRoles.Select(r => $"<@&{r.ID}>"))) - }, - Timestamp = DateTimeOffset.UtcNow, - Colour = _colourPalette.Blue, - Thumbnail = e.GuildIconUrl is null - ? new Optional() - : new EmbedThumbnail(e.GuildIconUrl.ToString()) - }), + { IsSuccess: true, Entity: { } guildInfo } => + await _viewManager.RespondWithView(new GuildInfoView(guildInfo)), { IsSuccess: true } => Result.FromError(new NotFoundError($"Guild with ID: {_context.GuildID.Value} not found")), - { IsSuccess: false } res => res, + { IsSuccess: false } res => res }; [Command("invite")] diff --git a/src/Mmcc.Bot/Features/Guilds/Views/GuildInfo.view.cs b/src/Mmcc.Bot/Features/Guilds/Views/GuildInfo.view.cs new file mode 100644 index 0000000..9427804 --- /dev/null +++ b/src/Mmcc.Bot/Features/Guilds/Views/GuildInfo.view.cs @@ -0,0 +1,54 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Mmcc.Bot.Common.Models.Colours; +using Porbeagle; +using Porbeagle.Attributes; +using Remora.Discord.API.Abstractions.Objects; +using Remora.Discord.API.Objects; +using Remora.Rest.Core; + +namespace Mmcc.Bot.Features.Guilds.Views; + +[DiscordView] +public partial record GuildInfoView : IMessageView +{ + public GuildInfoView(GetGuildInfo.QueryResult guildInfo) + => Embed = new GuildInfoEmbed(guildInfo); + + public Optional Text { get; init; } = new(); + + public Embed Embed { get; } +} + +public record GuildInfoEmbed : Embed +{ + public GuildInfoEmbed(GetGuildInfo.QueryResult guildInfo) : base( + Title: "Guild info", + Description: "Information about the current guild.", + Timestamp: DateTimeOffset.UtcNow, + Colour: ColourPalette.Blue, + Thumbnail: guildInfo.GuildIconUrl is null + ? new Optional() + : new EmbedThumbnail(guildInfo.GuildIconUrl.ToString()), + Fields:new List + { + new(Name: "Name", Value: guildInfo.GuildName, IsInline: false), + new(Name: "Owner", Value: $"<@{guildInfo.GuildOwnerId}>", IsInline: false), + new(Name: "Max members", Value: guildInfo.GuildMaxMembers.ToString() ?? "Unavailable", IsInline: false), + new(Name: "Available roles", Value: guildInfo.GuildRoles.AsDiscordFormattedText(), IsInline: false) + } + ) + { + } +} + +file static class RolesListMapperExtensions +{ + internal static string AsDiscordFormattedText(this IEnumerable roles) + { + var linkableRoles = roles.Select(r => $"<@&{r.ID}>"); + + return string.Join(", ", linkableRoles); + } +} diff --git a/src/Mmcc.Bot/Commands/Core/HelpCommands.cs b/src/Mmcc.Bot/Features/Help/HelpCommands.cs similarity index 97% rename from src/Mmcc.Bot/Commands/Core/HelpCommands.cs rename to src/Mmcc.Bot/Features/Help/HelpCommands.cs index e7b0060..82612a5 100644 --- a/src/Mmcc.Bot/Commands/Core/HelpCommands.cs +++ b/src/Mmcc.Bot/Features/Help/HelpCommands.cs @@ -8,7 +8,7 @@ using Remora.Commands.Groups; using Remora.Results; -namespace Mmcc.Bot.Commands.Core; +namespace Mmcc.Bot.Features.Help; /// /// Help commands. diff --git a/src/Mmcc.Bot/Mmcc.Bot.csproj b/src/Mmcc.Bot/Mmcc.Bot.csproj index 28d050a..c2d73ab 100644 --- a/src/Mmcc.Bot/Mmcc.Bot.csproj +++ b/src/Mmcc.Bot/Mmcc.Bot.csproj @@ -1,4 +1,4 @@ - + net7.0 diff --git a/src/Mmcc.Bot/Program.cs b/src/Mmcc.Bot/Program.cs index a1d91c3..98bd2ee 100644 --- a/src/Mmcc.Bot/Program.cs +++ b/src/Mmcc.Bot/Program.cs @@ -50,7 +50,7 @@ services.AddInMemoryStores(); services.AddBotDatabaseContext(); - services.AddSingleton(); + services.AddSingleton(); // FluentValidation; services.AddValidatorsFromAssemblyContaining(); From cffdae6f370a8d3f23d7dc62fdf226bec3658d1e Mon Sep 17 00:00:00 2001 From: TraceLD Date: Mon, 3 Apr 2023 03:53:20 +0100 Subject: [PATCH 18/30] Clean up code --- .../Restarts/MinecraftAutoRestartsCommands.cs | 4 ++-- src/Mmcc.Bot/Features/Guilds/GetGuildInfo.cs | 18 ++++-------------- src/Mmcc.Bot/Features/Guilds/GetInviteLink.cs | 16 ++++------------ 3 files changed, 10 insertions(+), 28 deletions(-) diff --git a/src/Mmcc.Bot/Commands/Minecraft/Restarts/MinecraftAutoRestartsCommands.cs b/src/Mmcc.Bot/Commands/Minecraft/Restarts/MinecraftAutoRestartsCommands.cs index 05f4c1c..49bb2bf 100644 --- a/src/Mmcc.Bot/Commands/Minecraft/Restarts/MinecraftAutoRestartsCommands.cs +++ b/src/Mmcc.Bot/Commands/Minecraft/Restarts/MinecraftAutoRestartsCommands.cs @@ -98,9 +98,9 @@ public async Task Scheduled() { var res = await _mediator.Send(new GetAllScheduled.Query()); - return res.Count switch + return res switch { - > 0 => await _responder.Respond(new Embed + [_, ..] => await _responder.Respond(new Embed { Title = "Scheduled recurring restarts", Thumbnail = EmbedProperties.MmccLogoThumbnail, diff --git a/src/Mmcc.Bot/Features/Guilds/GetGuildInfo.cs b/src/Mmcc.Bot/Features/Guilds/GetGuildInfo.cs index a021de8..8dd5b85 100644 --- a/src/Mmcc.Bot/Features/Guilds/GetGuildInfo.cs +++ b/src/Mmcc.Bot/Features/Guilds/GetGuildInfo.cs @@ -45,22 +45,16 @@ public record QueryResult( IList GuildRoles, Uri? GuildIconUrl ); - - /// + public class Handler : IRequestHandler> { private readonly IDiscordRestGuildAPI _guildApi; - - /// - /// Instantiates a new instance of class. - /// - /// The guild API. + public Handler(IDiscordRestGuildAPI guildApi) { _guildApi = guildApi; } - - /// + public async Task> Handle(Query request, CancellationToken cancellationToken) { var getGuildInfoResult = await _guildApi.GetGuildAsync(request.GuildId, ct: cancellationToken); @@ -70,11 +64,7 @@ public async Task> Handle(Query request, CancellationToken c } var guildInfo = getGuildInfoResult.Entity; - if (guildInfo is null) - { - return new NotFoundError("Guild not found."); - } - + Uri? iconUrl; if (guildInfo.Icon is not null) { diff --git a/src/Mmcc.Bot/Features/Guilds/GetInviteLink.cs b/src/Mmcc.Bot/Features/Guilds/GetInviteLink.cs index f7656e9..9cbe23e 100644 --- a/src/Mmcc.Bot/Features/Guilds/GetInviteLink.cs +++ b/src/Mmcc.Bot/Features/Guilds/GetInviteLink.cs @@ -30,34 +30,26 @@ public Validator() .NotNull(); } } - - /// + public class Handler : IRequestHandler> { private readonly IDiscordRestGuildAPI _guildApi; - - /// - /// Instantiates a new instance of class. - /// - /// The guild API. + public Handler(IDiscordRestGuildAPI guildApi) { _guildApi = guildApi; } - - /// + public async Task> Handle(Query request, CancellationToken cancellationToken) { var getGuildInvitesResult = await _guildApi.GetGuildInvitesAsync(request.GuildId, cancellationToken); - if (!getGuildInvitesResult.IsSuccess) { return Result.FromError(getGuildInvitesResult); } var invites = getGuildInvitesResult.Entity; - - if (invites is null || !invites.Any()) + if (!invites.Any()) { return new NotFoundError( "Could not find an active invite link. Administrators should create an invite link in Discord guild settings that does not expire."); From cf95f24cc3a67ba1cb80fbe40215a55dd2830f08 Mon Sep 17 00:00:00 2001 From: TraceLD Date: Fri, 28 Apr 2023 00:54:45 +0100 Subject: [PATCH 19/30] port more commands to the new system --- .../Buttons/DonateButton.cs | 6 +-- src/Mmcc.Bot/Commands/CommandsSetup.cs | 1 + .../Minecraft/MinecraftServersCommands.cs | 42 +++---------------- .../Minecraft/Views/OnlineServers.view.cs | 20 +++++++++ src/Mmcc.Bot/CoreSetup/ConfigurationSetup.cs | 1 + .../Diagnostics/DiagnosticsCommands.cs | 12 +++--- .../Diagnostics}/DiagnosticsSettings.cs | 2 +- ...sourcesToCheck.cs => GetBotDiagnostics.cs} | 7 ++-- ...ics.view.cs => GetBotDiagnostics.dview.cs} | 16 +++---- .../Diagnostics/GetDrivesDiagnostics.cs | 14 +++---- ....view.cs => GetDrivesDiagnostics.dview.cs} | 8 ++-- src/Mmcc.Bot/Features/Guilds/GetGuildInfo.cs | 8 ++-- ...uildInfo.view.cs => GetGuildInfo.dview.cs} | 8 ++-- src/Mmcc.Bot/Features/Guilds/GetInviteLink.cs | 8 ++-- src/Mmcc.Bot/Features/Guilds/GuildCommands.cs | 5 +-- .../MmccInfo/MmccInfo.view.cs | 2 +- .../MmccInfo/MmccInfoCommands.cs | 5 ++- src/Mmcc.Bot/Mmcc.Bot.csproj | 7 +++- 18 files changed, 83 insertions(+), 89 deletions(-) create mode 100644 src/Mmcc.Bot/Commands/Minecraft/Views/OnlineServers.view.cs rename src/{Mmcc.Bot.Common/Models/Settings => Mmcc.Bot/Features/Diagnostics}/DiagnosticsSettings.cs (92%) rename src/Mmcc.Bot/Features/Diagnostics/{PingAllNetworkResourcesToCheck.cs => GetBotDiagnostics.cs} (87%) rename src/Mmcc.Bot/Features/Diagnostics/{Views/BotDiagnostics.view.cs => GetBotDiagnostics.dview.cs} (73%) rename src/Mmcc.Bot/Features/Diagnostics/{Views/DrivesDiagnostics.view.cs => GetDrivesDiagnostics.dview.cs} (86%) rename src/Mmcc.Bot/Features/Guilds/{Views/GuildInfo.view.cs => GetGuildInfo.dview.cs} (88%) rename src/Mmcc.Bot/{Commands => Features}/MmccInfo/MmccInfo.view.cs (91%) rename src/Mmcc.Bot/{Commands => Features}/MmccInfo/MmccInfoCommands.cs (81%) diff --git a/src/Mmcc.Bot.Common.UI/Buttons/DonateButton.cs b/src/Mmcc.Bot.Common.UI/Buttons/DonateButton.cs index 739b907..7e2087e 100644 --- a/src/Mmcc.Bot.Common.UI/Buttons/DonateButton.cs +++ b/src/Mmcc.Bot.Common.UI/Buttons/DonateButton.cs @@ -5,8 +5,8 @@ namespace Mmcc.Bot.Common.UI.Buttons; public record DonateButton() : ButtonComponent( - ButtonComponentStyle.Link, - "Donate", - new PartialEmoji(Name: "❤️"), + Style: ButtonComponentStyle.Link, + Label: "Donate", + Emoji: new PartialEmoji(Name: "❤️"), URL: MmccUrls.Donations ); \ No newline at end of file diff --git a/src/Mmcc.Bot/Commands/CommandsSetup.cs b/src/Mmcc.Bot/Commands/CommandsSetup.cs index 130afc6..030c47a 100644 --- a/src/Mmcc.Bot/Commands/CommandsSetup.cs +++ b/src/Mmcc.Bot/Commands/CommandsSetup.cs @@ -12,6 +12,7 @@ using Mmcc.Bot.Features.Diagnostics; using Mmcc.Bot.Features.Guilds; using Mmcc.Bot.Features.Help; +using Mmcc.Bot.Features.MmccInfo; using Remora.Commands.Extensions; using Remora.Discord.Commands.Extensions; diff --git a/src/Mmcc.Bot/Commands/Minecraft/MinecraftServersCommands.cs b/src/Mmcc.Bot/Commands/Minecraft/MinecraftServersCommands.cs index 9a007a4..c0581d1 100644 --- a/src/Mmcc.Bot/Commands/Minecraft/MinecraftServersCommands.cs +++ b/src/Mmcc.Bot/Commands/Minecraft/MinecraftServersCommands.cs @@ -9,7 +9,6 @@ using Mmcc.Bot.Common.Statics; using Mmcc.Bot.Polychat.MessageSenders; using Mmcc.Bot.Polychat.Services; -using Mmcc.Bot.RemoraAbstractions.Conditions; using Mmcc.Bot.RemoraAbstractions.Conditions.CommandSpecific; using Mmcc.Bot.RemoraAbstractions.Services.MessageResponders; using Remora.Commands.Attributes; @@ -21,9 +20,6 @@ namespace Mmcc.Bot.Commands.Minecraft; -/// -/// Commands for managing MC servers. -/// [Group("mc")] [Description("Minecraft (Polychat)")] [RequireGuild] @@ -34,15 +30,7 @@ public class MinecraftServersCommands : CommandGroup private readonly IColourPalette _colourPalette; private readonly IPolychatService _polychatService; private readonly CommandMessageResponder _responder; - - /// - /// Instantiates a new instance of class. - /// - /// The message context. - /// The mediator. - /// The colour palette. - /// The polychat service. - /// The command responder. + public MinecraftServersCommands( MessageContext context, IMediator mediator, @@ -57,44 +45,24 @@ CommandMessageResponder responder _polychatService = polychatService; _responder = responder; } - - /// - /// Shows current TPS of a MC server. - /// - /// ID of the server. - /// Result of the operation. + [Command("tps")] [Description("Shows current TPS of a MC server")] public async Task Tps(string serverId) => await _mediator.Send(new SendTpsCommand.Command(serverId, _context.ChannelID)); - - /// - /// Executes a command on a MC server. - /// - /// ID of the server. - /// Command arguments. - /// Result of the operation. + [Command("exec", "e", "execute")] [Description("Executes a command on a MC server")] [RequireUserGuildPermission(DiscordPermission.BanMembers)] public async Task Exec(string serverId, [Greedy] IEnumerable args) => await _mediator.Send(new SendExecCommand.Command(serverId, _context.ChannelID, args)); - - /// - /// Restarts a MC server. - /// - /// ID of the server to restart. - /// Result of the operation. + [Command("restart", "r")] [Description("Restarts a server")] [RequireUserGuildPermission(DiscordPermission.BanMembers)] public async Task Restart(string serverId) => await _mediator.Send(new SendRestartCommand.Command(serverId, _context.ChannelID)); - - /// - /// Shows info about online servers. - /// - /// Result of the operation. + [Command("online", "o")] [Description("Shows info about online servers")] public async Task Online() diff --git a/src/Mmcc.Bot/Commands/Minecraft/Views/OnlineServers.view.cs b/src/Mmcc.Bot/Commands/Minecraft/Views/OnlineServers.view.cs new file mode 100644 index 0000000..37c5773 --- /dev/null +++ b/src/Mmcc.Bot/Commands/Minecraft/Views/OnlineServers.view.cs @@ -0,0 +1,20 @@ +using System.Collections.Generic; +using Mmcc.Bot.Polychat.Models; +using Porbeagle; +using Porbeagle.Attributes; +using Remora.Discord.API.Objects; +using Remora.Rest.Core; + +namespace Mmcc.Bot.Commands.Minecraft.Views; + +[DiscordView] +public sealed partial record OnlineServersView : IMessageView +{ + public OnlineServersView(IEnumerable results) + => Embed = new(); + + public Optional Text { get; init; } = new(); + + public Embed Embed { get; } +} + diff --git a/src/Mmcc.Bot/CoreSetup/ConfigurationSetup.cs b/src/Mmcc.Bot/CoreSetup/ConfigurationSetup.cs index f6ad45f..5a50659 100644 --- a/src/Mmcc.Bot/CoreSetup/ConfigurationSetup.cs +++ b/src/Mmcc.Bot/CoreSetup/ConfigurationSetup.cs @@ -3,6 +3,7 @@ using Mmcc.Bot.Common.Extensions.Microsoft.Extensions.DependencyInjection; using Mmcc.Bot.Common.Models.Settings; using Mmcc.Bot.Database.Settings; +using Mmcc.Bot.Features.Diagnostics; using Mmcc.Bot.Polychat.Models.Settings; using Remora.Discord.API.Abstractions.Gateway.Commands; using Remora.Discord.Gateway; diff --git a/src/Mmcc.Bot/Features/Diagnostics/DiagnosticsCommands.cs b/src/Mmcc.Bot/Features/Diagnostics/DiagnosticsCommands.cs index 38a05e0..dc85b40 100644 --- a/src/Mmcc.Bot/Features/Diagnostics/DiagnosticsCommands.cs +++ b/src/Mmcc.Bot/Features/Diagnostics/DiagnosticsCommands.cs @@ -1,7 +1,7 @@ -using System.ComponentModel; +using System; +using System.ComponentModel; using System.Threading.Tasks; using MediatR; -using Mmcc.Bot.Features.Diagnostics.Views; using Mmcc.Bot.RemoraAbstractions.Conditions.CommandSpecific; using Porbeagle; using Remora.Commands.Attributes; @@ -13,7 +13,7 @@ namespace Mmcc.Bot.Features.Diagnostics; [Group("diagnostics")] [Description("Server and bot diagnostics")] -public class DiagnosticsCommands : CommandGroup +public sealed class DiagnosticsCommands : CommandGroup { private readonly IMediator _mediator; private readonly IContextAwareViewManager _viewManager; @@ -28,12 +28,12 @@ public DiagnosticsCommands(IMediator mediator, IContextAwareViewManager viewMana [Description("Show status of the bot and APIs it uses")] public async Task BotDiagnostics() { - var result = await _mediator.Send(new PingAllNetworkResourcesToCheck.Query()); + var result = await _mediator.Send(new GetBotDiagnostics.Query()); return result switch { { IsSuccess: true, Entity: { } pingResults } - => await _viewManager.RespondWithView(new BotDiagnosticsView(pingResults)), + => await _viewManager.RespondWithView(new GetBotDiagnosticsView(pingResults)), { IsSuccess: false } => result }; @@ -47,6 +47,6 @@ public async Task DrivesDiagnostics() { var result = await _mediator.Send(new GetDrivesDiagnostics.Query()); - return await _viewManager.RespondWithView(new DrivesDiagnosticsView(result)); + return await _viewManager.RespondWithView(new GetDrivesDiagnosticsView(result)); } } \ No newline at end of file diff --git a/src/Mmcc.Bot.Common/Models/Settings/DiagnosticsSettings.cs b/src/Mmcc.Bot/Features/Diagnostics/DiagnosticsSettings.cs similarity index 92% rename from src/Mmcc.Bot.Common/Models/Settings/DiagnosticsSettings.cs rename to src/Mmcc.Bot/Features/Diagnostics/DiagnosticsSettings.cs index 9548f67..efca0a6 100644 --- a/src/Mmcc.Bot.Common/Models/Settings/DiagnosticsSettings.cs +++ b/src/Mmcc.Bot/Features/Diagnostics/DiagnosticsSettings.cs @@ -1,6 +1,6 @@ using System.Collections.Generic; -namespace Mmcc.Bot.Common.Models.Settings; +namespace Mmcc.Bot.Features.Diagnostics; public interface IDiagnosticsSettings { diff --git a/src/Mmcc.Bot/Features/Diagnostics/PingAllNetworkResourcesToCheck.cs b/src/Mmcc.Bot/Features/Diagnostics/GetBotDiagnostics.cs similarity index 87% rename from src/Mmcc.Bot/Features/Diagnostics/PingAllNetworkResourcesToCheck.cs rename to src/Mmcc.Bot/Features/Diagnostics/GetBotDiagnostics.cs index f76a5e0..5fa9678 100644 --- a/src/Mmcc.Bot/Features/Diagnostics/PingAllNetworkResourcesToCheck.cs +++ b/src/Mmcc.Bot/Features/Diagnostics/GetBotDiagnostics.cs @@ -4,7 +4,6 @@ using System.Threading; using System.Threading.Tasks; using MediatR; -using Mmcc.Bot.Common.Models.Settings; using Remora.Results; namespace Mmcc.Bot.Features.Diagnostics; @@ -12,13 +11,13 @@ namespace Mmcc.Bot.Features.Diagnostics; /// /// Pings all network resources to check specified in . /// -public class PingAllNetworkResourcesToCheck +public sealed class GetBotDiagnostics { public record struct Query : IRequest>>; - public record QueryResult(string Name, string Address, IPStatus Status, long? RoundtripTime); + public sealed record QueryResult(string Name, string Address, IPStatus Status, long? RoundtripTime); - public class Handler : IRequestHandler>> + public sealed class Handler : IRequestHandler>> { private readonly IDiagnosticsSettings _settings; diff --git a/src/Mmcc.Bot/Features/Diagnostics/Views/BotDiagnostics.view.cs b/src/Mmcc.Bot/Features/Diagnostics/GetBotDiagnostics.dview.cs similarity index 73% rename from src/Mmcc.Bot/Features/Diagnostics/Views/BotDiagnostics.view.cs rename to src/Mmcc.Bot/Features/Diagnostics/GetBotDiagnostics.dview.cs index 20a98e8..a050e72 100644 --- a/src/Mmcc.Bot/Features/Diagnostics/Views/BotDiagnostics.view.cs +++ b/src/Mmcc.Bot/Features/Diagnostics/GetBotDiagnostics.dview.cs @@ -9,12 +9,12 @@ using Remora.Discord.API.Objects; using Remora.Rest.Core; -namespace Mmcc.Bot.Features.Diagnostics.Views; +namespace Mmcc.Bot.Features.Diagnostics; [DiscordView] -public partial record BotDiagnosticsView : IMessageView +public sealed partial record GetBotDiagnosticsView : IMessageView { - public BotDiagnosticsView(IEnumerable results) + public GetBotDiagnosticsView(IEnumerable results) => Embed = new BotDiagnosticsEmbed(results); public Optional Text { get; init; } = new(); @@ -22,9 +22,9 @@ public BotDiagnosticsView(IEnumerable results) : base( + public BotDiagnosticsEmbed(IEnumerable results) : base( Title: "Bot diagnostics", Description: "Information about the status of the bot and the APIs it uses", Timestamp: DateTimeOffset.UtcNow, @@ -34,7 +34,7 @@ public BotDiagnosticsEmbed(IEnumerable> GetFields(IEnumerable results) + private static Optional> GetFields(IEnumerable results) { var fields = new List { @@ -47,9 +47,9 @@ private static Optional> GetFields(IEnumerable /// Gets the drives diagnostics. /// -public class GetDrivesDiagnostics +public sealed class GetDrivesDiagnostics { /// /// Query to get drives. /// - public class Query : IRequest> + public sealed class Query : IRequest> { } - + /// /// Drive diagnostics. /// - public class QueryResult + public sealed class QueryResult { public string Name { get; set; } = null!; public DriveType DriveType { get; set; } @@ -30,8 +30,8 @@ public class QueryResult public double PercentageUsed { get; set; } public float GigabytesTotalSize { get; set; } } - - public class Handler : RequestHandler> + + public sealed class Handler : RequestHandler> { protected override IList Handle(Query request) { @@ -45,7 +45,7 @@ protected override IList Handle(Query request) Label = string.IsNullOrWhiteSpace(d.VolumeLabel) ? "None" : d.VolumeLabel, DriveFormat = d.DriveFormat, GigabytesFree = d.AvailableFreeSpace / 1024f / 1024f / 1024f, - PercentageUsed = (double) (d.TotalSize - d.AvailableFreeSpace) / d.TotalSize * 100, + PercentageUsed = (double)(d.TotalSize - d.AvailableFreeSpace) / d.TotalSize * 100, GigabytesTotalSize = d.TotalSize / 1024f / 1024f / 1024f }) .ToList(); diff --git a/src/Mmcc.Bot/Features/Diagnostics/Views/DrivesDiagnostics.view.cs b/src/Mmcc.Bot/Features/Diagnostics/GetDrivesDiagnostics.dview.cs similarity index 86% rename from src/Mmcc.Bot/Features/Diagnostics/Views/DrivesDiagnostics.view.cs rename to src/Mmcc.Bot/Features/Diagnostics/GetDrivesDiagnostics.dview.cs index 93c2cc3..d0cb410 100644 --- a/src/Mmcc.Bot/Features/Diagnostics/Views/DrivesDiagnostics.view.cs +++ b/src/Mmcc.Bot/Features/Diagnostics/GetDrivesDiagnostics.dview.cs @@ -9,19 +9,19 @@ using Remora.Discord.API.Objects; using Remora.Rest.Core; -namespace Mmcc.Bot.Features.Diagnostics.Views; +namespace Mmcc.Bot.Features.Diagnostics; [DiscordView] -public partial record DrivesDiagnosticsView : IMessageView +public sealed partial record GetDrivesDiagnosticsView : IMessageView { - public DrivesDiagnosticsView(IEnumerable results) + public GetDrivesDiagnosticsView(IEnumerable results) => Embed = new DrivesDiagnosticsEmbed(results); public Optional Text { get; init; } = new(); public Embed Embed { get; } } -public record DrivesDiagnosticsEmbed : Embed +public sealed record DrivesDiagnosticsEmbed : Embed { public DrivesDiagnosticsEmbed(IEnumerable results) : base( Title: "Drives diagnostics", diff --git a/src/Mmcc.Bot/Features/Guilds/GetGuildInfo.cs b/src/Mmcc.Bot/Features/Guilds/GetGuildInfo.cs index 8dd5b85..02487bd 100644 --- a/src/Mmcc.Bot/Features/Guilds/GetGuildInfo.cs +++ b/src/Mmcc.Bot/Features/Guilds/GetGuildInfo.cs @@ -21,12 +21,12 @@ public class GetGuildInfo /// /// Query to get guild info. /// - public record Query(Snowflake GuildId) : IRequest>; + public sealed record Query(Snowflake GuildId) : IRequest>; /// /// Validates the . /// - public class Validator : AbstractValidator + public sealed class Validator : AbstractValidator { public Validator() { @@ -38,7 +38,7 @@ public Validator() /// /// Result of the query to get guild info. /// - public record QueryResult( + public sealed record QueryResult( string GuildName, Snowflake GuildOwnerId, int? GuildMaxMembers, @@ -46,7 +46,7 @@ public record QueryResult( Uri? GuildIconUrl ); - public class Handler : IRequestHandler> + public sealed class Handler : IRequestHandler> { private readonly IDiscordRestGuildAPI _guildApi; diff --git a/src/Mmcc.Bot/Features/Guilds/Views/GuildInfo.view.cs b/src/Mmcc.Bot/Features/Guilds/GetGuildInfo.dview.cs similarity index 88% rename from src/Mmcc.Bot/Features/Guilds/Views/GuildInfo.view.cs rename to src/Mmcc.Bot/Features/Guilds/GetGuildInfo.dview.cs index 9427804..6acaa86 100644 --- a/src/Mmcc.Bot/Features/Guilds/Views/GuildInfo.view.cs +++ b/src/Mmcc.Bot/Features/Guilds/GetGuildInfo.dview.cs @@ -8,12 +8,12 @@ using Remora.Discord.API.Objects; using Remora.Rest.Core; -namespace Mmcc.Bot.Features.Guilds.Views; +namespace Mmcc.Bot.Features.Guilds; [DiscordView] -public partial record GuildInfoView : IMessageView +public sealed partial record GetGuildInfoView : IMessageView { - public GuildInfoView(GetGuildInfo.QueryResult guildInfo) + public GetGuildInfoView(GetGuildInfo.QueryResult guildInfo) => Embed = new GuildInfoEmbed(guildInfo); public Optional Text { get; init; } = new(); @@ -21,7 +21,7 @@ public GuildInfoView(GetGuildInfo.QueryResult guildInfo) public Embed Embed { get; } } -public record GuildInfoEmbed : Embed +public sealed record GuildInfoEmbed : Embed { public GuildInfoEmbed(GetGuildInfo.QueryResult guildInfo) : base( Title: "Guild info", diff --git a/src/Mmcc.Bot/Features/Guilds/GetInviteLink.cs b/src/Mmcc.Bot/Features/Guilds/GetInviteLink.cs index 9cbe23e..367e864 100644 --- a/src/Mmcc.Bot/Features/Guilds/GetInviteLink.cs +++ b/src/Mmcc.Bot/Features/Guilds/GetInviteLink.cs @@ -12,17 +12,17 @@ namespace Mmcc.Bot.Features.Guilds; /// /// Gets an invite link to a guild. /// -public class GetInviteLink +public sealed class GetInviteLink { /// /// Query to get an invite link to a guild. /// - public record Query(Snowflake GuildId) : IRequest>; + public sealed record Query(Snowflake GuildId) : IRequest>; /// /// Validates the . /// - public class Validator : AbstractValidator + public sealed class Validator : AbstractValidator { public Validator() { @@ -31,7 +31,7 @@ public Validator() } } - public class Handler : IRequestHandler> + public sealed class Handler : IRequestHandler> { private readonly IDiscordRestGuildAPI _guildApi; diff --git a/src/Mmcc.Bot/Features/Guilds/GuildCommands.cs b/src/Mmcc.Bot/Features/Guilds/GuildCommands.cs index f06774c..629b7e4 100644 --- a/src/Mmcc.Bot/Features/Guilds/GuildCommands.cs +++ b/src/Mmcc.Bot/Features/Guilds/GuildCommands.cs @@ -2,7 +2,6 @@ using System.Threading.Tasks; using MediatR; using Mmcc.Bot.Common.Models.Colours; -using Mmcc.Bot.Features.Guilds.Views; using Mmcc.Bot.RemoraAbstractions.Conditions.CommandSpecific; using Mmcc.Bot.RemoraAbstractions.Services.MessageResponders; using Porbeagle; @@ -14,7 +13,7 @@ namespace Mmcc.Bot.Features.Guilds; [RequireGuild] -public class GuildCommands : CommandGroup +public sealed class GuildCommands : CommandGroup { private readonly MessageContext _context; private readonly IColourPalette _colourPalette; @@ -43,7 +42,7 @@ public async Task GuildInfo() => await _mediator.Send(new GetGuildInfo.Query(_context.GuildID.Value)) switch { { IsSuccess: true, Entity: { } guildInfo } => - await _viewManager.RespondWithView(new GuildInfoView(guildInfo)), + await _viewManager.RespondWithView(new GetGuildInfoView(guildInfo)), { IsSuccess: true } => Result.FromError(new NotFoundError($"Guild with ID: {_context.GuildID.Value} not found")), diff --git a/src/Mmcc.Bot/Commands/MmccInfo/MmccInfo.view.cs b/src/Mmcc.Bot/Features/MmccInfo/MmccInfo.view.cs similarity index 91% rename from src/Mmcc.Bot/Commands/MmccInfo/MmccInfo.view.cs rename to src/Mmcc.Bot/Features/MmccInfo/MmccInfo.view.cs index dd46852..b6d064d 100644 --- a/src/Mmcc.Bot/Commands/MmccInfo/MmccInfo.view.cs +++ b/src/Mmcc.Bot/Features/MmccInfo/MmccInfo.view.cs @@ -6,7 +6,7 @@ namespace Mmcc.Bot.Commands.MmccInfo; [DiscordView] -public partial record MmccInfoView : IMessageView +public sealed partial record MmccInfoView : IMessageView { public Optional Text { get; init; } = "Useful links"; diff --git a/src/Mmcc.Bot/Commands/MmccInfo/MmccInfoCommands.cs b/src/Mmcc.Bot/Features/MmccInfo/MmccInfoCommands.cs similarity index 81% rename from src/Mmcc.Bot/Commands/MmccInfo/MmccInfoCommands.cs rename to src/Mmcc.Bot/Features/MmccInfo/MmccInfoCommands.cs index 50d88d0..6c603bb 100644 --- a/src/Mmcc.Bot/Commands/MmccInfo/MmccInfoCommands.cs +++ b/src/Mmcc.Bot/Features/MmccInfo/MmccInfoCommands.cs @@ -1,11 +1,12 @@ using System.ComponentModel; using System.Threading.Tasks; +using Mmcc.Bot.Commands.MmccInfo; using Porbeagle; using Remora.Commands.Attributes; using Remora.Commands.Groups; using Remora.Results; -namespace Mmcc.Bot.Commands.MmccInfo; +namespace Mmcc.Bot.Features.MmccInfo; public class MmccInfoCommands : CommandGroup { @@ -16,6 +17,6 @@ public MmccInfoCommands(IContextAwareViewManager viewManager) => [Command("mmcc")] [Description("Shows useful MMCC links")] - public async Task Mmcc() + public async Task GetMmccInfo() => await _viewManager.RespondWithView(new MmccInfoView()); } \ No newline at end of file diff --git a/src/Mmcc.Bot/Mmcc.Bot.csproj b/src/Mmcc.Bot/Mmcc.Bot.csproj index c2d73ab..f37304d 100644 --- a/src/Mmcc.Bot/Mmcc.Bot.csproj +++ b/src/Mmcc.Bot/Mmcc.Bot.csproj @@ -4,7 +4,7 @@ net7.0 dotnet-Mmcc.Bot-4BA5AD38-B3B4-456A-853F-080768D88F42 enable - 11 + default @@ -16,6 +16,7 @@ + @@ -40,4 +41,8 @@ + + + + From 048557a48ed65f48154b15d2fdfdd990b44e380b Mon Sep 17 00:00:00 2001 From: TraceLD Date: Sun, 14 May 2023 00:08:04 +0100 Subject: [PATCH 20/30] Add a working Discord command generator --- .../Models/Colours/IColourPalette.cs | 15 + src/Mmcc.Bot.SourceGenerators/GlobalUsings.cs | 5 + .../Mmcc.Bot.SourceGenerators.csproj | 24 ++ .../StringExtensions.cs | 9 + .../VSA/Attributes.cs | 33 ++ src/Mmcc.Bot.SourceGenerators/VSA/Contexts.cs | 42 +++ .../VSA/VSAClassAttributesData.cs | 10 + .../VSA/VerticalSliceArchitectureGenerator.cs | 356 ++++++++++++++++++ src/Mmcc.Bot.sln | 6 + .../Moderation/GeneralModerationCommands.cs | 6 +- src/Mmcc.Bot/Mmcc.Bot.csproj | 1 + src/Mmcc.Bot/Program.cs | 5 +- 12 files changed, 507 insertions(+), 5 deletions(-) create mode 100644 src/Mmcc.Bot.SourceGenerators/GlobalUsings.cs create mode 100644 src/Mmcc.Bot.SourceGenerators/Mmcc.Bot.SourceGenerators.csproj create mode 100644 src/Mmcc.Bot.SourceGenerators/StringExtensions.cs create mode 100644 src/Mmcc.Bot.SourceGenerators/VSA/Attributes.cs create mode 100644 src/Mmcc.Bot.SourceGenerators/VSA/Contexts.cs create mode 100644 src/Mmcc.Bot.SourceGenerators/VSA/VSAClassAttributesData.cs create mode 100644 src/Mmcc.Bot.SourceGenerators/VSA/VerticalSliceArchitectureGenerator.cs diff --git a/src/Mmcc.Bot.Common/Models/Colours/IColourPalette.cs b/src/Mmcc.Bot.Common/Models/Colours/IColourPalette.cs index 11e5427..fa94cee 100644 --- a/src/Mmcc.Bot.Common/Models/Colours/IColourPalette.cs +++ b/src/Mmcc.Bot.Common/Models/Colours/IColourPalette.cs @@ -2,6 +2,8 @@ namespace Mmcc.Bot.Common.Models.Colours { + // TODO: REMOVE THIS CLASS; + /// /// Colour palette for embeds; /// @@ -17,4 +19,17 @@ public interface IColourPalette public Color Purple { get; } public Color Pink { get; } } + + public class TailwindColourPalette : IColourPalette + { + public Color Black => ColorTranslator.FromHtml("#262626"); + public Color Gray => ColorTranslator.FromHtml("#6B7280"); + public Color Red => ColorTranslator.FromHtml("#EF4444"); + public Color Yellow => ColorTranslator.FromHtml("#F59E0B"); + public Color Green => ColorTranslator.FromHtml("#10B981"); + public Color Blue => ColorTranslator.FromHtml("#3B82F6"); + public Color Indigo => ColorTranslator.FromHtml("#6366F1"); + public Color Purple => ColorTranslator.FromHtml("#8B5CF6"); + public Color Pink => ColorTranslator.FromHtml("#EC4899"); + } } \ No newline at end of file diff --git a/src/Mmcc.Bot.SourceGenerators/GlobalUsings.cs b/src/Mmcc.Bot.SourceGenerators/GlobalUsings.cs new file mode 100644 index 0000000..3e24bfa --- /dev/null +++ b/src/Mmcc.Bot.SourceGenerators/GlobalUsings.cs @@ -0,0 +1,5 @@ +global using System.Runtime.CompilerServices; +global using Microsoft.CodeAnalysis; +global using Microsoft.CodeAnalysis.CSharp; +global using Microsoft.CodeAnalysis.CSharp.Syntax; +global using Microsoft.CodeAnalysis.Text; \ No newline at end of file diff --git a/src/Mmcc.Bot.SourceGenerators/Mmcc.Bot.SourceGenerators.csproj b/src/Mmcc.Bot.SourceGenerators/Mmcc.Bot.SourceGenerators.csproj new file mode 100644 index 0000000..6d415f2 --- /dev/null +++ b/src/Mmcc.Bot.SourceGenerators/Mmcc.Bot.SourceGenerators.csproj @@ -0,0 +1,24 @@ + + + + netstandard2.0 + false + enable + enable + latest + true + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + diff --git a/src/Mmcc.Bot.SourceGenerators/StringExtensions.cs b/src/Mmcc.Bot.SourceGenerators/StringExtensions.cs new file mode 100644 index 0000000..ba1c7ec --- /dev/null +++ b/src/Mmcc.Bot.SourceGenerators/StringExtensions.cs @@ -0,0 +1,9 @@ +namespace Mmcc.Bot.SourceGenerators; + +internal static class StringExtensions +{ + internal static string ToCamelCase(this string str) => + string.IsNullOrEmpty(str) || str.Length < 2 + ? str.ToLowerInvariant() + : char.ToLowerInvariant(str[0]) + str.Substring(1); +} \ No newline at end of file diff --git a/src/Mmcc.Bot.SourceGenerators/VSA/Attributes.cs b/src/Mmcc.Bot.SourceGenerators/VSA/Attributes.cs new file mode 100644 index 0000000..1e30bb1 --- /dev/null +++ b/src/Mmcc.Bot.SourceGenerators/VSA/Attributes.cs @@ -0,0 +1,33 @@ +namespace Mmcc.Bot.SourceGenerators.VSA; + +internal static class Attributes +{ + public static string GenerateDiscordFromMediatRAttribute => + """ + namespace Mmcc.Bot.SourceGenerators.VSA; + + [global::System.CodeDom.Compiler.GeneratedCode("Mmcc.Bot.SourceGenerators", "1.0.0")] + [global::System.AttributeUsage(global::System.AttributeTargets.Class)] + public class MapDiscordCommandAttribute : global::System.Attribute + where TCommandGroup : global::Remora.Commands.Groups.CommandGroup + { + public string Name { get; set; } + public string Description { get; set; } + public bool IsGreedy { get; set; } + public string[] Aliases { get; set; } + + public MapDiscordCommandAttribute( + string name, + string description, + bool isGreedy, + params string[] aliases + ) + { + Name = name; + Description = description; + IsGreedy = isGreedy; + Aliases = aliases; + } + } + """; +} diff --git a/src/Mmcc.Bot.SourceGenerators/VSA/Contexts.cs b/src/Mmcc.Bot.SourceGenerators/VSA/Contexts.cs new file mode 100644 index 0000000..9429f11 --- /dev/null +++ b/src/Mmcc.Bot.SourceGenerators/VSA/Contexts.cs @@ -0,0 +1,42 @@ +namespace Mmcc.Bot.SourceGenerators.VSA; + +internal sealed class Contexts +{ + internal class ClassContext + { + public string Namespace { get; set; } = null!; + public string ClassName { get; set; } = null!; + } + + internal sealed class VSAClassContext : ClassContext + { + public RequestClassContext RequestClassContext { get; set; } = null!; + public DiscordCommandContext DiscordCommandContext { get; set; } = null!; + public bool ShouldHandleNullReturn { get; set; } + } + + internal sealed class RequestClassContext : ClassContext + { + public IReadOnlyList Properties { get; set; } = null!; + } + + internal sealed class DiscordCommandContext : ClassContext + { + public bool IsGreedy { get; set; } + public string CommandName { get; set; } = null!; + public string CommandDescription { get; set; } = null!; + public IReadOnlyList CommandAliases { get; set; } = null!; + public DiscordViewContext MatchedView { get; set; } = null!; + } + + internal sealed class DiscordViewContext : ClassContext + { + public bool HasOnEmpty { get; set; } + } + + internal sealed class PropertyContext + { + public string Type { get; set; } = null!; + public string Name { get; set; } = null!; + } +} diff --git a/src/Mmcc.Bot.SourceGenerators/VSA/VSAClassAttributesData.cs b/src/Mmcc.Bot.SourceGenerators/VSA/VSAClassAttributesData.cs new file mode 100644 index 0000000..0fc32f5 --- /dev/null +++ b/src/Mmcc.Bot.SourceGenerators/VSA/VSAClassAttributesData.cs @@ -0,0 +1,10 @@ +namespace Mmcc.Bot.SourceGenerators.VSA; + +public sealed class DiscordCommandAttributeData +{ + public INamedTypeSymbol AssociatedCommandGroup { get; set; } = null!; + public bool IsGreedy { get; set; } + public string Name { get; set; } = null!; + public string Description { get; set; } = null!; + public string[] Aliases { get; set; } = null!; +} \ No newline at end of file diff --git a/src/Mmcc.Bot.SourceGenerators/VSA/VerticalSliceArchitectureGenerator.cs b/src/Mmcc.Bot.SourceGenerators/VSA/VerticalSliceArchitectureGenerator.cs new file mode 100644 index 0000000..0929ade --- /dev/null +++ b/src/Mmcc.Bot.SourceGenerators/VSA/VerticalSliceArchitectureGenerator.cs @@ -0,0 +1,356 @@ +using System.Diagnostics; +using System.Globalization; +using System.Text; + +namespace Mmcc.Bot.SourceGenerators.VSA; + +[Generator] +internal sealed class VerticalSliceArchitectureGenerator : IIncrementalGenerator +{ + public static SymbolDisplayFormat TypeFormat + => SymbolDisplayFormat.FullyQualifiedFormat.WithMiscellaneousOptions( + SymbolDisplayMiscellaneousOptions.UseSpecialTypes | SymbolDisplayMiscellaneousOptions.ExpandNullable | + SymbolDisplayMiscellaneousOptions.IncludeNullableReferenceTypeModifier); + + public void Initialize(IncrementalGeneratorInitializationContext context) + { + context.RegisterPostInitializationOutput(ctx => + ctx.AddSource("MapDiscordCommandAttribute.g.cs", SourceText.From(Attributes.GenerateDiscordFromMediatRAttribute, Encoding.UTF8)) + ); + + var provider = context.SyntaxProvider + .CreateSyntaxProvider(IsVsaClassCandidateSyntactically, SemanticTransform) + .Where(static typesData => typesData.HasValue) + .Select(static (typesData, ct) => GetVSAClassContext(typesData!.Value, ct)) + .Where(static context => context is not null); + + context.RegisterSourceOutput(provider, GenerateSource!); + } + + private static void GenerateSource(SourceProductionContext productionContext, Contexts.VSAClassContext vsaContext) + { + var sanitisedCommandName = vsaContext.DiscordCommandContext.CommandName.Replace("\"", ""); + var methodName = CultureInfo.InvariantCulture.TextInfo.ToTitleCase(sanitisedCommandName); + var props = vsaContext + .RequestClassContext + .Properties; + + var methodParamsString = BuildDiscordCommandMethodParams(props, vsaContext.DiscordCommandContext.IsGreedy); + var requestCtorParams = string.Join(", ", props.Select(p => p.Name.ToCamelCase())); + + var generatedSource = $$""" + // auto-generated + + namespace {{vsaContext.DiscordCommandContext.Namespace}}; + + public partial class {{vsaContext.DiscordCommandContext.ClassName}} + { + [global::Remora.Commands.Attributes.Command({{vsaContext.DiscordCommandContext.CommandName}})] + [global::System.ComponentModel.Description({{vsaContext.DiscordCommandContext.CommandDescription}})] + public async global::System.Threading.Tasks.Task {{methodName}}({{methodParamsString}}) + { + var request = new {{vsaContext.Namespace}}.{{vsaContext.ClassName}}.{{vsaContext.RequestClassContext.ClassName}}({{requestCtorParams}}); + var result = await _mediator.Send(request); + + return result switch + { + { IsSuccess: true, Entity: { } e } + => await _vm.RespondWithView(new {{vsaContext.DiscordCommandContext.MatchedView.Namespace}}.{{vsaContext.DiscordCommandContext.MatchedView.ClassName}}(e)), + {{GenerateNullHandlerIfNeeded(vsaContext)}} + { IsSuccess: false } => result + }; + } + } + """; + + var fileName = $"{vsaContext.Namespace}.{vsaContext.ClassName}.dcmd.g.cs"; + productionContext.AddSource(fileName, generatedSource); + } + + private static string GenerateNullHandlerIfNeeded(Contexts.VSAClassContext vsaClassContext) + { + if (!vsaClassContext.ShouldHandleNullReturn) + return string.Empty; + + var viewContext = vsaClassContext.DiscordCommandContext.MatchedView; + + return !viewContext.HasOnEmpty + ? """ + + { IsSuccess: true } => + global::Remora.Results.Result.FromError(new global::Remora.Results.NotFoundError()), + + """ + : $$""" + + { IsSuccess: true } => + global::Remora.Results.Result.FromError(new global::Remora.Results.NotFoundError({{viewContext.Namespace}}.{{viewContext.ClassName}}.OnEmpty(request))), + + """; + } + + private static string BuildDiscordCommandMethodParams(IReadOnlyList props, bool isGreedy) + { + if (!isGreedy) + return string.Join(", ", props.Select(p => $"{p.Type} {p.Name.ToCamelCase()}")); + + var sb = new StringBuilder(); + for (int i = 0; i < props.Count; i++) + { + if (i == props.Count - 1) + { + sb.Append($"[global::Remora.Commands.Attributes.Greedy] {props[i].Type} {props[i].Name.ToCamelCase()}"); + } + else + { + sb.Append($"{props[i].Type} {props[i].Name.ToCamelCase()}, "); + } + } + + return sb.ToString(); + } + + private static Contexts.VSAClassContext? GetVSAClassContext((INamedTypeSymbol VsaType, INamedTypeSymbol CmdGroupType, INamedTypeSymbol ViewType, AttributeArgumentListSyntax AttributeArguments) typesData, CancellationToken ct) + { + var vsaNamespace = typesData.VsaType.ContainingNamespace.ToDisplayString(); + var vsaName = typesData.VsaType.Name; + + var requestInfo = GetRequestClassInfo(typesData.VsaType); + if (requestInfo is null) + return null; + + var discordCommandContext = GetDiscordCommandContext(typesData.CmdGroupType, typesData.ViewType, + requestInfo.Value.Type, typesData.AttributeArguments); + var shouldHandleNullReturn = GetShouldHandleNullReturn(typesData.VsaType); + + return new Contexts.VSAClassContext + { + Namespace = vsaNamespace, + ClassName = vsaName, + RequestClassContext = requestInfo.Value.Context, + DiscordCommandContext = discordCommandContext, + ShouldHandleNullReturn = shouldHandleNullReturn + }; + } + + private static (INamedTypeSymbol Type, Contexts.RequestClassContext Context)? GetRequestClassInfo(INamedTypeSymbol vsaType) + { + var typeMembers = vsaType.GetTypeMembers(); + INamedTypeSymbol? requestType = null; + requestType ??= typeMembers.FirstOrDefault(x => x.Name.Equals("Query", StringComparison.Ordinal)); + requestType ??= typeMembers.FirstOrDefault(x => x.Name.Equals("Command", StringComparison.Ordinal)); + + if (requestType is null) + return null; + + var @namespace = requestType.ContainingNamespace.ToDisplayString(); + var className = requestType.Name; + var properties = requestType + .GetMembers() + .OfType() + .Where(p => p is + { + Kind: SymbolKind.Property, + DeclaredAccessibility: Accessibility.Public, + IsStatic: false, + SetMethod.IsInitOnly: true + }) + .Select(p => new Contexts.PropertyContext + { + Name = p.Name, + Type = p.Type.ToDisplayString(TypeFormat) + }) + .ToList(); + + var context = new Contexts.RequestClassContext + { + Namespace = @namespace, + ClassName = className, + Properties = properties + }; + + return (requestType, context); + } + + private static Contexts.DiscordCommandContext GetDiscordCommandContext( + INamedTypeSymbol cmdGroupType, + INamedTypeSymbol viewType, + INamedTypeSymbol requestType, + AttributeArgumentListSyntax attributeArgumentsSyntax + ) + { + var @namespace = cmdGroupType.ContainingNamespace.ToDisplayString(); + var className = cmdGroupType.Name; + var args = attributeArgumentsSyntax.Arguments; + var commandName = args[0].Expression.ToFullString(); + var commandDescription = args[1].Expression.ToFullString(); + var isGreedy = args[2].Expression.IsKind(SyntaxKind.TrueLiteralExpression); + var aliases = args + .Skip(3) + .Select(x => x.Expression.ToFullString()) + .ToList(); + + var matchedViewContext = GetViewContext(viewType, requestType); + + return new Contexts.DiscordCommandContext + { + Namespace = @namespace, + ClassName = className, + CommandName = commandName, + CommandDescription = commandDescription, + IsGreedy = isGreedy, + CommandAliases = aliases, + MatchedView = matchedViewContext + }; + } + + private static Contexts.DiscordViewContext GetViewContext(INamedTypeSymbol viewType, INamedTypeSymbol requestType) + { + var @namespace = viewType.ContainingNamespace.ToDisplayString(); + var className = viewType.Name; + var onEmptyMethod = viewType + .GetMembers() + .OfType() + .FirstOrDefault(m => m is + { + Kind: SymbolKind.Method, + MethodKind: MethodKind.Ordinary, + IsStatic: true, + DeclaredAccessibility: Accessibility.Public or Accessibility.Internal, + Name: "OnEmpty", + Parameters: + { + Length: 1 + } parameters + } && SymbolEqualityComparer.Default.Equals(parameters[0].Type, requestType)); + + return new Contexts.DiscordViewContext + { + Namespace = @namespace, + ClassName = className, + HasOnEmpty = onEmptyMethod is not null + }; + } + + private static bool GetShouldHandleNullReturn(INamedTypeSymbol vsaType) + { + var typeMembers = vsaType.GetTypeMembers(); + var handleMethod = typeMembers + .FirstOrDefault(x => x.Name.Equals("Handler", StringComparison.Ordinal))? + .GetMembers() + .OfType() + .FirstOrDefault(m => m is + { + Kind: SymbolKind.Method, + MethodKind: MethodKind.Ordinary, + IsStatic: false, + DeclaredAccessibility: Accessibility.Public or Accessibility.Internal, + Name: "Handle" + }); + if (handleMethod is null) + return false; + + if (!handleMethod.ReturnType.OriginalDefinition.ToDisplayString() + .Equals("System.Threading.Tasks.Task", StringComparison.Ordinal)) + { + return handleMethod.ReturnType.NullableAnnotation == NullableAnnotation.Annotated; + } + + var returnTypeInsideTask = ((INamedTypeSymbol)handleMethod.ReturnType).TypeArguments.FirstOrDefault(); + if (returnTypeInsideTask is null) + return false; // something weird would be going on; + + if (!returnTypeInsideTask.OriginalDefinition.ToDisplayString() + .Equals("Remora.Results.Result", StringComparison.Ordinal)) + { + return returnTypeInsideTask.NullableAnnotation == NullableAnnotation.Annotated; + } + + var resultType = returnTypeInsideTask; + var returnType = ((INamedTypeSymbol)resultType).TypeArguments.FirstOrDefault(); + return returnType?.NullableAnnotation is NullableAnnotation.Annotated; + } + + private static bool IsVsaClassCandidateSyntactically(SyntaxNode node, CancellationToken ct) + => node is ClassDeclarationSyntax + { + AttributeLists.Count: > 0, + BaseList: null or { Types.Count: 0 } + } candidate + && candidate.Modifiers.Any(SyntaxKind.PartialKeyword) + && !candidate.Modifiers.Any(SyntaxKind.StaticKeyword); + + private static (INamedTypeSymbol VsaType, INamedTypeSymbol CmdGroupType, INamedTypeSymbol ViewType, AttributeArgumentListSyntax AttributeArguments)? SemanticTransform(GeneratorSyntaxContext ctx, CancellationToken ct) + { + Debug.Assert(ctx.Node is ClassDeclarationSyntax); + var candidate = Unsafe.As(ctx.Node); + + var symbol = ctx.SemanticModel.GetDeclaredSymbol(candidate, ct); + var generateDiscordAttribute = + ctx.SemanticModel.Compilation.GetTypeByMetadataName("Mmcc.Bot.SourceGenerators.VSA.MapDiscordCommandAttribute`1"); + + if (symbol is not null + && TryGetAttributeData(candidate, generateDiscordAttribute, ctx.SemanticModel, out var attributeData) + && attributeData.HasValue) + { + var viewType = ctx.SemanticModel.Compilation.GetTypeByMetadataName($"{symbol.ContainingNamespace}.{symbol.Name}View"); + + if (viewType is not null) + { + return (symbol, attributeData.Value.CmdGroupType, viewType, attributeData.Value.Arguments); + } + } + + return null; + } + + private static bool TryGetAttributeData( + ClassDeclarationSyntax candidate, + INamedTypeSymbol? target, + SemanticModel semanticModel, + out ( + INamedTypeSymbol CmdGroupType, + AttributeArgumentListSyntax Arguments + )? attributeData + ) + { + foreach (var attributeList in candidate.AttributeLists) + { + foreach (var attribute in attributeList.Attributes) + { + var attributeSymbolInfo = semanticModel.GetSymbolInfo(attribute); + var attributeSymbol = attributeSymbolInfo.Symbol; + + if (attributeSymbol is not null + && SymbolEqualityComparer.Default.Equals(attributeSymbol.ContainingSymbol.OriginalDefinition, target) + && attribute is + { + Name: GenericNameSyntax + { + TypeArgumentList.Arguments: + { + Count: 1 + } typeArguments + }, + ArgumentList: + { + Arguments.Count: >= 3 + } attributeArgumentsSyntax + } + ) + { + var commandGroupSymbolCandidate = semanticModel.GetSymbolInfo(typeArguments[0]).Symbol; + + if (commandGroupSymbolCandidate is INamedTypeSymbol commandGroupSymbol) + { + attributeData = (commandGroupSymbol, attributeArgumentsSyntax); + return true; + } + } + } + } + + attributeData = null; + return false; + } +} diff --git a/src/Mmcc.Bot.sln b/src/Mmcc.Bot.sln index 2d9adea..63728b2 100644 --- a/src/Mmcc.Bot.sln +++ b/src/Mmcc.Bot.sln @@ -27,6 +27,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Mmcc.Bot.Generators.Polycha EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Mmcc.Bot.Common.UI", "Mmcc.Bot.Common.UI\Mmcc.Bot.Common.UI.csproj", "{95A40405-B933-4C00-B13E-CA7B7362F160}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Mmcc.Bot.SourceGenerators", "Mmcc.Bot.SourceGenerators\Mmcc.Bot.SourceGenerators.csproj", "{744B88E6-56D4-4E87-8BEE-37FC053FF5A0}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -73,6 +75,10 @@ Global {95A40405-B933-4C00-B13E-CA7B7362F160}.Debug|Any CPU.Build.0 = Debug|Any CPU {95A40405-B933-4C00-B13E-CA7B7362F160}.Release|Any CPU.ActiveCfg = Release|Any CPU {95A40405-B933-4C00-B13E-CA7B7362F160}.Release|Any CPU.Build.0 = Release|Any CPU + {744B88E6-56D4-4E87-8BEE-37FC053FF5A0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {744B88E6-56D4-4E87-8BEE-37FC053FF5A0}.Debug|Any CPU.Build.0 = Debug|Any CPU + {744B88E6-56D4-4E87-8BEE-37FC053FF5A0}.Release|Any CPU.ActiveCfg = Release|Any CPU + {744B88E6-56D4-4E87-8BEE-37FC053FF5A0}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/src/Mmcc.Bot/Commands/Moderation/GeneralModerationCommands.cs b/src/Mmcc.Bot/Commands/Moderation/GeneralModerationCommands.cs index 2e8f19e..29dc585 100644 --- a/src/Mmcc.Bot/Commands/Moderation/GeneralModerationCommands.cs +++ b/src/Mmcc.Bot/Commands/Moderation/GeneralModerationCommands.cs @@ -24,10 +24,10 @@ namespace Mmcc.Bot.Commands.Moderation; /// /// General moderation commands that do not fit into any specific categories. /// +[RequireGuild] [Group("moderation", "mod")] [Description("Moderation (general)")] -[RequireGuild] -public class GeneralModerationCommands : CommandGroup +public partial class GeneralModerationCommands : CommandGroup { private readonly MessageContext _context; private readonly IMediator _mediator; @@ -106,7 +106,7 @@ public async Task Deactivate(int id) return getAppResult; } - Result deactivateResult = getAppResult.Entity.ModerationActionType switch + Result deactivateResult = getAppResult.Entity?.ModerationActionType switch { ModerationActionType.Ban => await _mediator.Send(new Unban.Command { ModerationAction = getAppResult.Entity }), diff --git a/src/Mmcc.Bot/Mmcc.Bot.csproj b/src/Mmcc.Bot/Mmcc.Bot.csproj index f37304d..0733a5d 100644 --- a/src/Mmcc.Bot/Mmcc.Bot.csproj +++ b/src/Mmcc.Bot/Mmcc.Bot.csproj @@ -36,6 +36,7 @@ + diff --git a/src/Mmcc.Bot/Program.cs b/src/Mmcc.Bot/Program.cs index 98bd2ee..8f3f56d 100644 --- a/src/Mmcc.Bot/Program.cs +++ b/src/Mmcc.Bot/Program.cs @@ -50,8 +50,9 @@ services.AddInMemoryStores(); services.AddBotDatabaseContext(); - services.AddSingleton(); - + // TODO: remove this; + services.AddSingleton(); + // FluentValidation; services.AddValidatorsFromAssemblyContaining(); services.AddValidatorsFromAssemblyContaining(); From 2e14555c64f7bf00a0574030f70b1dc2434a3571 Mon Sep 17 00:00:00 2001 From: TraceLD Date: Sun, 14 May 2023 05:01:29 +0100 Subject: [PATCH 21/30] Add working Remora conditions copying --- src/Mmcc.Bot.SourceGenerators/VSA/Contexts.cs | 1 + .../VSA/VerticalSliceArchitectureGenerator.cs | 93 ++++++++++++++++--- 2 files changed, 79 insertions(+), 15 deletions(-) diff --git a/src/Mmcc.Bot.SourceGenerators/VSA/Contexts.cs b/src/Mmcc.Bot.SourceGenerators/VSA/Contexts.cs index 9429f11..be9c61d 100644 --- a/src/Mmcc.Bot.SourceGenerators/VSA/Contexts.cs +++ b/src/Mmcc.Bot.SourceGenerators/VSA/Contexts.cs @@ -13,6 +13,7 @@ internal sealed class VSAClassContext : ClassContext public RequestClassContext RequestClassContext { get; set; } = null!; public DiscordCommandContext DiscordCommandContext { get; set; } = null!; public bool ShouldHandleNullReturn { get; set; } + public IReadOnlyList RemoraConditionsArguments { get; set; } = null!; } internal sealed class RequestClassContext : ClassContext diff --git a/src/Mmcc.Bot.SourceGenerators/VSA/VerticalSliceArchitectureGenerator.cs b/src/Mmcc.Bot.SourceGenerators/VSA/VerticalSliceArchitectureGenerator.cs index 0929ade..b260813 100644 --- a/src/Mmcc.Bot.SourceGenerators/VSA/VerticalSliceArchitectureGenerator.cs +++ b/src/Mmcc.Bot.SourceGenerators/VSA/VerticalSliceArchitectureGenerator.cs @@ -7,7 +7,7 @@ namespace Mmcc.Bot.SourceGenerators.VSA; [Generator] internal sealed class VerticalSliceArchitectureGenerator : IIncrementalGenerator { - public static SymbolDisplayFormat TypeFormat + private static SymbolDisplayFormat TypeFormat => SymbolDisplayFormat.FullyQualifiedFormat.WithMiscellaneousOptions( SymbolDisplayMiscellaneousOptions.UseSpecialTypes | SymbolDisplayMiscellaneousOptions.ExpandNullable | SymbolDisplayMiscellaneousOptions.IncludeNullableReferenceTypeModifier); @@ -45,6 +45,7 @@ namespace {{vsaContext.DiscordCommandContext.Namespace}}; public partial class {{vsaContext.DiscordCommandContext.ClassName}} { + {{GenerateRemoraConditionArgumentsString(vsaContext.RemoraConditionsArguments)}} [global::Remora.Commands.Attributes.Command({{vsaContext.DiscordCommandContext.CommandName}})] [global::System.ComponentModel.Description({{vsaContext.DiscordCommandContext.CommandDescription}})] public async global::System.Threading.Tasks.Task {{methodName}}({{methodParamsString}}) @@ -89,6 +90,19 @@ private static string GenerateNullHandlerIfNeeded(Contexts.VSAClassContext vsaCl """; } + private static string GenerateRemoraConditionArgumentsString(IEnumerable remoraConditionArguments) + { + const string indent = " "; + var sb = new StringBuilder(); + + foreach (var argument in remoraConditionArguments) + { + sb.AppendLine($"{indent}{argument}"); + } + + return sb.ToString().TrimEnd(); + } + private static string BuildDiscordCommandMethodParams(IReadOnlyList props, bool isGreedy) { if (!isGreedy) @@ -107,10 +121,10 @@ private static string BuildDiscordCommandMethodParams(IReadOnlyList RemoraConditionsArgs) typesData, CancellationToken ct) { var vsaNamespace = typesData.VsaType.ContainingNamespace.ToDisplayString(); var vsaName = typesData.VsaType.Name; @@ -129,7 +143,8 @@ private static string BuildDiscordCommandMethodParams(IReadOnlyList RemoraConditionsArgs)? SemanticTransform(GeneratorSyntaxContext ctx, CancellationToken ct) { Debug.Assert(ctx.Node is ClassDeclarationSyntax); var candidate = Unsafe.As(ctx.Node); @@ -297,7 +312,7 @@ private static (INamedTypeSymbol VsaType, INamedTypeSymbol CmdGroupType, INamedT if (viewType is not null) { - return (symbol, attributeData.Value.CmdGroupType, viewType, attributeData.Value.Arguments); + return (symbol, attributeData.Value.CmdGroupType, viewType, attributeData.Value.Arguments, attributeData.Value.RemoraConditionsArgs); } } @@ -310,10 +325,14 @@ private static bool TryGetAttributeData( SemanticModel semanticModel, out ( INamedTypeSymbol CmdGroupType, - AttributeArgumentListSyntax Arguments + AttributeArgumentListSyntax Arguments, + List RemoraConditionsArgs )? attributeData ) { + INamedTypeSymbol? cmdGroupType = null; + AttributeArgumentListSyntax? targetArguments = null; + var conditionAttributes = new List(); foreach (var attributeList in candidate.AttributeLists) { foreach (var attribute in attributeList.Attributes) @@ -321,9 +340,11 @@ AttributeArgumentListSyntax Arguments var attributeSymbolInfo = semanticModel.GetSymbolInfo(attribute); var attributeSymbol = attributeSymbolInfo.Symbol; - if (attributeSymbol is not null - && SymbolEqualityComparer.Default.Equals(attributeSymbol.ContainingSymbol.OriginalDefinition, target) - && attribute is + if (attributeSymbol is null) + continue; + + // Target attribute; + if (attribute is { Name: GenericNameSyntax { @@ -336,21 +357,63 @@ AttributeArgumentListSyntax Arguments { Arguments.Count: >= 3 } attributeArgumentsSyntax - } + } + && SymbolEqualityComparer.Default.Equals(attributeSymbol.ContainingSymbol.OriginalDefinition, target) ) { var commandGroupSymbolCandidate = semanticModel.GetSymbolInfo(typeArguments[0]).Symbol; if (commandGroupSymbolCandidate is INamedTypeSymbol commandGroupSymbol) { - attributeData = (commandGroupSymbol, attributeArgumentsSyntax); - return true; + cmdGroupType = commandGroupSymbol; + targetArguments = attributeArgumentsSyntax; } } + // Remora conditions; + else if (attribute.Name.ToString().StartsWith("Require")) + { + var symbol = semanticModel.GetSymbolInfo(attribute).Symbol; + if (symbol is not IMethodSymbol methodSymbol) + continue; + + var conditionAttributeType = methodSymbol.ContainingType; + var conditionAttributeNamespace = conditionAttributeType.ContainingNamespace.ToDisplayString(); + if (!conditionAttributeNamespace.StartsWith("Remora.Discord.Commands.Conditions") && !conditionAttributeNamespace.StartsWith("Mmcc.Bot.RemoraAbstractions.Conditions")) + continue; + + if (attribute.ArgumentList is null || attribute.ArgumentList.Arguments.Count == 0) + { + conditionAttributes.Add($"[{conditionAttributeType.ToDisplayString()}]"); + } + else + { + var args = new List(attribute.ArgumentList.Arguments.Count); + foreach (var argSyntax in attribute.ArgumentList.Arguments) + { + var argSymbol = semanticModel.GetSymbolInfo(argSyntax.Expression).Symbol; + if (argSymbol is not IFieldSymbol argFieldSymbol) + continue; + if (argFieldSymbol.Type is not INamedTypeSymbol argType) + continue; + + var argString = argType.EnumUnderlyingType is not null + ? argSymbol.ToDisplayString() + : argSyntax.Expression.ToFullString(); + + args.Add(argString); + } + + var fullAttributeString = $"[{conditionAttributeType.ToDisplayString()}({string.Join(", ", args)})]"; + conditionAttributes.Add(fullAttributeString); + } + } } } - attributeData = null; - return false; + attributeData = cmdGroupType is null || targetArguments is null + ? null + : (cmdGroupType, targetArguments, conditionAttributes); + + return attributeData is not null; } } From a830a359df66368c379452c10fd31043125284cd Mon Sep 17 00:00:00 2001 From: TraceLD Date: Sun, 14 May 2023 06:29:29 +0100 Subject: [PATCH 22/30] Fix a bunch of issues with the Discord commands source generator --- .../{VSA => DiscordCommands}/Attributes.cs | 8 +- .../{VSA => DiscordCommands}/Contexts.cs | 9 +- .../DiscordCommandGenerator.cs} | 159 +++++++++++------- src/Mmcc.Bot.SourceGenerators/GlobalUsings.cs | 3 +- .../Mmcc.Bot.SourceGenerators.csproj | 21 ++- .../VSA/VSAClassAttributesData.cs | 10 -- 6 files changed, 129 insertions(+), 81 deletions(-) rename src/Mmcc.Bot.SourceGenerators/{VSA => DiscordCommands}/Attributes.cs (78%) rename src/Mmcc.Bot.SourceGenerators/{VSA => DiscordCommands}/Contexts.cs (80%) rename src/Mmcc.Bot.SourceGenerators/{VSA/VerticalSliceArchitectureGenerator.cs => DiscordCommands/DiscordCommandGenerator.cs} (76%) delete mode 100644 src/Mmcc.Bot.SourceGenerators/VSA/VSAClassAttributesData.cs diff --git a/src/Mmcc.Bot.SourceGenerators/VSA/Attributes.cs b/src/Mmcc.Bot.SourceGenerators/DiscordCommands/Attributes.cs similarity index 78% rename from src/Mmcc.Bot.SourceGenerators/VSA/Attributes.cs rename to src/Mmcc.Bot.SourceGenerators/DiscordCommands/Attributes.cs index 1e30bb1..2fa11c4 100644 --- a/src/Mmcc.Bot.SourceGenerators/VSA/Attributes.cs +++ b/src/Mmcc.Bot.SourceGenerators/DiscordCommands/Attributes.cs @@ -1,14 +1,14 @@ -namespace Mmcc.Bot.SourceGenerators.VSA; +namespace Mmcc.Bot.SourceGenerators.DiscordCommands; internal static class Attributes { public static string GenerateDiscordFromMediatRAttribute => """ - namespace Mmcc.Bot.SourceGenerators.VSA; + namespace Mmcc.Bot.SourceGenerators.DiscordCommands; [global::System.CodeDom.Compiler.GeneratedCode("Mmcc.Bot.SourceGenerators", "1.0.0")] [global::System.AttributeUsage(global::System.AttributeTargets.Class)] - public class MapDiscordCommandAttribute : global::System.Attribute + public class GenerateDiscordCommandAttribute : global::System.Attribute where TCommandGroup : global::Remora.Commands.Groups.CommandGroup { public string Name { get; set; } @@ -16,7 +16,7 @@ public class MapDiscordCommandAttribute : global::System.Attribut public bool IsGreedy { get; set; } public string[] Aliases { get; set; } - public MapDiscordCommandAttribute( + public GenerateDiscordCommandAttribute( string name, string description, bool isGreedy, diff --git a/src/Mmcc.Bot.SourceGenerators/VSA/Contexts.cs b/src/Mmcc.Bot.SourceGenerators/DiscordCommands/Contexts.cs similarity index 80% rename from src/Mmcc.Bot.SourceGenerators/VSA/Contexts.cs rename to src/Mmcc.Bot.SourceGenerators/DiscordCommands/Contexts.cs index be9c61d..2bd734c 100644 --- a/src/Mmcc.Bot.SourceGenerators/VSA/Contexts.cs +++ b/src/Mmcc.Bot.SourceGenerators/DiscordCommands/Contexts.cs @@ -1,4 +1,4 @@ -namespace Mmcc.Bot.SourceGenerators.VSA; +namespace Mmcc.Bot.SourceGenerators.DiscordCommands; internal sealed class Contexts { @@ -13,7 +13,12 @@ internal sealed class VSAClassContext : ClassContext public RequestClassContext RequestClassContext { get; set; } = null!; public DiscordCommandContext DiscordCommandContext { get; set; } = null!; public bool ShouldHandleNullReturn { get; set; } - public IReadOnlyList RemoraConditionsArguments { get; set; } = null!; + public IReadOnlyList RemoraConditionsAttributeContexts { get; set; } = null!; + } + + internal sealed class ConditionAttributeContext : ClassContext + { + public List? ArgumentsValues { get; set; } } internal sealed class RequestClassContext : ClassContext diff --git a/src/Mmcc.Bot.SourceGenerators/VSA/VerticalSliceArchitectureGenerator.cs b/src/Mmcc.Bot.SourceGenerators/DiscordCommands/DiscordCommandGenerator.cs similarity index 76% rename from src/Mmcc.Bot.SourceGenerators/VSA/VerticalSliceArchitectureGenerator.cs rename to src/Mmcc.Bot.SourceGenerators/DiscordCommands/DiscordCommandGenerator.cs index b260813..603890a 100644 --- a/src/Mmcc.Bot.SourceGenerators/VSA/VerticalSliceArchitectureGenerator.cs +++ b/src/Mmcc.Bot.SourceGenerators/DiscordCommands/DiscordCommandGenerator.cs @@ -2,28 +2,31 @@ using System.Globalization; using System.Text; -namespace Mmcc.Bot.SourceGenerators.VSA; +namespace Mmcc.Bot.SourceGenerators.DiscordCommands; +/// +/// Generates a Discord command from a vertical slice architecture-style parent class. +/// [Generator] -internal sealed class VerticalSliceArchitectureGenerator : IIncrementalGenerator +internal sealed class DiscordCommandGenerator : IIncrementalGenerator { private static SymbolDisplayFormat TypeFormat => SymbolDisplayFormat.FullyQualifiedFormat.WithMiscellaneousOptions( SymbolDisplayMiscellaneousOptions.UseSpecialTypes | SymbolDisplayMiscellaneousOptions.ExpandNullable | SymbolDisplayMiscellaneousOptions.IncludeNullableReferenceTypeModifier); - + public void Initialize(IncrementalGeneratorInitializationContext context) { - context.RegisterPostInitializationOutput(ctx => - ctx.AddSource("MapDiscordCommandAttribute.g.cs", SourceText.From(Attributes.GenerateDiscordFromMediatRAttribute, Encoding.UTF8)) - ); + context.RegisterPostInitializationOutput(ctx => + ctx.AddSource("GenerateDiscordCommandAttribute.g.cs", SourceText.From(Attributes.GenerateDiscordFromMediatRAttribute, Encoding.UTF8)) + ); var provider = context.SyntaxProvider .CreateSyntaxProvider(IsVsaClassCandidateSyntactically, SemanticTransform) .Where(static typesData => typesData.HasValue) .Select(static (typesData, ct) => GetVSAClassContext(typesData!.Value, ct)) .Where(static context => context is not null); - + context.RegisterSourceOutput(provider, GenerateSource!); } @@ -37,7 +40,7 @@ private static void GenerateSource(SourceProductionContext productionContext, Co var methodParamsString = BuildDiscordCommandMethodParams(props, vsaContext.DiscordCommandContext.IsGreedy); var requestCtorParams = string.Join(", ", props.Select(p => p.Name.ToCamelCase())); - + var generatedSource = $$""" // auto-generated @@ -45,7 +48,7 @@ namespace {{vsaContext.DiscordCommandContext.Namespace}}; public partial class {{vsaContext.DiscordCommandContext.ClassName}} { - {{GenerateRemoraConditionArgumentsString(vsaContext.RemoraConditionsArguments)}} + {{GenerateRemoraConditionAttributesString(vsaContext.RemoraConditionsAttributeContexts)}} [global::Remora.Commands.Attributes.Command({{vsaContext.DiscordCommandContext.CommandName}})] [global::System.ComponentModel.Description({{vsaContext.DiscordCommandContext.CommandDescription}})] public async global::System.Threading.Tasks.Task {{methodName}}({{methodParamsString}}) @@ -74,14 +77,14 @@ private static string GenerateNullHandlerIfNeeded(Contexts.VSAClassContext vsaCl return string.Empty; var viewContext = vsaClassContext.DiscordCommandContext.MatchedView; - - return !viewContext.HasOnEmpty + + return !viewContext.HasOnEmpty ? """ { IsSuccess: true } => global::Remora.Results.Result.FromError(new global::Remora.Results.NotFoundError()), - """ + """ : $$""" { IsSuccess: true } => @@ -90,14 +93,21 @@ private static string GenerateNullHandlerIfNeeded(Contexts.VSAClassContext vsaCl """; } - private static string GenerateRemoraConditionArgumentsString(IEnumerable remoraConditionArguments) + private static string GenerateRemoraConditionAttributesString(IReadOnlyList? remoraConditionsAttributeContexts) { + if (remoraConditionsAttributeContexts is null || remoraConditionsAttributeContexts.Count == 0) + return string.Empty; + const string indent = " "; var sb = new StringBuilder(); - foreach (var argument in remoraConditionArguments) + foreach (var attribute in remoraConditionsAttributeContexts) { - sb.AppendLine($"{indent}{argument}"); + var attributeString = attribute.ArgumentsValues is null || attribute.ArgumentsValues.Count == 0 + ? $"[{attribute.Namespace}.{attribute.ClassName}]" + : $"[{attribute.Namespace}.{attribute.ClassName}({string.Join(", ", attribute.ArgumentsValues)})]"; + + sb.AppendLine($"{indent}{attributeString}"); } return sb.ToString().TrimEnd(); @@ -124,17 +134,17 @@ private static string BuildDiscordCommandMethodParams(IReadOnlyList RemoraConditionsArgs) typesData, CancellationToken ct) - { + private static Contexts.VSAClassContext? GetVSAClassContext((INamedTypeSymbol VsaType, INamedTypeSymbol CmdGroupType, INamedTypeSymbol ViewType, AttributeData AttributeData) typesData, CancellationToken ct) + { var vsaNamespace = typesData.VsaType.ContainingNamespace.ToDisplayString(); var vsaName = typesData.VsaType.Name; - + var requestInfo = GetRequestClassInfo(typesData.VsaType); if (requestInfo is null) return null; var discordCommandContext = GetDiscordCommandContext(typesData.CmdGroupType, typesData.ViewType, - requestInfo.Value.Type, typesData.AttributeArguments); + requestInfo.Value.Type, typesData.AttributeData.TargetArguments); var shouldHandleNullReturn = GetShouldHandleNullReturn(typesData.VsaType); return new Contexts.VSAClassContext @@ -144,10 +154,27 @@ private static string BuildDiscordCommandMethodParams(IReadOnlyList GetRemoraConditionsAttributeContexts(AttributeData attributeData) + { + var conditionsAttributes = attributeData.RemoraConditionsAttributes; + var context = conditionsAttributes + .Select(attr => new Contexts.ConditionAttributeContext + { + Namespace = attr.AttributeType.ContainingNamespace.ToDisplayString(), + ClassName = attr.AttributeType.Name, + ArgumentsValues = attr.Arguments? + .Select(arg => arg.Match(symbol => symbol.ToDisplayString(), expression => expression.ToFullString())) + .ToList() + }) + .ToList(); + + return context; + } + private static (INamedTypeSymbol Type, Contexts.RequestClassContext Context)? GetRequestClassInfo(INamedTypeSymbol vsaType) { var typeMembers = vsaType.GetTypeMembers(); @@ -157,7 +184,7 @@ private static (INamedTypeSymbol Type, Contexts.RequestClassContext Context)? Ge if (requestType is null) return null; - + var @namespace = requestType.ContainingNamespace.ToDisplayString(); var className = requestType.Name; var properties = requestType @@ -165,7 +192,7 @@ private static (INamedTypeSymbol Type, Contexts.RequestClassContext Context)? Ge .OfType() .Where(p => p is { - Kind: SymbolKind.Property, + Kind: SymbolKind.Property, DeclaredAccessibility: Accessibility.Public, IsStatic: false, SetMethod.IsInitOnly: true @@ -218,7 +245,7 @@ AttributeArgumentListSyntax attributeArgumentsSyntax MatchedView = matchedViewContext }; } - + private static Contexts.DiscordViewContext GetViewContext(INamedTypeSymbol viewType, INamedTypeSymbol requestType) { var @namespace = viewType.ContainingNamespace.ToDisplayString(); @@ -270,7 +297,7 @@ private static bool GetShouldHandleNullReturn(INamedTypeSymbol vsaType) { return handleMethod.ReturnType.NullableAnnotation == NullableAnnotation.Annotated; } - + var returnTypeInsideTask = ((INamedTypeSymbol)handleMethod.ReturnType).TypeArguments.FirstOrDefault(); if (returnTypeInsideTask is null) return false; // something weird would be going on; @@ -288,23 +315,23 @@ private static bool GetShouldHandleNullReturn(INamedTypeSymbol vsaType) private static bool IsVsaClassCandidateSyntactically(SyntaxNode node, CancellationToken ct) => node is ClassDeclarationSyntax - { - AttributeLists.Count: > 0, - BaseList: null or { Types.Count: 0 } - } candidate + { + AttributeLists.Count: > 0, + BaseList: null or { Types.Count: 0 } + } candidate && candidate.Modifiers.Any(SyntaxKind.PartialKeyword) && !candidate.Modifiers.Any(SyntaxKind.StaticKeyword); - private static (INamedTypeSymbol VsaType, INamedTypeSymbol CmdGroupType, INamedTypeSymbol ViewType, AttributeArgumentListSyntax AttributeArguments, List RemoraConditionsArgs)? SemanticTransform(GeneratorSyntaxContext ctx, CancellationToken ct) + private static (INamedTypeSymbol VsaType, INamedTypeSymbol CmdGroupType, INamedTypeSymbol ViewType, AttributeData AttributeData)? SemanticTransform(GeneratorSyntaxContext ctx, CancellationToken ct) { Debug.Assert(ctx.Node is ClassDeclarationSyntax); var candidate = Unsafe.As(ctx.Node); var symbol = ctx.SemanticModel.GetDeclaredSymbol(candidate, ct); var generateDiscordAttribute = - ctx.SemanticModel.Compilation.GetTypeByMetadataName("Mmcc.Bot.SourceGenerators.VSA.MapDiscordCommandAttribute`1"); + ctx.SemanticModel.Compilation.GetTypeByMetadataName("Mmcc.Bot.SourceGenerators.DiscordCommands.GenerateDiscordCommandAttribute`1"); - if (symbol is not null + if (symbol is not null && TryGetAttributeData(candidate, generateDiscordAttribute, ctx.SemanticModel, out var attributeData) && attributeData.HasValue) { @@ -312,7 +339,7 @@ private static (INamedTypeSymbol VsaType, INamedTypeSymbol CmdGroupType, INamedT if (viewType is not null) { - return (symbol, attributeData.Value.CmdGroupType, viewType, attributeData.Value.Arguments, attributeData.Value.RemoraConditionsArgs); + return (symbol, attributeData.Value.CmdGroupType, viewType, attributeData.Value); } } @@ -323,16 +350,12 @@ private static bool TryGetAttributeData( ClassDeclarationSyntax candidate, INamedTypeSymbol? target, SemanticModel semanticModel, - out ( - INamedTypeSymbol CmdGroupType, - AttributeArgumentListSyntax Arguments, - List RemoraConditionsArgs - )? attributeData + out AttributeData? attributeData ) { INamedTypeSymbol? cmdGroupType = null; AttributeArgumentListSyntax? targetArguments = null; - var conditionAttributes = new List(); + var conditionAttributes = new List<(INamedTypeSymbol, IReadOnlyList>?)>(); foreach (var attributeList in candidate.AttributeLists) { foreach (var attribute in attributeList.Attributes) @@ -342,9 +365,9 @@ List RemoraConditionsArgs if (attributeSymbol is null) continue; - + // Target attribute; - if (attribute is + if (attribute is { Name: GenericNameSyntax { @@ -357,7 +380,7 @@ List RemoraConditionsArgs { Arguments.Count: >= 3 } attributeArgumentsSyntax - } + } && SymbolEqualityComparer.Default.Equals(attributeSymbol.ContainingSymbol.OriginalDefinition, target) ) { @@ -380,40 +403,58 @@ List RemoraConditionsArgs var conditionAttributeNamespace = conditionAttributeType.ContainingNamespace.ToDisplayString(); if (!conditionAttributeNamespace.StartsWith("Remora.Discord.Commands.Conditions") && !conditionAttributeNamespace.StartsWith("Mmcc.Bot.RemoraAbstractions.Conditions")) continue; - + if (attribute.ArgumentList is null || attribute.ArgumentList.Arguments.Count == 0) { - conditionAttributes.Add($"[{conditionAttributeType.ToDisplayString()}]"); + conditionAttributes.Add((conditionAttributeType, null)); } else - { - var args = new List(attribute.ArgumentList.Arguments.Count); + { + var args = new List>(attribute.ArgumentList.Arguments.Count); foreach (var argSyntax in attribute.ArgumentList.Arguments) { var argSymbol = semanticModel.GetSymbolInfo(argSyntax.Expression).Symbol; - if (argSymbol is not IFieldSymbol argFieldSymbol) - continue; - if (argFieldSymbol.Type is not INamedTypeSymbol argType) - continue; - - var argString = argType.EnumUnderlyingType is not null - ? argSymbol.ToDisplayString() - : argSyntax.Expression.ToFullString(); - - args.Add(argString); + if (argSymbol is not IFieldSymbol argFieldSymbol + || argFieldSymbol.Type is not INamedTypeSymbol argType + || argType.EnumUnderlyingType is null + ) + { + args.Add(argSyntax.Expression); + } + else + { + args.Add(OneOf.FromT0(argFieldSymbol)); + } } - var fullAttributeString = $"[{conditionAttributeType.ToDisplayString()}({string.Join(", ", args)})]"; - conditionAttributes.Add(fullAttributeString); - } + conditionAttributes.Add((conditionAttributeType, args)); + } } } } attributeData = cmdGroupType is null || targetArguments is null ? null - : (cmdGroupType, targetArguments, conditionAttributes); + : new AttributeData(cmdGroupType, targetArguments, conditionAttributes); return attributeData is not null; } } + +internal readonly struct AttributeData +{ + internal readonly INamedTypeSymbol CmdGroupType; + internal readonly AttributeArgumentListSyntax TargetArguments; + internal readonly IReadOnlyList<(INamedTypeSymbol AttributeType, IReadOnlyList>? Arguments)> RemoraConditionsAttributes; + + internal AttributeData( + INamedTypeSymbol cmdGroupType, + AttributeArgumentListSyntax targetArguments, + IReadOnlyList<(INamedTypeSymbol, IReadOnlyList>?)> remoraConditionAttributes + ) + { + CmdGroupType = cmdGroupType; + TargetArguments = targetArguments; + RemoraConditionsAttributes = remoraConditionAttributes; + } +} \ No newline at end of file diff --git a/src/Mmcc.Bot.SourceGenerators/GlobalUsings.cs b/src/Mmcc.Bot.SourceGenerators/GlobalUsings.cs index 3e24bfa..e712fbf 100644 --- a/src/Mmcc.Bot.SourceGenerators/GlobalUsings.cs +++ b/src/Mmcc.Bot.SourceGenerators/GlobalUsings.cs @@ -2,4 +2,5 @@ global using Microsoft.CodeAnalysis; global using Microsoft.CodeAnalysis.CSharp; global using Microsoft.CodeAnalysis.CSharp.Syntax; -global using Microsoft.CodeAnalysis.Text; \ No newline at end of file +global using Microsoft.CodeAnalysis.Text; +global using OneOf; \ No newline at end of file diff --git a/src/Mmcc.Bot.SourceGenerators/Mmcc.Bot.SourceGenerators.csproj b/src/Mmcc.Bot.SourceGenerators/Mmcc.Bot.SourceGenerators.csproj index 6d415f2..677284c 100644 --- a/src/Mmcc.Bot.SourceGenerators/Mmcc.Bot.SourceGenerators.csproj +++ b/src/Mmcc.Bot.SourceGenerators/Mmcc.Bot.SourceGenerators.csproj @@ -1,7 +1,8 @@ - + netstandard2.0 + true false enable enable @@ -10,15 +11,25 @@ + all runtime; build; native; contentfiles; analyzers; buildtransitive - + - - - + + $(GetTargetPathDependsOn);GetDependencyTargetPaths + + + + + + + + + + diff --git a/src/Mmcc.Bot.SourceGenerators/VSA/VSAClassAttributesData.cs b/src/Mmcc.Bot.SourceGenerators/VSA/VSAClassAttributesData.cs deleted file mode 100644 index 0fc32f5..0000000 --- a/src/Mmcc.Bot.SourceGenerators/VSA/VSAClassAttributesData.cs +++ /dev/null @@ -1,10 +0,0 @@ -namespace Mmcc.Bot.SourceGenerators.VSA; - -public sealed class DiscordCommandAttributeData -{ - public INamedTypeSymbol AssociatedCommandGroup { get; set; } = null!; - public bool IsGreedy { get; set; } - public string Name { get; set; } = null!; - public string Description { get; set; } = null!; - public string[] Aliases { get; set; } = null!; -} \ No newline at end of file From c735e2aabc9a67e46670a8aa66b6eb74af0d9dbe Mon Sep 17 00:00:00 2001 From: TraceLD Date: Sun, 14 May 2023 06:47:20 +0100 Subject: [PATCH 23/30] Clean up the Discord command source generator --- .../CommonContexts.cs | 16 ++ .../DiscordCommandGenerator.cs | 251 +++++++++--------- ...s => DiscordCommandGeneratorAttributes.cs} | 4 +- ....cs => DiscordCommandGeneratorContexts.cs} | 30 +-- src/Mmcc.Bot.sln.DotSettings | 1 + 5 files changed, 161 insertions(+), 141 deletions(-) create mode 100644 src/Mmcc.Bot.SourceGenerators/CommonContexts.cs rename src/Mmcc.Bot.SourceGenerators/DiscordCommands/{Attributes.cs => DiscordCommandGeneratorAttributes.cs} (90%) rename src/Mmcc.Bot.SourceGenerators/DiscordCommands/{Contexts.cs => DiscordCommandGeneratorContexts.cs} (71%) diff --git a/src/Mmcc.Bot.SourceGenerators/CommonContexts.cs b/src/Mmcc.Bot.SourceGenerators/CommonContexts.cs new file mode 100644 index 0000000..7f89261 --- /dev/null +++ b/src/Mmcc.Bot.SourceGenerators/CommonContexts.cs @@ -0,0 +1,16 @@ +namespace Mmcc.Bot.SourceGenerators; + +public sealed class CommonContexts +{ + internal class ClassContext + { + public string Namespace { get; set; } = null!; + public string ClassName { get; set; } = null!; + } + + internal sealed class PropertyContext + { + public string Type { get; set; } = null!; + public string Name { get; set; } = null!; + } +} \ No newline at end of file diff --git a/src/Mmcc.Bot.SourceGenerators/DiscordCommands/DiscordCommandGenerator.cs b/src/Mmcc.Bot.SourceGenerators/DiscordCommands/DiscordCommandGenerator.cs index 603890a..3ffd580 100644 --- a/src/Mmcc.Bot.SourceGenerators/DiscordCommands/DiscordCommandGenerator.cs +++ b/src/Mmcc.Bot.SourceGenerators/DiscordCommands/DiscordCommandGenerator.cs @@ -1,6 +1,8 @@ using System.Diagnostics; using System.Globalization; using System.Text; +using static Mmcc.Bot.SourceGenerators.CommonContexts; +using static Mmcc.Bot.SourceGenerators.DiscordCommands.DiscordCommandGeneratorContexts; namespace Mmcc.Bot.SourceGenerators.DiscordCommands; @@ -18,123 +20,19 @@ private static SymbolDisplayFormat TypeFormat public void Initialize(IncrementalGeneratorInitializationContext context) { context.RegisterPostInitializationOutput(ctx => - ctx.AddSource("GenerateDiscordCommandAttribute.g.cs", SourceText.From(Attributes.GenerateDiscordFromMediatRAttribute, Encoding.UTF8)) + ctx.AddSource("GenerateDiscordCommandAttribute.g.cs", SourceText.From(DiscordCommandGeneratorAttributes.GenerateDiscordCommandAttribute, Encoding.UTF8)) ); var provider = context.SyntaxProvider .CreateSyntaxProvider(IsVsaClassCandidateSyntactically, SemanticTransform) .Where(static typesData => typesData.HasValue) - .Select(static (typesData, ct) => GetVSAClassContext(typesData!.Value, ct)) + .Select(static (typesData, _) => GetVsaClassContext(typesData!.Value)) .Where(static context => context is not null); context.RegisterSourceOutput(provider, GenerateSource!); } - private static void GenerateSource(SourceProductionContext productionContext, Contexts.VSAClassContext vsaContext) - { - var sanitisedCommandName = vsaContext.DiscordCommandContext.CommandName.Replace("\"", ""); - var methodName = CultureInfo.InvariantCulture.TextInfo.ToTitleCase(sanitisedCommandName); - var props = vsaContext - .RequestClassContext - .Properties; - - var methodParamsString = BuildDiscordCommandMethodParams(props, vsaContext.DiscordCommandContext.IsGreedy); - var requestCtorParams = string.Join(", ", props.Select(p => p.Name.ToCamelCase())); - - var generatedSource = $$""" - // auto-generated - - namespace {{vsaContext.DiscordCommandContext.Namespace}}; - - public partial class {{vsaContext.DiscordCommandContext.ClassName}} - { - {{GenerateRemoraConditionAttributesString(vsaContext.RemoraConditionsAttributeContexts)}} - [global::Remora.Commands.Attributes.Command({{vsaContext.DiscordCommandContext.CommandName}})] - [global::System.ComponentModel.Description({{vsaContext.DiscordCommandContext.CommandDescription}})] - public async global::System.Threading.Tasks.Task {{methodName}}({{methodParamsString}}) - { - var request = new {{vsaContext.Namespace}}.{{vsaContext.ClassName}}.{{vsaContext.RequestClassContext.ClassName}}({{requestCtorParams}}); - var result = await _mediator.Send(request); - - return result switch - { - { IsSuccess: true, Entity: { } e } - => await _vm.RespondWithView(new {{vsaContext.DiscordCommandContext.MatchedView.Namespace}}.{{vsaContext.DiscordCommandContext.MatchedView.ClassName}}(e)), - {{GenerateNullHandlerIfNeeded(vsaContext)}} - { IsSuccess: false } => result - }; - } - } - """; - - var fileName = $"{vsaContext.Namespace}.{vsaContext.ClassName}.dcmd.g.cs"; - productionContext.AddSource(fileName, generatedSource); - } - - private static string GenerateNullHandlerIfNeeded(Contexts.VSAClassContext vsaClassContext) - { - if (!vsaClassContext.ShouldHandleNullReturn) - return string.Empty; - - var viewContext = vsaClassContext.DiscordCommandContext.MatchedView; - - return !viewContext.HasOnEmpty - ? """ - - { IsSuccess: true } => - global::Remora.Results.Result.FromError(new global::Remora.Results.NotFoundError()), - - """ - : $$""" - - { IsSuccess: true } => - global::Remora.Results.Result.FromError(new global::Remora.Results.NotFoundError({{viewContext.Namespace}}.{{viewContext.ClassName}}.OnEmpty(request))), - - """; - } - - private static string GenerateRemoraConditionAttributesString(IReadOnlyList? remoraConditionsAttributeContexts) - { - if (remoraConditionsAttributeContexts is null || remoraConditionsAttributeContexts.Count == 0) - return string.Empty; - - const string indent = " "; - var sb = new StringBuilder(); - - foreach (var attribute in remoraConditionsAttributeContexts) - { - var attributeString = attribute.ArgumentsValues is null || attribute.ArgumentsValues.Count == 0 - ? $"[{attribute.Namespace}.{attribute.ClassName}]" - : $"[{attribute.Namespace}.{attribute.ClassName}({string.Join(", ", attribute.ArgumentsValues)})]"; - - sb.AppendLine($"{indent}{attributeString}"); - } - - return sb.ToString().TrimEnd(); - } - - private static string BuildDiscordCommandMethodParams(IReadOnlyList props, bool isGreedy) - { - if (!isGreedy) - return string.Join(", ", props.Select(p => $"{p.Type} {p.Name.ToCamelCase()}")); - - var sb = new StringBuilder(); - for (int i = 0; i < props.Count; i++) - { - if (i == props.Count - 1) - { - sb.Append($"[global::Remora.Commands.Attributes.Greedy] {props[i].Type} {props[i].Name.ToCamelCase()}"); - } - else - { - sb.Append($"{props[i].Type} {props[i].Name.ToCamelCase()}, "); - } - } - - return sb.ToString().TrimEnd(); - } - - private static Contexts.VSAClassContext? GetVSAClassContext((INamedTypeSymbol VsaType, INamedTypeSymbol CmdGroupType, INamedTypeSymbol ViewType, AttributeData AttributeData) typesData, CancellationToken ct) + private static VsaClassContext? GetVsaClassContext((INamedTypeSymbol VsaType, INamedTypeSymbol CmdGroupType, INamedTypeSymbol ViewType, AttributeData AttributeData) typesData) { var vsaNamespace = typesData.VsaType.ContainingNamespace.ToDisplayString(); var vsaName = typesData.VsaType.Name; @@ -147,7 +45,7 @@ private static string BuildDiscordCommandMethodParams(IReadOnlyList GetRemoraConditionsAttributeContexts(AttributeData attributeData) + private static IReadOnlyList GetRemoraConditionsAttributeContexts(AttributeData attributeData) { var conditionsAttributes = attributeData.RemoraConditionsAttributes; var context = conditionsAttributes - .Select(attr => new Contexts.ConditionAttributeContext + .Select(attr => new ConditionAttributeContext { Namespace = attr.AttributeType.ContainingNamespace.ToDisplayString(), ClassName = attr.AttributeType.Name, @@ -175,7 +73,7 @@ private static string BuildDiscordCommandMethodParams(IReadOnlyList new Contexts.PropertyContext + .Select(p => new PropertyContext { Name = p.Name, Type = p.Type.ToDisplayString(TypeFormat) }) .ToList(); - var context = new Contexts.RequestClassContext + var context = new RequestClassContext { Namespace = @namespace, ClassName = className, @@ -214,7 +112,7 @@ private static (INamedTypeSymbol Type, Contexts.RequestClassContext Context)? Ge return (requestType, context); } - private static Contexts.DiscordCommandContext GetDiscordCommandContext( + private static DiscordCommandContext GetDiscordCommandContext( INamedTypeSymbol cmdGroupType, INamedTypeSymbol viewType, INamedTypeSymbol requestType, @@ -234,7 +132,7 @@ AttributeArgumentListSyntax attributeArgumentsSyntax var matchedViewContext = GetViewContext(viewType, requestType); - return new Contexts.DiscordCommandContext + return new DiscordCommandContext { Namespace = @namespace, ClassName = className, @@ -246,7 +144,7 @@ AttributeArgumentListSyntax attributeArgumentsSyntax }; } - private static Contexts.DiscordViewContext GetViewContext(INamedTypeSymbol viewType, INamedTypeSymbol requestType) + private static DiscordViewContext GetViewContext(INamedTypeSymbol viewType, INamedTypeSymbol requestType) { var @namespace = viewType.ContainingNamespace.ToDisplayString(); var className = viewType.Name; @@ -266,7 +164,7 @@ private static Contexts.DiscordViewContext GetViewContext(INamedTypeSymbol viewT } parameters } && SymbolEqualityComparer.Default.Equals(parameters[0].Type, requestType)); - return new Contexts.DiscordViewContext + return new DiscordViewContext { Namespace = @namespace, ClassName = className, @@ -414,8 +312,7 @@ out AttributeData? attributeData foreach (var argSyntax in attribute.ArgumentList.Arguments) { var argSymbol = semanticModel.GetSymbolInfo(argSyntax.Expression).Symbol; - if (argSymbol is not IFieldSymbol argFieldSymbol - || argFieldSymbol.Type is not INamedTypeSymbol argType + if (argSymbol is not IFieldSymbol { Type: INamedTypeSymbol argType } argFieldSymbol || argType.EnumUnderlyingType is null ) { @@ -439,6 +336,122 @@ out AttributeData? attributeData return attributeData is not null; } + + private static void GenerateSource(SourceProductionContext productionContext, VsaClassContext vsaContext) + { + var sanitisedCommandName = vsaContext.DiscordCommandContext.CommandName.Replace("\"", ""); + var methodName = CultureInfo.InvariantCulture.TextInfo.ToTitleCase(sanitisedCommandName); + var props = vsaContext + .RequestClassContext + .Properties; + + var methodParamsString = GenerateDiscordCommandMethodParams(props, vsaContext.DiscordCommandContext.IsGreedy); + var requestCtorParams = string.Join(", ", props.Select(p => p.Name.ToCamelCase())); + + var generatedSource = $$""" + // auto-generated + + namespace {{vsaContext.DiscordCommandContext.Namespace}}; + + public partial class {{vsaContext.DiscordCommandContext.ClassName}} + { + {{GenerateRemoraConditionAttributesString(vsaContext.RemoraConditionsAttributeContexts)}} + [global::Remora.Commands.Attributes.Command({{vsaContext.DiscordCommandContext.CommandName}}{{GenerateAliasesString(vsaContext.DiscordCommandContext)}})] + [global::System.ComponentModel.Description({{vsaContext.DiscordCommandContext.CommandDescription}})] + public async global::System.Threading.Tasks.Task {{methodName}}({{methodParamsString}}) + { + var request = new {{vsaContext.Namespace}}.{{vsaContext.ClassName}}.{{vsaContext.RequestClassContext.ClassName}}({{requestCtorParams}}); + var result = await _mediator.Send(request); + + return result switch + { + { IsSuccess: true, Entity: { } e } + => await _vm.RespondWithView(new {{vsaContext.DiscordCommandContext.MatchedView.Namespace}}.{{vsaContext.DiscordCommandContext.MatchedView.ClassName}}(e)), + {{GenerateNullHandlerIfNeeded(vsaContext)}} + { IsSuccess: false } => result + }; + } + } + """; + + var fileName = $"{vsaContext.Namespace}.{vsaContext.ClassName}.dcmd.g.cs"; + productionContext.AddSource(fileName, generatedSource); + } + + private static string GenerateAliasesString(DiscordCommandContext discordCommandContext) + { + var aliases = discordCommandContext.CommandAliases; + var aliasesString = aliases.Any() + ? $", {string.Join(", ", aliases)}" + : string.Empty; + + return aliasesString; + } + + private static string GenerateNullHandlerIfNeeded(VsaClassContext vsaClassContext) + { + if (!vsaClassContext.ShouldHandleNullReturn) + return string.Empty; + + var viewContext = vsaClassContext.DiscordCommandContext.MatchedView; + + return !viewContext.HasOnEmpty + ? """ + + { IsSuccess: true } => + global::Remora.Results.Result.FromError(new global::Remora.Results.NotFoundError()), + + """ + : $$""" + + { IsSuccess: true } => + global::Remora.Results.Result.FromError(new global::Remora.Results.NotFoundError({{viewContext.Namespace}}.{{viewContext.ClassName}}.OnEmpty(request))), + + """; + } + + private static string GenerateRemoraConditionAttributesString(IReadOnlyList? remoraConditionsAttributeContexts) + { + if (remoraConditionsAttributeContexts is null || remoraConditionsAttributeContexts.Count == 0) + return string.Empty; + + const string indent = " "; + var sb = new StringBuilder(); + + foreach (var attribute in remoraConditionsAttributeContexts) + { + var attributeString = attribute.ArgumentsValues is null || attribute.ArgumentsValues.Count == 0 + ? $"[{attribute.Namespace}.{attribute.ClassName}]" + : $"[{attribute.Namespace}.{attribute.ClassName}({string.Join(", ", attribute.ArgumentsValues)})]"; + + sb.AppendLine($"{indent}{attributeString}"); + } + + return sb.ToString().TrimEnd(); + } + + private static string GenerateDiscordCommandMethodParams(IReadOnlyList props, bool isGreedy) + { + if (!isGreedy) + return string.Join(", ", props.Select(p => $"{p.Type} {p.Name.ToCamelCase()}")); + + var sb = new StringBuilder(); + for (int i = 0; i < props.Count; i++) + { + // ReSharper disable once ConvertIfStatementToConditionalTernaryExpression + // justification: Trace - cleaner here imo; + if (i == props.Count - 1) + { + sb.Append($"[global::Remora.Commands.Attributes.Greedy] {props[i].Type} {props[i].Name.ToCamelCase()}"); + } + else + { + sb.Append($"{props[i].Type} {props[i].Name.ToCamelCase()}, "); + } + } + + return sb.ToString().TrimEnd(); + } } internal readonly struct AttributeData diff --git a/src/Mmcc.Bot.SourceGenerators/DiscordCommands/Attributes.cs b/src/Mmcc.Bot.SourceGenerators/DiscordCommands/DiscordCommandGeneratorAttributes.cs similarity index 90% rename from src/Mmcc.Bot.SourceGenerators/DiscordCommands/Attributes.cs rename to src/Mmcc.Bot.SourceGenerators/DiscordCommands/DiscordCommandGeneratorAttributes.cs index 2fa11c4..08e8ca8 100644 --- a/src/Mmcc.Bot.SourceGenerators/DiscordCommands/Attributes.cs +++ b/src/Mmcc.Bot.SourceGenerators/DiscordCommands/DiscordCommandGeneratorAttributes.cs @@ -1,8 +1,8 @@ namespace Mmcc.Bot.SourceGenerators.DiscordCommands; -internal static class Attributes +internal static class DiscordCommandGeneratorAttributes { - public static string GenerateDiscordFromMediatRAttribute => + public static string GenerateDiscordCommandAttribute => """ namespace Mmcc.Bot.SourceGenerators.DiscordCommands; diff --git a/src/Mmcc.Bot.SourceGenerators/DiscordCommands/Contexts.cs b/src/Mmcc.Bot.SourceGenerators/DiscordCommands/DiscordCommandGeneratorContexts.cs similarity index 71% rename from src/Mmcc.Bot.SourceGenerators/DiscordCommands/Contexts.cs rename to src/Mmcc.Bot.SourceGenerators/DiscordCommands/DiscordCommandGeneratorContexts.cs index 2bd734c..2e2873b 100644 --- a/src/Mmcc.Bot.SourceGenerators/DiscordCommands/Contexts.cs +++ b/src/Mmcc.Bot.SourceGenerators/DiscordCommands/DiscordCommandGeneratorContexts.cs @@ -1,14 +1,10 @@ -namespace Mmcc.Bot.SourceGenerators.DiscordCommands; +using static Mmcc.Bot.SourceGenerators.CommonContexts; -internal sealed class Contexts +namespace Mmcc.Bot.SourceGenerators.DiscordCommands; + +internal sealed class DiscordCommandGeneratorContexts { - internal class ClassContext - { - public string Namespace { get; set; } = null!; - public string ClassName { get; set; } = null!; - } - - internal sealed class VSAClassContext : ClassContext + internal sealed class VsaClassContext : ClassContext { public RequestClassContext RequestClassContext { get; set; } = null!; public DiscordCommandContext DiscordCommandContext { get; set; } = null!; @@ -16,11 +12,6 @@ internal sealed class VSAClassContext : ClassContext public IReadOnlyList RemoraConditionsAttributeContexts { get; set; } = null!; } - internal sealed class ConditionAttributeContext : ClassContext - { - public List? ArgumentsValues { get; set; } - } - internal sealed class RequestClassContext : ClassContext { public IReadOnlyList Properties { get; set; } = null!; @@ -34,15 +25,14 @@ internal sealed class DiscordCommandContext : ClassContext public IReadOnlyList CommandAliases { get; set; } = null!; public DiscordViewContext MatchedView { get; set; } = null!; } - - internal sealed class DiscordViewContext : ClassContext + + internal sealed class ConditionAttributeContext : ClassContext { - public bool HasOnEmpty { get; set; } + public List? ArgumentsValues { get; set; } } - internal sealed class PropertyContext + internal sealed class DiscordViewContext : ClassContext { - public string Type { get; set; } = null!; - public string Name { get; set; } = null!; + public bool HasOnEmpty { get; set; } } } diff --git a/src/Mmcc.Bot.sln.DotSettings b/src/Mmcc.Bot.sln.DotSettings index 467258b..1dace7c 100644 --- a/src/Mmcc.Bot.sln.DotSettings +++ b/src/Mmcc.Bot.sln.DotSettings @@ -1,5 +1,6 @@  True + True True True True From 8ed902cf9621ccc919b546044e58b72694c51fac Mon Sep 17 00:00:00 2001 From: TraceLD Date: Sun, 14 May 2023 06:50:18 +0100 Subject: [PATCH 24/30] Formatting fix --- .../DiscordCommands/DiscordCommandGenerator.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Mmcc.Bot.SourceGenerators/DiscordCommands/DiscordCommandGenerator.cs b/src/Mmcc.Bot.SourceGenerators/DiscordCommands/DiscordCommandGenerator.cs index 3ffd580..4d155f7 100644 --- a/src/Mmcc.Bot.SourceGenerators/DiscordCommands/DiscordCommandGenerator.cs +++ b/src/Mmcc.Bot.SourceGenerators/DiscordCommands/DiscordCommandGenerator.cs @@ -65,8 +65,8 @@ private static IReadOnlyList GetRemoraConditionsAttri Namespace = attr.AttributeType.ContainingNamespace.ToDisplayString(), ClassName = attr.AttributeType.Name, ArgumentsValues = attr.Arguments? - .Select(arg => arg.Match(symbol => symbol.ToDisplayString(), expression => expression.ToFullString())) - .ToList() + .Select(arg => arg.Match(symbol => symbol.ToDisplayString(), expression => expression.ToFullString())) + .ToList() }) .ToList(); From f18c2ff447a42fa0d80eeda63d550fe7374495f1 Mon Sep 17 00:00:00 2001 From: TraceLD Date: Sun, 14 May 2023 06:56:10 +0100 Subject: [PATCH 25/30] Formatting fix --- .../Mmcc.Bot.SourceGenerators.csproj | 38 +++++++++---------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/src/Mmcc.Bot.SourceGenerators/Mmcc.Bot.SourceGenerators.csproj b/src/Mmcc.Bot.SourceGenerators/Mmcc.Bot.SourceGenerators.csproj index 677284c..6adc69b 100644 --- a/src/Mmcc.Bot.SourceGenerators/Mmcc.Bot.SourceGenerators.csproj +++ b/src/Mmcc.Bot.SourceGenerators/Mmcc.Bot.SourceGenerators.csproj @@ -2,7 +2,7 @@ netstandard2.0 - true + true false enable enable @@ -11,25 +11,25 @@ - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + - - $(GetTargetPathDependsOn);GetDependencyTargetPaths - + + $(GetTargetPathDependsOn);GetDependencyTargetPaths + + + + + + + + + + - - - - - - - - - From 3a2e4c731f90cb605725d03e2ad5b2db872a5712 Mon Sep 17 00:00:00 2001 From: TraceLD Date: Sun, 14 May 2023 07:33:47 +0100 Subject: [PATCH 26/30] Formatting fix --- .../DiscordCommands/DiscordCommandGenerator.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Mmcc.Bot.SourceGenerators/DiscordCommands/DiscordCommandGenerator.cs b/src/Mmcc.Bot.SourceGenerators/DiscordCommands/DiscordCommandGenerator.cs index 4d155f7..57433bf 100644 --- a/src/Mmcc.Bot.SourceGenerators/DiscordCommands/DiscordCommandGenerator.cs +++ b/src/Mmcc.Bot.SourceGenerators/DiscordCommands/DiscordCommandGenerator.cs @@ -337,7 +337,7 @@ out AttributeData? attributeData return attributeData is not null; } - private static void GenerateSource(SourceProductionContext productionContext, VsaClassContext vsaContext) + private static void GenerateSource(SourceProductionContext productionContext, VsaClassContext vsaContext) { var sanitisedCommandName = vsaContext.DiscordCommandContext.CommandName.Replace("\"", ""); var methodName = CultureInfo.InvariantCulture.TextInfo.ToTitleCase(sanitisedCommandName); From e91eda2142b0ec42dc08cb9587e6e45c48557100 Mon Sep 17 00:00:00 2001 From: TraceLD Date: Sun, 14 May 2023 09:23:07 +0100 Subject: [PATCH 27/30] Fix issues with nullability in Discord command source generator --- .../DiscordCommandGenerator.cs | 50 ++++++++++++------- .../DiscordCommandGeneratorAttributes.cs | 2 +- .../Mmcc.Bot.SourceGenerators.csproj | 10 ++-- 3 files changed, 37 insertions(+), 25 deletions(-) diff --git a/src/Mmcc.Bot.SourceGenerators/DiscordCommands/DiscordCommandGenerator.cs b/src/Mmcc.Bot.SourceGenerators/DiscordCommands/DiscordCommandGenerator.cs index 57433bf..333d5db 100644 --- a/src/Mmcc.Bot.SourceGenerators/DiscordCommands/DiscordCommandGenerator.cs +++ b/src/Mmcc.Bot.SourceGenerators/DiscordCommands/DiscordCommandGenerator.cs @@ -1,5 +1,4 @@ -using System.Diagnostics; -using System.Globalization; +using System.Globalization; using System.Text; using static Mmcc.Bot.SourceGenerators.CommonContexts; using static Mmcc.Bot.SourceGenerators.DiscordCommands.DiscordCommandGeneratorContexts; @@ -184,47 +183,60 @@ private static bool GetShouldHandleNullReturn(INamedTypeSymbol vsaType) Kind: SymbolKind.Method, MethodKind: MethodKind.Ordinary, IsStatic: false, - DeclaredAccessibility: Accessibility.Public or Accessibility.Internal, Name: "Handle" }); if (handleMethod is null) return false; - if (!handleMethod.ReturnType.OriginalDefinition.ToDisplayString() + return IsReturnTypeNullable(handleMethod); + } + + private static bool IsReturnTypeNullable(IMethodSymbol method) + { + if (method.ReturnType.OriginalDefinition.ToDisplayString() .Equals("System.Threading.Tasks.Task", StringComparison.Ordinal)) { - return handleMethod.ReturnType.NullableAnnotation == NullableAnnotation.Annotated; + var returnTypeInsideTask = ((INamedTypeSymbol)method.ReturnType).TypeArguments.FirstOrDefault(); + if (returnTypeInsideTask is null) + return false; // something weird would be going on; + + return IsReturnTypeNullable(returnTypeInsideTask); } - var returnTypeInsideTask = ((INamedTypeSymbol)handleMethod.ReturnType).TypeArguments.FirstOrDefault(); - if (returnTypeInsideTask is null) - return false; // something weird would be going on; + return IsReturnTypeNullable(method.ReturnType); + } - if (!returnTypeInsideTask.OriginalDefinition.ToDisplayString() - .Equals("Remora.Results.Result", StringComparison.Ordinal)) + private static bool IsReturnTypeNullable(ITypeSymbol type) + { + if (type.OriginalDefinition.ToDisplayString() + .Equals("Remora.Results.Result", StringComparison.Ordinal)) { - return returnTypeInsideTask.NullableAnnotation == NullableAnnotation.Annotated; + return IsTypeInsideResultNullable(type); } - var resultType = returnTypeInsideTask; + return IsTypeNullable(type); + } + + private static bool IsTypeInsideResultNullable(ITypeSymbol resultType) + { var returnType = ((INamedTypeSymbol)resultType).TypeArguments.FirstOrDefault(); - return returnType?.NullableAnnotation is NullableAnnotation.Annotated; + return IsTypeNullable(returnType); } + private static bool IsTypeNullable(ITypeSymbol? returnType) => returnType?.NullableAnnotation is NullableAnnotation.Annotated; + private static bool IsVsaClassCandidateSyntactically(SyntaxNode node, CancellationToken ct) => node is ClassDeclarationSyntax - { - AttributeLists.Count: > 0, - BaseList: null or { Types.Count: 0 } - } candidate + { + AttributeLists.Count: > 0, + BaseList: null or { Types.Count: 0 } + } candidate && candidate.Modifiers.Any(SyntaxKind.PartialKeyword) && !candidate.Modifiers.Any(SyntaxKind.StaticKeyword); private static (INamedTypeSymbol VsaType, INamedTypeSymbol CmdGroupType, INamedTypeSymbol ViewType, AttributeData AttributeData)? SemanticTransform(GeneratorSyntaxContext ctx, CancellationToken ct) { - Debug.Assert(ctx.Node is ClassDeclarationSyntax); var candidate = Unsafe.As(ctx.Node); - var symbol = ctx.SemanticModel.GetDeclaredSymbol(candidate, ct); var generateDiscordAttribute = ctx.SemanticModel.Compilation.GetTypeByMetadataName("Mmcc.Bot.SourceGenerators.DiscordCommands.GenerateDiscordCommandAttribute`1"); diff --git a/src/Mmcc.Bot.SourceGenerators/DiscordCommands/DiscordCommandGeneratorAttributes.cs b/src/Mmcc.Bot.SourceGenerators/DiscordCommands/DiscordCommandGeneratorAttributes.cs index 08e8ca8..65fed04 100644 --- a/src/Mmcc.Bot.SourceGenerators/DiscordCommands/DiscordCommandGeneratorAttributes.cs +++ b/src/Mmcc.Bot.SourceGenerators/DiscordCommands/DiscordCommandGeneratorAttributes.cs @@ -2,7 +2,7 @@ internal static class DiscordCommandGeneratorAttributes { - public static string GenerateDiscordCommandAttribute => + internal static string GenerateDiscordCommandAttribute => """ namespace Mmcc.Bot.SourceGenerators.DiscordCommands; diff --git a/src/Mmcc.Bot.SourceGenerators/Mmcc.Bot.SourceGenerators.csproj b/src/Mmcc.Bot.SourceGenerators/Mmcc.Bot.SourceGenerators.csproj index 6adc69b..b679a1b 100644 --- a/src/Mmcc.Bot.SourceGenerators/Mmcc.Bot.SourceGenerators.csproj +++ b/src/Mmcc.Bot.SourceGenerators/Mmcc.Bot.SourceGenerators.csproj @@ -11,12 +11,12 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive - + @@ -25,10 +25,10 @@ - + - - + + From e406e9e385c290fc44ee0654810cd8072d644a1c Mon Sep 17 00:00:00 2001 From: Trace Date: Sat, 1 Jul 2023 14:44:11 +0200 Subject: [PATCH 28/30] Add porbeagle submodule --- .gitmodules | 3 +++ src/porbeagle | 1 + 2 files changed, 4 insertions(+) create mode 160000 src/porbeagle diff --git a/.gitmodules b/.gitmodules index 7ca35a9..28319d5 100644 --- a/.gitmodules +++ b/.gitmodules @@ -4,3 +4,6 @@ [submodule "src/Mmcc.Bot.Polychat/Protos"] path = src/Mmcc.Bot.Polychat/Protos url = https://github.com/ModdedMinecraftClub/protos +[submodule "src/porbeagle"] + path = src/porbeagle + url = https://github.com/TraceLD/porbeagle diff --git a/src/porbeagle b/src/porbeagle new file mode 160000 index 0000000..7b6516e --- /dev/null +++ b/src/porbeagle @@ -0,0 +1 @@ +Subproject commit 7b6516e39783ce892b80685ef93750a68c711ee3 From 2c0224bff8511fb8a438699d874a77e3294c72e7 Mon Sep 17 00:00:00 2001 From: Trace Date: Sat, 1 Jul 2023 14:46:21 +0200 Subject: [PATCH 29/30] use the new porbeagle paths --- src/Mmcc.Bot/Mmcc.Bot.csproj | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Mmcc.Bot/Mmcc.Bot.csproj b/src/Mmcc.Bot/Mmcc.Bot.csproj index 0733a5d..6ef3aa5 100644 --- a/src/Mmcc.Bot/Mmcc.Bot.csproj +++ b/src/Mmcc.Bot/Mmcc.Bot.csproj @@ -38,8 +38,8 @@ - - + + From b811752b9fbe64dffe749d318b41b521454083e6 Mon Sep 17 00:00:00 2001 From: TraceLD Date: Sun, 6 Aug 2023 02:59:05 +0100 Subject: [PATCH 30/30] working source gen --- .../DiscordCommandGenerator.cs | 14 +++---- .../DiscordCommandGeneratorAttributes.cs | 8 ++-- .../Diagnostics/DiagnosticsCommands.cs | 41 +++---------------- .../Features/Diagnostics/GetBotDiagnostics.cs | 5 +-- src/Mmcc.Bot/Mmcc.Bot.csproj | 4 +- 5 files changed, 20 insertions(+), 52 deletions(-) diff --git a/src/Mmcc.Bot.SourceGenerators/DiscordCommands/DiscordCommandGenerator.cs b/src/Mmcc.Bot.SourceGenerators/DiscordCommands/DiscordCommandGenerator.cs index 333d5db..d323dc0 100644 --- a/src/Mmcc.Bot.SourceGenerators/DiscordCommands/DiscordCommandGenerator.cs +++ b/src/Mmcc.Bot.SourceGenerators/DiscordCommands/DiscordCommandGenerator.cs @@ -1,4 +1,5 @@ -using System.Globalization; +using System.Diagnostics; +using System.Globalization; using System.Text; using static Mmcc.Bot.SourceGenerators.CommonContexts; using static Mmcc.Bot.SourceGenerators.DiscordCommands.DiscordCommandGeneratorContexts; @@ -19,7 +20,7 @@ private static SymbolDisplayFormat TypeFormat public void Initialize(IncrementalGeneratorInitializationContext context) { context.RegisterPostInitializationOutput(ctx => - ctx.AddSource("GenerateDiscordCommandAttribute.g.cs", SourceText.From(DiscordCommandGeneratorAttributes.GenerateDiscordCommandAttribute, Encoding.UTF8)) + ctx.AddSource("DiscordCommandAttribute.g.cs", SourceText.From(DiscordCommandGeneratorAttributes.DiscordCommandAttribute, Encoding.UTF8)) ); var provider = context.SyntaxProvider @@ -231,18 +232,17 @@ private static bool IsVsaClassCandidateSyntactically(SyntaxNode node, Cancellati AttributeLists.Count: > 0, BaseList: null or { Types.Count: 0 } } candidate - && candidate.Modifiers.Any(SyntaxKind.PartialKeyword) && !candidate.Modifiers.Any(SyntaxKind.StaticKeyword); private static (INamedTypeSymbol VsaType, INamedTypeSymbol CmdGroupType, INamedTypeSymbol ViewType, AttributeData AttributeData)? SemanticTransform(GeneratorSyntaxContext ctx, CancellationToken ct) { var candidate = Unsafe.As(ctx.Node); var symbol = ctx.SemanticModel.GetDeclaredSymbol(candidate, ct); - var generateDiscordAttribute = - ctx.SemanticModel.Compilation.GetTypeByMetadataName("Mmcc.Bot.SourceGenerators.DiscordCommands.GenerateDiscordCommandAttribute`1"); + var discordCmdAttribute = + ctx.SemanticModel.Compilation.GetTypeByMetadataName("Mmcc.Bot.SourceGenerators.DiscordCommands.DiscordCommandAttribute`1"); if (symbol is not null - && TryGetAttributeData(candidate, generateDiscordAttribute, ctx.SemanticModel, out var attributeData) + && TryGetAttributeData(candidate, discordCmdAttribute, ctx.SemanticModel, out var attributeData) && attributeData.HasValue) { var viewType = ctx.SemanticModel.Compilation.GetTypeByMetadataName($"{symbol.ContainingNamespace}.{symbol.Name}View"); @@ -288,7 +288,7 @@ out AttributeData? attributeData }, ArgumentList: { - Arguments.Count: >= 3 + Arguments.Count: >= 2 } attributeArgumentsSyntax } && SymbolEqualityComparer.Default.Equals(attributeSymbol.ContainingSymbol.OriginalDefinition, target) diff --git a/src/Mmcc.Bot.SourceGenerators/DiscordCommands/DiscordCommandGeneratorAttributes.cs b/src/Mmcc.Bot.SourceGenerators/DiscordCommands/DiscordCommandGeneratorAttributes.cs index 65fed04..128864f 100644 --- a/src/Mmcc.Bot.SourceGenerators/DiscordCommands/DiscordCommandGeneratorAttributes.cs +++ b/src/Mmcc.Bot.SourceGenerators/DiscordCommands/DiscordCommandGeneratorAttributes.cs @@ -2,13 +2,13 @@ internal static class DiscordCommandGeneratorAttributes { - internal static string GenerateDiscordCommandAttribute => + internal static string DiscordCommandAttribute => """ namespace Mmcc.Bot.SourceGenerators.DiscordCommands; [global::System.CodeDom.Compiler.GeneratedCode("Mmcc.Bot.SourceGenerators", "1.0.0")] [global::System.AttributeUsage(global::System.AttributeTargets.Class)] - public class GenerateDiscordCommandAttribute : global::System.Attribute + public class DiscordCommandAttribute : global::System.Attribute where TCommandGroup : global::Remora.Commands.Groups.CommandGroup { public string Name { get; set; } @@ -16,10 +16,10 @@ public class GenerateDiscordCommandAttribute : global::System.Att public bool IsGreedy { get; set; } public string[] Aliases { get; set; } - public GenerateDiscordCommandAttribute( + public DiscordCommandAttribute( string name, string description, - bool isGreedy, + bool isGreedy = false, params string[] aliases ) { diff --git a/src/Mmcc.Bot/Features/Diagnostics/DiagnosticsCommands.cs b/src/Mmcc.Bot/Features/Diagnostics/DiagnosticsCommands.cs index dc85b40..1c5c899 100644 --- a/src/Mmcc.Bot/Features/Diagnostics/DiagnosticsCommands.cs +++ b/src/Mmcc.Bot/Features/Diagnostics/DiagnosticsCommands.cs @@ -1,52 +1,21 @@ -using System; -using System.ComponentModel; -using System.Threading.Tasks; +using System.ComponentModel; using MediatR; -using Mmcc.Bot.RemoraAbstractions.Conditions.CommandSpecific; using Porbeagle; using Remora.Commands.Attributes; using Remora.Commands.Groups; -using Remora.Discord.API.Abstractions.Objects; -using Remora.Results; namespace Mmcc.Bot.Features.Diagnostics; [Group("diagnostics")] [Description("Server and bot diagnostics")] -public sealed class DiagnosticsCommands : CommandGroup +public sealed partial class DiagnosticsCommands : CommandGroup { private readonly IMediator _mediator; - private readonly IContextAwareViewManager _viewManager; + private readonly IContextAwareViewManager _vm; - public DiagnosticsCommands(IMediator mediator, IContextAwareViewManager viewManager) + public DiagnosticsCommands(IMediator mediator, IContextAwareViewManager vm) { _mediator = mediator; - _viewManager = viewManager; - } - - [Command("bot")] - [Description("Show status of the bot and APIs it uses")] - public async Task BotDiagnostics() - { - var result = await _mediator.Send(new GetBotDiagnostics.Query()); - - return result switch - { - { IsSuccess: true, Entity: { } pingResults } - => await _viewManager.RespondWithView(new GetBotDiagnosticsView(pingResults)), - - { IsSuccess: false } => result - }; - } - - [Command("drives")] - [Description("Shows drives info (including free space)")] - [RequireGuild] - [RequireUserGuildPermission(DiscordPermission.BanMembers)] - public async Task DrivesDiagnostics() - { - var result = await _mediator.Send(new GetDrivesDiagnostics.Query()); - - return await _viewManager.RespondWithView(new GetDrivesDiagnosticsView(result)); + _vm = vm; } } \ No newline at end of file diff --git a/src/Mmcc.Bot/Features/Diagnostics/GetBotDiagnostics.cs b/src/Mmcc.Bot/Features/Diagnostics/GetBotDiagnostics.cs index 5fa9678..447f2f4 100644 --- a/src/Mmcc.Bot/Features/Diagnostics/GetBotDiagnostics.cs +++ b/src/Mmcc.Bot/Features/Diagnostics/GetBotDiagnostics.cs @@ -4,13 +4,12 @@ using System.Threading; using System.Threading.Tasks; using MediatR; +using Mmcc.Bot.SourceGenerators.DiscordCommands; using Remora.Results; namespace Mmcc.Bot.Features.Diagnostics; -/// -/// Pings all network resources to check specified in . -/// +[DiscordCommand("bot", "Show status of the bot and APIs it uses", isGreedy: false)] public sealed class GetBotDiagnostics { public record struct Query : IRequest>>; diff --git a/src/Mmcc.Bot/Mmcc.Bot.csproj b/src/Mmcc.Bot/Mmcc.Bot.csproj index 6ef3aa5..a144e4e 100644 --- a/src/Mmcc.Bot/Mmcc.Bot.csproj +++ b/src/Mmcc.Bot/Mmcc.Bot.csproj @@ -1,10 +1,10 @@  - net7.0 + net8.0 dotnet-Mmcc.Bot-4BA5AD38-B3B4-456A-853F-080768D88F42 enable - default + preview