diff --git a/Catalogger.Backend/Bot/Commands/ChannelCommands.cs b/Catalogger.Backend/Bot/Commands/ChannelCommands.cs new file mode 100644 index 0000000..fab35c1 --- /dev/null +++ b/Catalogger.Backend/Bot/Commands/ChannelCommands.cs @@ -0,0 +1,224 @@ +using System.ComponentModel; +using Catalogger.Backend.Cache; +using Catalogger.Backend.Database; +using Catalogger.Backend.Database.Queries; +using Catalogger.Backend.Extensions; +using Catalogger.Backend.Services; +using Remora.Commands.Attributes; +using Remora.Commands.Groups; +using Remora.Discord.API.Abstractions.Objects; +using Remora.Discord.API.Objects; +using Remora.Discord.Commands.Attributes; +using Remora.Discord.Commands.Contexts; +using Remora.Discord.Commands.Extensions; +using Remora.Discord.Commands.Feedback.Messages; +using Remora.Discord.Commands.Feedback.Services; +using Remora.Discord.Commands.Services; +using Remora.Discord.Interactivity; +using Remora.Discord.Interactivity.Services; +using Remora.Rest.Core; +using Remora.Results; +using DbGuild = Catalogger.Backend.Database.Models.Guild; +using IResult = Remora.Results.IResult; + +namespace Catalogger.Backend.Bot.Commands; + +public class ChannelCommands( + ILogger logger, + DatabaseContext db, + GuildCacheService guildCache, + ChannelCacheService channelCache, + IFeedbackService feedbackService, + ContextInjectionService contextInjection, + InMemoryDataService dataService) : CommandGroup +{ + private readonly ILogger _logger = logger.ForContext(); + + [Command("configure-channels")] + [Description("Configure log channels for this server.")] + [DiscordDefaultMemberPermissions(DiscordPermission.ManageGuild)] + public async Task ConfigureChannelsAsync() + { + if (contextInjection.Context is not IInteractionCommandContext ctx) throw new CataloggerError("No context"); + if (!ctx.TryGetUserID(out var userId)) throw new CataloggerError("No user ID in context"); + if (!ctx.TryGetGuildID(out var guildId)) throw new CataloggerError("No guild ID in context"); + if (!guildCache.TryGet(guildId, out var guild)) throw new CataloggerError("Guild not in cache"); + var guildChannels = channelCache.GuildChannels(guildId).ToList(); + var guildConfig = await db.GetGuildAsync(guildId); + + var (embeds, components) = BuildRootMenu(guildChannels, guild, guildConfig); + + var msg = await feedbackService.SendContextualAsync(embeds: embeds, + options: new FeedbackMessageOptions(MessageComponents: components)).GetOrThrow(); + + dataService.TryAddData(msg.ID, new ChannelCommandData(userId, CurrentPage: null)); + + return Result.Success; + } + + public static (List, List) BuildRootMenu(List guildChannels, IGuild guild, + DbGuild guildConfig) + { + List embeds = + [ + new Embed( + Title: $"Log channels for {guild.Name}", + Description: "Press one of the buttons below to change the channel for that log type.", + Colour: DiscordUtils.Purple, + Fields: new[] + { + new EmbedField("Server changes", PrettyChannelString(guildConfig.Channels.GuildUpdate), true), + new EmbedField("Emoji changes", PrettyChannelString(guildConfig.Channels.GuildEmojisUpdate), true), + new EmbedField("New roles", PrettyChannelString(guildConfig.Channels.GuildRoleCreate), true), + new EmbedField("Edited roles", PrettyChannelString(guildConfig.Channels.GuildRoleUpdate), true), + new EmbedField("Deleted roles", PrettyChannelString(guildConfig.Channels.GuildRoleDelete), true), + + new EmbedField("New channels", PrettyChannelString(guildConfig.Channels.ChannelCreate), true), + new EmbedField("Edited channels", PrettyChannelString(guildConfig.Channels.ChannelUpdate), true), + new EmbedField("Deleted channels", PrettyChannelString(guildConfig.Channels.ChannelDelete), true), + new EmbedField("Members joining", PrettyChannelString(guildConfig.Channels.GuildMemberAdd), true), + new EmbedField("Members leaving", PrettyChannelString(guildConfig.Channels.GuildMemberRemove), + true), + + new EmbedField("Member role changes", PrettyChannelString(guildConfig.Channels.GuildMemberUpdate), + true), + new EmbedField("Key role changes", PrettyChannelString(guildConfig.Channels.GuildKeyRoleUpdate), + true), + new EmbedField("Member name changes", + PrettyChannelString(guildConfig.Channels.GuildMemberNickUpdate), + true), + new EmbedField("Member avatar changes", + PrettyChannelString(guildConfig.Channels.GuildMemberAvatarUpdate), true), + new EmbedField("Kicks", PrettyChannelString(guildConfig.Channels.GuildMemberKick), true), + + new EmbedField("Bans", PrettyChannelString(guildConfig.Channels.GuildBanAdd), true), + new EmbedField("Unbans", PrettyChannelString(guildConfig.Channels.GuildBanRemove), true), + new EmbedField("New invites", PrettyChannelString(guildConfig.Channels.InviteCreate), true), + new EmbedField("Deleted invites", PrettyChannelString(guildConfig.Channels.InviteDelete), true), + new EmbedField("Edited messages", PrettyChannelString(guildConfig.Channels.MessageUpdate), true), + + new EmbedField("Deleted messages", PrettyChannelString(guildConfig.Channels.MessageDelete), true), + new EmbedField("Bulk deleted messages", PrettyChannelString(guildConfig.Channels.MessageDeleteBulk), + true), + }) + ]; + + List components = + [ + new ActionRowComponent([ + new ButtonComponent(ButtonComponentStyle.Primary, Label: "Server changes", + CustomID: CustomIDHelpers.CreateButtonIDWithState("config-channels", + nameof(LogChannelType.GuildUpdate))), + new ButtonComponent(ButtonComponentStyle.Primary, Label: "Emoji changes", + CustomID: CustomIDHelpers.CreateButtonIDWithState("config-channels", + nameof(LogChannelType.GuildEmojisUpdate))), + new ButtonComponent(ButtonComponentStyle.Primary, Label: "New roles", + CustomID: CustomIDHelpers.CreateButtonIDWithState("config-channels", + nameof(LogChannelType.GuildRoleCreate))), + new ButtonComponent(ButtonComponentStyle.Primary, Label: "Edited roles", + CustomID: CustomIDHelpers.CreateButtonIDWithState("config-channels", + nameof(LogChannelType.GuildRoleUpdate))), + new ButtonComponent(ButtonComponentStyle.Primary, Label: "Deleted roles", + CustomID: CustomIDHelpers.CreateButtonIDWithState("config-channels", + nameof(LogChannelType.GuildRoleDelete))), + ]), + new ActionRowComponent([ + new ButtonComponent(ButtonComponentStyle.Primary, Label: "New channels", + CustomID: CustomIDHelpers.CreateButtonIDWithState("config-channels", + nameof(LogChannelType.ChannelCreate))), + new ButtonComponent(ButtonComponentStyle.Primary, Label: "Edited channels", + CustomID: CustomIDHelpers.CreateButtonIDWithState("config-channels", + nameof(LogChannelType.ChannelUpdate))), + new ButtonComponent(ButtonComponentStyle.Primary, Label: "Deleted channels", + CustomID: CustomIDHelpers.CreateButtonIDWithState("config-channels", + nameof(LogChannelType.ChannelDelete))), + new ButtonComponent(ButtonComponentStyle.Primary, Label: "Members joining", + CustomID: CustomIDHelpers.CreateButtonIDWithState("config-channels", + nameof(LogChannelType.GuildMemberAdd))), + new ButtonComponent(ButtonComponentStyle.Primary, Label: "Members leaving", + CustomID: CustomIDHelpers.CreateButtonIDWithState("config-channels", + nameof(LogChannelType.GuildMemberRemove))), + ]), + new ActionRowComponent([ + new ButtonComponent(ButtonComponentStyle.Primary, Label: "Member role changes", + CustomID: CustomIDHelpers.CreateButtonIDWithState("config-channels", + nameof(LogChannelType.GuildMemberUpdate))), + new ButtonComponent(ButtonComponentStyle.Primary, Label: "Key role changes", + CustomID: CustomIDHelpers.CreateButtonIDWithState("config-channels", + nameof(LogChannelType.GuildKeyRoleUpdate))), + new ButtonComponent(ButtonComponentStyle.Primary, Label: "Member name changes", + CustomID: CustomIDHelpers.CreateButtonIDWithState("config-channels", + nameof(LogChannelType.GuildMemberNickUpdate))), + new ButtonComponent(ButtonComponentStyle.Primary, Label: "Members avatar changes", + CustomID: CustomIDHelpers.CreateButtonIDWithState("config-channels", + nameof(LogChannelType.GuildMemberAvatarUpdate))), + new ButtonComponent(ButtonComponentStyle.Primary, Label: "Kicks", + CustomID: CustomIDHelpers.CreateButtonIDWithState("config-channels", + nameof(LogChannelType.GuildMemberKick))), + ]), + new ActionRowComponent([ + new ButtonComponent(ButtonComponentStyle.Primary, Label: "Bans", + CustomID: CustomIDHelpers.CreateButtonIDWithState("config-channels", + nameof(LogChannelType.GuildBanAdd))), + new ButtonComponent(ButtonComponentStyle.Primary, Label: "Unbans", + CustomID: CustomIDHelpers.CreateButtonIDWithState("config-channels", + nameof(LogChannelType.GuildBanRemove))), + new ButtonComponent(ButtonComponentStyle.Primary, Label: "New invites", + CustomID: CustomIDHelpers.CreateButtonIDWithState("config-channels", + nameof(LogChannelType.InviteCreate))), + new ButtonComponent(ButtonComponentStyle.Primary, Label: "Deleted invites", + CustomID: CustomIDHelpers.CreateButtonIDWithState("config-channels", + nameof(LogChannelType.InviteDelete))), + new ButtonComponent(ButtonComponentStyle.Primary, Label: "Edited messages", + CustomID: CustomIDHelpers.CreateButtonIDWithState("config-channels", + nameof(LogChannelType.MessageUpdate))), + ]), + new ActionRowComponent([ + new ButtonComponent(ButtonComponentStyle.Primary, Label: "Deleted messages", + CustomID: CustomIDHelpers.CreateButtonIDWithState("config-channels", + nameof(LogChannelType.MessageDelete))), + new ButtonComponent(ButtonComponentStyle.Primary, Label: "Bulk deleted messages", + CustomID: CustomIDHelpers.CreateButtonIDWithState("config-channels", + nameof(LogChannelType.MessageDeleteBulk))), + new ButtonComponent(ButtonComponentStyle.Secondary, Label: "Close", + CustomID: CustomIDHelpers.CreateButtonIDWithState("config-channels", "close")), + ]), + ]; + + return (embeds, components); + + string PrettyChannelString(ulong id) + { + if (id == 0) return "Not set"; + if (guildChannels.All(c => c.ID != id)) return $"unknown channel {id}"; + return $"<#{id}>"; + } + } + + public static string PrettyLogTypeName(LogChannelType type) => type switch + { + LogChannelType.GuildUpdate => "Server changes", + LogChannelType.GuildEmojisUpdate => "Emoji changes", + LogChannelType.GuildRoleCreate => "New roles", + LogChannelType.GuildRoleUpdate => "Edited roles", + LogChannelType.GuildRoleDelete => "Deleted roles", + LogChannelType.ChannelCreate => "New channels", + LogChannelType.ChannelUpdate => "Edited channels", + LogChannelType.ChannelDelete => "Deleted channels", + LogChannelType.GuildMemberAdd => "Members joining", + LogChannelType.GuildMemberUpdate => "Members leaving", + LogChannelType.GuildKeyRoleUpdate => "Key role changes", + LogChannelType.GuildMemberNickUpdate => "Member name changes", + LogChannelType.GuildMemberAvatarUpdate => "Member avatar changes", + LogChannelType.GuildMemberRemove => "Members leaving", + LogChannelType.GuildMemberKick => "Kicks", + LogChannelType.GuildBanAdd => "Bans", + LogChannelType.GuildBanRemove => "Unbans", + LogChannelType.InviteCreate => "New invites", + LogChannelType.InviteDelete => "Deleted invites", + LogChannelType.MessageUpdate => "Edited messages", + LogChannelType.MessageDelete => "Deleted messages", + LogChannelType.MessageDeleteBulk => "Bulk deleted messages", + _ => throw new ArgumentOutOfRangeException(nameof(type), type, "Invalid LogChannelType value") + }; +} \ No newline at end of file diff --git a/Catalogger.Backend/Bot/Commands/ChannelCommandsComponents.cs b/Catalogger.Backend/Bot/Commands/ChannelCommandsComponents.cs new file mode 100644 index 0000000..18f6c99 --- /dev/null +++ b/Catalogger.Backend/Bot/Commands/ChannelCommandsComponents.cs @@ -0,0 +1,322 @@ +using Catalogger.Backend.Cache; +using Catalogger.Backend.Database; +using Catalogger.Backend.Database.Queries; +using Catalogger.Backend.Extensions; +using Catalogger.Backend.Services; +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.Feedback.Messages; +using Remora.Discord.Commands.Feedback.Services; +using Remora.Discord.Commands.Services; +using Remora.Discord.Interactivity; +using Remora.Discord.Interactivity.Services; +using Remora.Rest.Core; +using Remora.Results; + +namespace Catalogger.Backend.Bot.Commands; + +public class ChannelCommandsComponents( + ILogger logger, + DatabaseContext db, + GuildCacheService guildCache, + ChannelCacheService channelCache, + ContextInjectionService contextInjection, + IFeedbackService feedbackService, + IDiscordRestInteractionAPI interactionApi, + InMemoryDataService dataService) : InteractionGroup +{ + private readonly ILogger _logger = logger.ForContext(); + + [Button("config-channels")] + [SuppressInteractionResponse(true)] + public async Task OnButtonPressedAsync(string state) + { + if (contextInjection.Context is not IInteractionCommandContext ctx) throw new CataloggerError("No context"); + if (!ctx.TryGetUserID(out var userId)) throw new CataloggerError("No user ID in context"); + if (!ctx.Interaction.Message.TryGet(out var msg)) throw new CataloggerError("No message ID in context"); + if (!ctx.TryGetGuildID(out var guildId)) throw new CataloggerError("No guild ID in context"); + if (!guildCache.TryGet(guildId, out var guild)) throw new CataloggerError("Guild not in cache"); + var guildChannels = channelCache.GuildChannels(guildId).ToList(); + var guildConfig = await db.GetGuildAsync(guildId); + + var result = await dataService.LeaseDataAsync(msg.ID); + await using var lease = result.GetOrThrow(); + if (lease.Data.UserId != userId) + { + return (Result)await feedbackService.SendContextualAsync("This is not your configuration menu.", + options: new FeedbackMessageOptions(MessageFlags: MessageFlags.Ephemeral)); + } + + switch (state) + { + case "close": + return await interactionApi.UpdateMessageAsync(ctx.Interaction, + new InteractionMessageCallbackData(Components: Array.Empty())); + case "reset": + if (lease.Data.CurrentPage == null) + throw new CataloggerError("CurrentPage was null in reset button callback"); + if (!Enum.TryParse(lease.Data.CurrentPage, out var channelType)) + throw new CataloggerError($"Invalid config-channels CurrentPage: '{lease.Data.CurrentPage}'"); + + // TODO: figure out some way to make this less verbose? + switch (channelType) + { + case LogChannelType.GuildUpdate: + guildConfig.Channels.GuildUpdate = 0; + break; + case LogChannelType.GuildEmojisUpdate: + guildConfig.Channels.GuildEmojisUpdate = 0; + break; + case LogChannelType.GuildRoleCreate: + guildConfig.Channels.GuildRoleCreate = 0; + break; + case LogChannelType.GuildRoleUpdate: + guildConfig.Channels.GuildRoleUpdate = 0; + break; + case LogChannelType.GuildRoleDelete: + guildConfig.Channels.GuildRoleDelete = 0; + break; + case LogChannelType.ChannelCreate: + guildConfig.Channels.ChannelCreate = 0; + break; + case LogChannelType.ChannelUpdate: + guildConfig.Channels.ChannelUpdate = 0; + break; + case LogChannelType.ChannelDelete: + guildConfig.Channels.ChannelDelete = 0; + break; + case LogChannelType.GuildMemberAdd: + guildConfig.Channels.GuildMemberAdd = 0; + break; + case LogChannelType.GuildMemberUpdate: + guildConfig.Channels.GuildMemberUpdate = 0; + break; + case LogChannelType.GuildKeyRoleUpdate: + guildConfig.Channels.GuildKeyRoleUpdate = 0; + break; + case LogChannelType.GuildMemberNickUpdate: + guildConfig.Channels.GuildMemberNickUpdate = 0; + break; + case LogChannelType.GuildMemberAvatarUpdate: + guildConfig.Channels.GuildMemberAvatarUpdate = 0; + break; + case LogChannelType.GuildMemberRemove: + guildConfig.Channels.GuildMemberRemove = 0; + break; + case LogChannelType.GuildMemberKick: + guildConfig.Channels.GuildMemberKick = 0; + break; + case LogChannelType.GuildBanAdd: + guildConfig.Channels.GuildBanAdd = 0; + break; + case LogChannelType.GuildBanRemove: + guildConfig.Channels.GuildBanRemove = 0; + break; + case LogChannelType.InviteCreate: + guildConfig.Channels.InviteCreate = 0; + break; + case LogChannelType.InviteDelete: + guildConfig.Channels.InviteDelete = 0; + break; + case LogChannelType.MessageUpdate: + guildConfig.Channels.MessageUpdate = 0; + break; + case LogChannelType.MessageDelete: + guildConfig.Channels.MessageDelete = 0; + break; + case LogChannelType.MessageDeleteBulk: + guildConfig.Channels.MessageDeleteBulk = 0; + break; + default: + throw new ArgumentOutOfRangeException(); + } + + db.Update(guildConfig); + await db.SaveChangesAsync(); + goto case "return"; + case "return": + var (e, c) = ChannelCommands.BuildRootMenu(guildChannels, guild, guildConfig); + await interactionApi.UpdateMessageAsync(ctx.Interaction, + new InteractionMessageCallbackData(Embeds: e, Components: c)); + lease.Data = new ChannelCommandData(userId, CurrentPage: null); + return Result.Success; + } + + if (!Enum.TryParse(state, out var logChannelType)) + throw new CataloggerError($"Invalid config-channels state {state}"); + + var channelId = WebhookExecutorService.GetDefaultLogChannel(guildConfig, logChannelType); + string? channelMention; + if (channelId is null or 0) channelMention = null; + else if (guildChannels.All(c => c.ID != channelId.Value)) channelMention = $"unknown channel {channelId}"; + else channelMention = $"<#{channelId}>"; + + List embeds = + [ + new Embed( + Title: ChannelCommands.PrettyLogTypeName(logChannelType), + Description: channelMention == null + ? "This event is not currently logged.\nTo start logging it somewhere, select a channel below." + : $"This event is currently set to log to {channelMention}." + + "\nTo change where it is logged, select a channel below." + + "\nTo disable logging this event entirely, select \"Stop logging\" below.", + Colour: DiscordUtils.Purple) + ]; + + List components = + [ + new ActionRowComponent(new[] + { + new ChannelSelectComponent(CustomID: CustomIDHelpers.CreateSelectMenuID("config-channels"), + ChannelTypes: new[] { ChannelType.GuildText }) + }), + new ActionRowComponent(new[] + { + new ButtonComponent(ButtonComponentStyle.Danger, Label: "Stop logging", + CustomID: CustomIDHelpers.CreateButtonIDWithState("config-channels", "reset"), + IsDisabled: channelMention == null), + new ButtonComponent(ButtonComponentStyle.Secondary, Label: "Return to menu", + CustomID: CustomIDHelpers.CreateButtonIDWithState("config-channels", "return")) + }) + ]; + + lease.Data = new ChannelCommandData(userId, CurrentPage: state); + return await interactionApi.UpdateMessageAsync(ctx.Interaction, + new InteractionMessageCallbackData(Embeds: embeds, Components: components)); + } + + [SelectMenu("config-channels")] + [SuppressInteractionResponse(true)] + public async Task OnMenuSelectionAsync(IReadOnlyList channels) + { + if (contextInjection.Context is not IInteractionCommandContext ctx) throw new CataloggerError("No context"); + if (!ctx.TryGetUserID(out var userId)) throw new CataloggerError("No user ID in context"); + if (!ctx.Interaction.Message.TryGet(out var msg)) throw new CataloggerError("No message ID in context"); + if (!ctx.TryGetGuildID(out var guildId)) throw new CataloggerError("No guild ID in context"); + if (!guildCache.TryGet(guildId, out var guild)) throw new CataloggerError("Guild not in cache"); + var guildConfig = await db.GetGuildAsync(guildId); + var channelId = channels[0].ID.ToUlong(); + + var result = await dataService.LeaseDataAsync(msg.ID); + await using var lease = result.GetOrThrow(); + if (lease.Data.UserId != userId) + { + return (Result)await feedbackService.SendContextualAsync("This is not your configuration menu.", + options: new FeedbackMessageOptions(MessageFlags: MessageFlags.Ephemeral)); + } + + if (!Enum.TryParse(lease.Data.CurrentPage, out var channelType)) + throw new CataloggerError($"Invalid config-channels CurrentPage '{lease.Data.CurrentPage}'"); + + switch (channelType) + { + case LogChannelType.GuildUpdate: + guildConfig.Channels.GuildUpdate = channelId; + break; + case LogChannelType.GuildEmojisUpdate: + guildConfig.Channels.GuildEmojisUpdate = channelId; + break; + case LogChannelType.GuildRoleCreate: + guildConfig.Channels.GuildRoleCreate = channelId; + break; + case LogChannelType.GuildRoleUpdate: + guildConfig.Channels.GuildRoleUpdate = channelId; + break; + case LogChannelType.GuildRoleDelete: + guildConfig.Channels.GuildRoleDelete = channelId; + break; + case LogChannelType.ChannelCreate: + guildConfig.Channels.ChannelCreate = channelId; + break; + case LogChannelType.ChannelUpdate: + guildConfig.Channels.ChannelUpdate = channelId; + break; + case LogChannelType.ChannelDelete: + guildConfig.Channels.ChannelDelete = channelId; + break; + case LogChannelType.GuildMemberAdd: + guildConfig.Channels.GuildMemberAdd = channelId; + break; + case LogChannelType.GuildMemberUpdate: + guildConfig.Channels.GuildMemberUpdate = channelId; + break; + case LogChannelType.GuildKeyRoleUpdate: + guildConfig.Channels.GuildKeyRoleUpdate = channelId; + break; + case LogChannelType.GuildMemberNickUpdate: + guildConfig.Channels.GuildMemberNickUpdate = channelId; + break; + case LogChannelType.GuildMemberAvatarUpdate: + guildConfig.Channels.GuildMemberAvatarUpdate = channelId; + break; + case LogChannelType.GuildMemberRemove: + guildConfig.Channels.GuildMemberRemove = channelId; + break; + case LogChannelType.GuildMemberKick: + guildConfig.Channels.GuildMemberKick = channelId; + break; + case LogChannelType.GuildBanAdd: + guildConfig.Channels.GuildBanAdd = channelId; + break; + case LogChannelType.GuildBanRemove: + guildConfig.Channels.GuildBanRemove = channelId; + break; + case LogChannelType.InviteCreate: + guildConfig.Channels.InviteCreate = channelId; + break; + case LogChannelType.InviteDelete: + guildConfig.Channels.InviteDelete = channelId; + break; + case LogChannelType.MessageUpdate: + guildConfig.Channels.MessageUpdate = channelId; + break; + case LogChannelType.MessageDelete: + guildConfig.Channels.MessageDelete = channelId; + break; + case LogChannelType.MessageDeleteBulk: + guildConfig.Channels.MessageDeleteBulk = channelId; + break; + default: + throw new ArgumentOutOfRangeException(); + } + + db.Update(guildConfig); + await db.SaveChangesAsync(); + + List embeds = + [ + new Embed( + Title: ChannelCommands.PrettyLogTypeName(channelType), + Description: $"This event is currently set to log to <#{channelId}>." + + "\nTo change where it is logged, select a channel below." + + "\nTo disable logging this event entirely, select \"Stop logging\" below.", + Colour: DiscordUtils.Purple) + ]; + + List components = + [ + new ActionRowComponent(new[] + { + new ChannelSelectComponent(CustomID: CustomIDHelpers.CreateSelectMenuID("config-channels"), + ChannelTypes: new[] { ChannelType.GuildText }) + }), + new ActionRowComponent(new[] + { + new ButtonComponent(ButtonComponentStyle.Danger, Label: "Stop logging", + CustomID: CustomIDHelpers.CreateButtonIDWithState("config-channels", "reset")), + new ButtonComponent(ButtonComponentStyle.Secondary, Label: "Return to menu", + CustomID: CustomIDHelpers.CreateButtonIDWithState("config-channels", "return")) + }) + ]; + + lease.Data = lease.Data with { UserId = userId }; + return await interactionApi.UpdateMessageAsync(ctx.Interaction, + new InteractionMessageCallbackData(Embeds: embeds, Components: components)); + } +} + +public record ChannelCommandData(Snowflake UserId, string? CurrentPage); \ No newline at end of file diff --git a/Catalogger.Backend/Database/Models/Guild.cs b/Catalogger.Backend/Database/Models/Guild.cs index 1c0d11f..681db4b 100644 --- a/Catalogger.Backend/Database/Models/Guild.cs +++ b/Catalogger.Backend/Database/Models/Guild.cs @@ -34,27 +34,27 @@ public class Guild public Dictionary> IgnoredUsersPerChannel { get; init; } = []; public Dictionary Redirects { get; init; } = []; - public ulong GuildUpdate { get; init; } - public ulong GuildEmojisUpdate { get; init; } - public ulong GuildRoleCreate { get; init; } - public ulong GuildRoleUpdate { get; init; } - public ulong GuildRoleDelete { get; init; } - public ulong ChannelCreate { get; init; } - public ulong ChannelUpdate { get; init; } - public ulong ChannelDelete { get; init; } - public ulong GuildMemberAdd { get; init; } - public ulong GuildMemberUpdate { get; init; } - public ulong GuildKeyRoleUpdate { get; init; } - public ulong GuildMemberNickUpdate { get; init; } - public ulong GuildMemberAvatarUpdate { get; init; } - public ulong GuildMemberRemove { get; init; } - public ulong GuildMemberKick { get; init; } - public ulong GuildBanAdd { get; init; } - public ulong GuildBanRemove { get; init; } - public ulong InviteCreate { get; init; } - public ulong InviteDelete { get; init; } - public ulong MessageUpdate { get; init; } - public ulong MessageDelete { get; init; } - public ulong MessageDeleteBulk { get; init; } + public ulong GuildUpdate { get; set; } + public ulong GuildEmojisUpdate { get; set; } + public ulong GuildRoleCreate { get; set; } + public ulong GuildRoleUpdate { get; set; } + public ulong GuildRoleDelete { get; set; } + public ulong ChannelCreate { get; set; } + public ulong ChannelUpdate { get; set; } + public ulong ChannelDelete { get; set; } + public ulong GuildMemberAdd { get; set; } + public ulong GuildMemberUpdate { get; set; } + public ulong GuildKeyRoleUpdate { get; set; } + public ulong GuildMemberNickUpdate { get; set; } + public ulong GuildMemberAvatarUpdate { get; set; } + public ulong GuildMemberRemove { get; set; } + public ulong GuildMemberKick { get; set; } + public ulong GuildBanAdd { get; set; } + public ulong GuildBanRemove { get; set; } + public ulong InviteCreate { get; set; } + public ulong InviteDelete { get; set; } + public ulong MessageUpdate { get; set; } + public ulong MessageDelete { get; set; } + public ulong MessageDeleteBulk { get; set; } } } \ No newline at end of file diff --git a/Catalogger.Backend/Extensions/DiscordExtensions.cs b/Catalogger.Backend/Extensions/DiscordExtensions.cs index 4b3fc04..8caab54 100644 --- a/Catalogger.Backend/Extensions/DiscordExtensions.cs +++ b/Catalogger.Backend/Extensions/DiscordExtensions.cs @@ -1,4 +1,7 @@ +using OneOf; using Remora.Discord.API.Abstractions.Objects; +using Remora.Discord.API.Abstractions.Rest; +using Remora.Discord.API.Objects; using Remora.Rest.Core; using Remora.Results; @@ -49,5 +52,13 @@ public static class DiscordExtensions public static async Task GetOrThrow(this Task> result) => (await result).GetOrThrow(); + public static async Task UpdateMessageAsync(this IDiscordRestInteractionAPI interactionApi, + IInteraction interaction, InteractionMessageCallbackData data) => + await interactionApi.CreateInteractionResponseAsync(interaction.ID, + interaction.Token, + new InteractionResponse(InteractionCallbackType.UpdateMessage, + new Optional>(data))); + public class DiscordRestException(string message) : Exception(message); } \ No newline at end of file diff --git a/Catalogger.Backend/Extensions/StartupExtensions.cs b/Catalogger.Backend/Extensions/StartupExtensions.cs index 990ddc6..16363dc 100644 --- a/Catalogger.Backend/Extensions/StartupExtensions.cs +++ b/Catalogger.Backend/Extensions/StartupExtensions.cs @@ -1,3 +1,4 @@ +using Catalogger.Backend.Bot.Commands; using Catalogger.Backend.Bot.Responders; using Catalogger.Backend.Cache; using Catalogger.Backend.Database; @@ -8,6 +9,8 @@ using NodaTime; using Remora.Discord.API; using Remora.Discord.API.Abstractions.Rest; using Remora.Discord.Commands.Services; +using Remora.Discord.Interactivity.Services; +using Remora.Rest.Core; using Serilog; using Serilog.Events; @@ -71,7 +74,8 @@ public static class StartupExtensions .AddScoped() .AddScoped() .AddSingleton() - .AddSingleton(); + .AddSingleton() + .AddSingleton(InMemoryDataService.Instance); public static async Task Initialize(this WebApplication app) { diff --git a/Catalogger.Backend/Program.cs b/Catalogger.Backend/Program.cs index 71b8e98..1cce447 100644 --- a/Catalogger.Backend/Program.cs +++ b/Catalogger.Backend/Program.cs @@ -37,14 +37,16 @@ builder.Host GatewayIntents.GuildWebhooks | GatewayIntents.MessageContents | GatewayIntents.GuildEmojisAndStickers) - .AddDiscordCommands(enableSlash: true) + .AddDiscordCommands(enableSlash: true, useDefaultCommandResponder: false) .AddCommandTree() // Start command tree .WithCommandGroup() + .WithCommandGroup() // End command tree .Finish() .AddPagination() .AddInteractivity() + .AddInteractionGroup() ); builder.Services diff --git a/Catalogger.Backend/Services/WebhookExecutorService.cs b/Catalogger.Backend/Services/WebhookExecutorService.cs index b6afc26..bb5e6c8 100644 --- a/Catalogger.Backend/Services/WebhookExecutorService.cs +++ b/Catalogger.Backend/Services/WebhookExecutorService.cs @@ -167,7 +167,7 @@ public class WebhookExecutorService( return GetDefaultLogChannel(guild, logChannelType); } - private ulong? GetDefaultLogChannel(Guild guild, LogChannelType channelType) => channelType switch + public static ulong? GetDefaultLogChannel(Guild guild, LogChannelType channelType) => channelType switch { LogChannelType.GuildUpdate => guild.Channels.GuildUpdate, LogChannelType.GuildEmojisUpdate => guild.Channels.GuildEmojisUpdate,