diff --git a/ApplicationCommands/ConfigModule.cs b/ApplicationCommands/ConfigModule.cs index 0800124..7980d5b 100644 --- a/ApplicationCommands/ConfigModule.cs +++ b/ApplicationCommands/ConfigModule.cs @@ -23,6 +23,10 @@ public class ConfigModule : BotModuleBase { private const string HelpSubCmdMessage = "Modify the announcement message."; private const string HelpSubCmdPing = "Set whether to ping users mentioned in the announcement."; + internal const string ModalCidAnnounce = "edit-announce"; + private const string ModalComCidSingle = "msg-single"; + private const string ModalComCidMulti = "msg-multi"; + [SlashCommand("help", "Show information regarding announcement messages.")] public async Task CmdAnnounceHelp() { const string subcommands = @@ -60,11 +64,49 @@ public class ConfigModule : BotModuleBase { [SlashCommand("set-message", HelpPfxModOnly + HelpSubCmdMessage)] public async Task CmdSetMessage() { - // TODO implement this - var pfx = TextCommands.CommandsCommon.CommandPrefix; - await RespondAsync(":x: Sorry, changing the announcement message via slash commands is not yet available. " + - "Please use the corresponding text command: " + - $"`{pfx}config message` for single, `{pfx}config message-pl` for multi.", ephemeral: true); + var gconf = await Context.Guild.GetConfigAsync().ConfigureAwait(false); + + var txtSingle = new TextInputBuilder() { + Label = "Single - Message for one birthday", + CustomId = ModalComCidSingle, + Style = TextInputStyle.Paragraph, + MaxLength = 1500, + Required = false, + Placeholder = BackgroundServices.BirthdayRoleUpdate.DefaultAnnounce, + Value = gconf.AnnounceMessages.Item1 ?? "" + }; + var txtMulti = new TextInputBuilder() { + Label = "Multi - Message for multiple birthdays", + CustomId = ModalComCidMulti, + Style = TextInputStyle.Paragraph, + MaxLength = 1500, + Required = false, + Placeholder = BackgroundServices.BirthdayRoleUpdate.DefaultAnnouncePl, + Value = gconf.AnnounceMessages.Item2 ?? "" + }; + + var form = new ModalBuilder() + .WithTitle("Edit announcement message") + .WithCustomId(ModalCidAnnounce) + .AddTextInput(txtSingle) + .AddTextInput(txtMulti) + .Build(); + + await RespondWithModalAsync(form).ConfigureAwait(false); + } + + internal static async Task CmdSetMessageResponse(SocketModal modal, SocketGuildChannel channel, + Dictionary data) { + string? newSingle = data[ModalComCidSingle].Value; + string? newMulti = data[ModalComCidMulti].Value; + if (string.IsNullOrWhiteSpace(newSingle)) newSingle = null; + if (string.IsNullOrWhiteSpace(newMulti)) newMulti = null; + + var gconf = await channel.Guild.GetConfigAsync().ConfigureAwait(false); + gconf.AnnounceMessages = (newSingle, newMulti); + await gconf.UpdateAsync().ConfigureAwait(false); + + await modal.RespondAsync(":white_check_mark: Announcement messages have been updated."); } [SlashCommand("set-ping", HelpPfxModOnly + HelpSubCmdPing)] diff --git a/ApplicationCommands/ModalResponder.cs b/ApplicationCommands/ModalResponder.cs new file mode 100644 index 0000000..532c884 --- /dev/null +++ b/ApplicationCommands/ModalResponder.cs @@ -0,0 +1,39 @@ +namespace BirthdayBot.ApplicationCommands; + +/// +/// An instance-less class meant to handle incoming submitted modals. +/// +static class ModalResponder { + private delegate Task Responder(SocketModal modal, SocketGuildChannel channel, + Dictionary data); + + internal static async Task DiscordClient_ModalSubmitted(ShardInstance inst, SocketModal arg) { + Responder handler = arg.Data.CustomId switch { + ConfigModule.SubCmdsConfigAnnounce.ModalCidAnnounce => ConfigModule.SubCmdsConfigAnnounce.CmdSetMessageResponse, + _ => DefaultHandler + }; + + var data = arg.Data.Components.ToDictionary(k => k.CustomId); + + if (arg.Channel is not SocketGuildChannel channel) { + inst.Log(nameof(ModalResponder), $"Modal of type `{arg.Data.CustomId}` but channel data unavailable. " + + $"Sender ID {arg.User.Id}, name {arg.User}."); + await arg.RespondAsync(":x: Invalid request. Are you trying this command from a channel the bot can't see?") + .ConfigureAwait(false); + return; + } + + try { + inst.Log(nameof(ModalResponder), $"Modal of type `{arg.Data.CustomId}` at {channel.Guild}!{arg.User}."); + await handler(arg, channel, data).ConfigureAwait(false); + } catch (Exception e) { + inst.Log(nameof(ModalResponder), $"Unhandled exception. {e}"); + // TODO when implementing proper application error logging, see here + await arg.RespondAsync(ShardInstance.InternalError); + } + } + + private static async Task DefaultHandler(SocketModal modal, SocketGuildChannel channel, + Dictionary data) + => await modal.RespondAsync(":x: ...???"); +} diff --git a/ShardInstance.cs b/ShardInstance.cs index 7dac9d2..98078ff 100644 --- a/ShardInstance.cs +++ b/ShardInstance.cs @@ -49,6 +49,7 @@ public sealed class ShardInstance : IDisposable { _interactionService = _services.GetRequiredService(); DiscordClient.InteractionCreated += DiscordClient_InteractionCreated; _interactionService.SlashCommandExecuted += InteractionService_SlashCommandExecuted; + DiscordClient.ModalSubmitted += modal => { return ModalResponder.DiscordClient_ModalSubmitted(this, modal); }; // Background task constructor begins background processing immediately. _background = new ShardBackgroundWorker(this); @@ -75,7 +76,7 @@ public sealed class ShardInstance : IDisposable { Log(nameof(ShardInstance), "Instance disposed."); } - public void Log(string source, string message) => Program.Log($"Shard {ShardId:00}] [{source}", message); + internal void Log(string source, string message) => Program.Log($"Shard {ShardId:00}] [{source}", message); private Task Client_Log(LogMessage arg) { // Suppress certain messages