using Catalogger.Backend.Cache.InMemoryCache; 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, GuildCache guildCache, ChannelCache 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 0) channelMention = null; else if (guildChannels.All(c => c.ID != channelId)) 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);