Instantly share code, notes, and snippets.
Last active
May 27, 2021 03:41
-
Star
0
(0)
You must be signed in to star a gist -
Fork
0
(0)
You must be signed in to fork a gist
-
-
Save VelvetToroyashi/5f8dfcb4968f532a0ba8d6f6e710689a to your computer and use it in GitHub Desktop.
An implementation of an interactive, button-based role menu.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| public async Task CreateInteractive(CommandContext ctx) | |
| { | |
| string buttonIdPrefix = $"{ctx.Message.Id}|{ctx.User.Id}|rolemenu|"; | |
| InteractivityExtension input = ctx.Client.GetInteractivity(); | |
| InteractivityResult<DiscordMessage> messageInput; | |
| InteractivityResult<ComponentInteractionCreateEventArgs> buttonInput; | |
| DiscordFollowupMessageBuilder followupMessageBuilder = new(); | |
| DiscordMessage currentMessage; | |
| DiscordMessage roleMenuDiscordMessage = null!; | |
| string roleMenuTitle = string.Empty; | |
| StringBuilder roleMenuMessage = new(); | |
| List<DiscordComponent> buttons = new(25); | |
| List<(DiscordEmoji, DiscordRole)> zipList = new(25); | |
| //Only used on the first message. | |
| DiscordButtonComponent start = new(ButtonStyle.Success, $"{buttonIdPrefix}init", "Start"); | |
| DiscordButtonComponent no = new(ButtonStyle.Danger, $"{buttonIdPrefix}decline", "No", emoji: new("❌")); | |
| DiscordButtonComponent yes = new(ButtonStyle.Success, $"{buttonIdPrefix}confirm", "Yes", emoji: new("✅")); | |
| DiscordButtonComponent cancel = new(ButtonStyle.Secondary, $"{buttonIdPrefix}abort", "Cancel", emoji: new("⚠️")); | |
| DiscordButtonComponent publish = new(ButtonStyle.Success, $"{buttonIdPrefix}publish", "Publish!", true, new("➡️")); | |
| DiscordButtonComponent preview = new(ButtonStyle.Primary, $"{buttonIdPrefix}preview", "Preview!", emoji: new("📝")); | |
| DiscordButtonComponent add = new(ButtonStyle.Success, $"{buttonIdPrefix}add_option", "Add option (0/25)", emoji: new("➕")); | |
| DiscordButtonComponent remove = new(ButtonStyle.Danger, $"{buttonIdPrefix}remove_option", "Remove option", true, new("➖")); | |
| DiscordButtonComponent update = new(ButtonStyle.Primary, $"{buttonIdPrefix}update_option", "Update option", true, new("🔄")); | |
| DiscordComponent[] YNC = {yes, no, cancel}; | |
| DiscordComponent[] roleMenuOptionsTop = {publish, preview, cancel}; | |
| DiscordComponent[] roleMenuOptionsBottom = {add, remove, update}; | |
| DiscordMessageBuilder builder = new DiscordMessageBuilder() | |
| .WithContent("Press start to start. This message is valid for 10 minutes") | |
| .WithComponents(start); | |
| currentMessage = await builder.SendAsync(ctx.Channel); | |
| buttonInput = await input.WaitForButtonAsync(currentMessage, UserInteractionWaitTimeout); | |
| start.Disabled = true; | |
| builder.WithContent("Rolemenu setup in progress."); | |
| await currentMessage.ModifyAsync(builder); | |
| if (buttonInput.TimedOut) | |
| { | |
| await ctx.RespondAsync($"{ctx.User.Mention} your setup has timed out."); | |
| return; | |
| } | |
| await buttonInput.Result.Interaction.CreateResponseAsync(InteractionResponseType.DefferedMessageUpdate); | |
| currentMessage = await buttonInput.Result.Interaction.CreateFollowupMessageAsync(followupMessageBuilder.WithContent("All good role menus start with a name. What's this one's?")); | |
| bool roleMenuNameResult = await GetRoleMenuNameAsync(); | |
| if (!roleMenuNameResult) return; | |
| await Task.Delay(MessageUserReadWaitDelay); | |
| var econ = (IArgumentConverter<DiscordEmoji>) new DiscordEmojiConverter(); | |
| var rcon = (IArgumentConverter<DiscordRole>) new DiscordRoleConverter(); | |
| while (true) | |
| { | |
| await currentMessage.ModifyAsync(m => m.WithContent("What would you like to do?").WithComponents(roleMenuOptionsTop).WithComponents(roleMenuOptionsBottom)); | |
| buttonInput = await input.WaitForButtonAsync(currentMessage, UserInteractionWaitTimeout); | |
| if (buttonInput.TimedOut) | |
| { | |
| await SendTimeoutMessageAsync(); | |
| return; | |
| } | |
| await buttonInput.Result.Interaction.CreateResponseAsync(InteractionResponseType.DefferedMessageUpdate); | |
| if (buttonInput.Result.Id.EndsWith("abort")) | |
| await currentMessage.ModifyAsync(m => m.WithContent("Aborted.")); | |
| if (buttonInput.Result.Id.EndsWith("remove_option")) | |
| await RemoveOptionAsync(); | |
| if (buttonInput.Result.Id.EndsWith("preview")) | |
| await PreviewRoleMenuAsync(); | |
| if (buttonInput.Result.Id.EndsWith("add_option")) | |
| await TryAddOptionAsync(); | |
| if (buttonInput.Result.Id.EndsWith("publish")) | |
| { | |
| bool messagePublished = await ShowPublishAsync(); | |
| if (!messagePublished) | |
| continue; | |
| await currentMessage.ModifyAsync(m => m.WithContent($"Congratulations! Your role menu is set up. You can find it here: \n{roleMenuDiscordMessage.JumpLink}")); | |
| //TODO: IMPLEMENT DATABASE STUFF | |
| return; | |
| } | |
| if (buttonInput.Result.Id.EndsWith("update_option")) | |
| await UpdateOptionAsync(); | |
| } | |
| async Task UpdateOptionAsync() | |
| { | |
| while (true) | |
| { | |
| // I could've used buttons here, but I'm lazy. // | |
| await currentMessage.ModifyAsync(m => m.WithContent("Please format your message as such: `<old emoji> <old role> <new emoji> <new role>`! Or type cancel to cancel.")); | |
| messageInput = await input.WaitForMessageAsync(m => m.Author == ctx.User); | |
| if (messageInput.TimedOut) | |
| { | |
| await currentMessage.ModifyAsync("Sorry, but you took too long!"); | |
| await Task.Delay(MessageUserReadWaitDelay / 4); | |
| return; | |
| } | |
| if (string.Equals("cancel", messageInput.Result.Content, StringComparison.OrdinalIgnoreCase)) | |
| return; | |
| string[] split = messageInput.Result.Content.Split(' ', StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries); | |
| if (split.Length is not 4) | |
| { | |
| await currentMessage.ModifyAsync("Sorry, but you provided incorrect parmeters!"); | |
| await Task.Delay(MessageUserReadWaitDelay / 4); | |
| continue; | |
| } | |
| string oldemoteraw = split[0]; | |
| string oldroleraw = split[1]; | |
| string newemoteraw = split[2]; | |
| string newroleraw = split[3]; | |
| Optional<DiscordEmoji> oldEmoji; | |
| Optional<DiscordEmoji> newEmoji; | |
| Optional<DiscordRole> oldRole; | |
| Optional<DiscordRole> newRole; | |
| oldEmoji = await econ.ConvertAsync(oldemoteraw, ctx); | |
| oldRole = await rcon.ConvertAsync(oldroleraw, ctx); | |
| newEmoji = await econ.ConvertAsync(newemoteraw, ctx); | |
| newRole = await rcon.ConvertAsync(newroleraw, ctx); | |
| if (!await TryCheckValue(oldEmoji, "emoji")) return; | |
| if (!await TryCheckValue(newEmoji, "emoji")) return; | |
| if (!await TryCheckValue(oldRole, "role")) return; | |
| if (!await TryCheckValue(newRole, "role")) return; | |
| if (oldEmoji == newEmoji || oldRole == newRole) | |
| { | |
| await currentMessage.ModifyAsync("New option must be different from old option!"); | |
| await Task.Delay(MessageUserReadWaitDelay / 4); | |
| } | |
| else | |
| { | |
| int index = zipList.IndexOf((oldEmoji.Value, oldRole.Value)); | |
| if (index is -1) | |
| { | |
| await currentMessage.ModifyAsync("That option doesn't exist!"); | |
| await Task.Delay(MessageUserReadWaitDelay / 4); | |
| } | |
| else | |
| { | |
| zipList[index] = (newEmoji.Value, newRole.Value); | |
| await currentMessage.ModifyAsync("Done!"); | |
| await Task.Delay(MessageUserReadWaitDelay / 2); | |
| } | |
| } | |
| } | |
| async Task<bool> TryCheckValue<T>(Optional<T> valueContainer, string valueLabel) | |
| { | |
| if (valueContainer.HasValue) | |
| return true; | |
| await currentMessage.ModifyAsync($"Sorry but that's not a valid {valueLabel}! Please try again."); | |
| await Task.Delay(MessageUserReadWaitDelay / 4); | |
| return false; | |
| } | |
| } | |
| // Returns whether the menu was published. // | |
| async Task<bool> ShowPublishAsync() | |
| { | |
| await currentMessage.ModifyAsync(m => m.WithContent("Are you sure you want to publish?").WithComponents(yes, no)); | |
| buttonInput = await input.WaitForButtonAsync(currentMessage, ctx.User); | |
| if (buttonInput.TimedOut) | |
| { | |
| await currentMessage.ModifyAsync(m => m.WithContent("Sorry, but you took too long!")); | |
| await Task.Delay(MessageUserReadWaitDelay / 2); | |
| return false; | |
| } | |
| if (buttonInput.Result.Id.EndsWith("decline")) | |
| return false; | |
| if (buttonInput.Result.Id.EndsWith("confirm")) | |
| { | |
| while (true) | |
| { | |
| await currentMessage.ModifyAsync(m => m.WithContent("So, what channel would you like to publish to? Alternatively type cancel to cancel.")); | |
| messageInput = await input.WaitForMessageAsync(m => m.MentionedChannels.Count > 0 || | |
| string.Equals("cancel", m.Content, StringComparison.OrdinalIgnoreCase) | |
| && m.Author == ctx.User, UserInteractionWaitTimeout); | |
| if (messageInput.TimedOut) | |
| { | |
| await currentMessage.ModifyAsync("Sorry, but you took to long to respond!"); | |
| await Task.Delay(MessageUserReadWaitDelay / 4); | |
| return false; | |
| } | |
| if (string.Equals("cancel", messageInput.Result.Content, StringComparison.OrdinalIgnoreCase)) | |
| return false; | |
| DiscordChannel chn = messageInput.Result.MentionedChannels[0]; | |
| if (chn.Type is not ChannelType.Text) | |
| { | |
| await currentMessage.ModifyAsync("Sorry, but you can only publish to a text channel!"); | |
| await Task.Delay(MessageUserReadWaitDelay / 4); | |
| continue; | |
| } | |
| if (!chn.PermissionsFor(ctx.Guild.CurrentMember).HasFlag(Permissions.SendMessages)) | |
| { | |
| await currentMessage.ModifyAsync("I can't send messages to that channel!"); | |
| await Task.Delay(MessageUserReadWaitDelay / 4); | |
| continue; | |
| } | |
| await currentMessage.ModifyAsync("Alright!"); | |
| await Task.Delay(MessageUserReadWaitDelay / 2); | |
| break; | |
| } | |
| builder.Clear(); | |
| builder | |
| .WithoutMentions() | |
| .WithContent(BuildRoleMenuMessage().ToString()); | |
| foreach (var chk in buttons.Chunk(5)) | |
| builder.WithComponents(chk.ToArray()); | |
| try | |
| { | |
| roleMenuDiscordMessage = await builder.SendAsync(messageInput.Result.MentionedChannels[0]); | |
| return true; | |
| } | |
| catch (Exception e) | |
| { | |
| await currentMessage.ModifyAsync($"I'm so sorry! Something went wrong when trying to publish your message. :( \nThe response was `{e.Message}`. Feel free to shoot this to the developers!"); | |
| return false; | |
| } | |
| } | |
| return false; | |
| StringBuilder BuildRoleMenuMessage() | |
| { | |
| roleMenuMessage.Clear(); | |
| roleMenuMessage.AppendLine(roleMenuTitle); | |
| foreach ((DiscordEmoji e, DiscordRole r) in zipList) | |
| roleMenuMessage.AppendLine($"{e} **→** {r.Mention}"); | |
| return roleMenuMessage; | |
| } | |
| } | |
| async Task SendTimeoutMessageAsync() | |
| { | |
| await currentMessage.ModifyAsync("Timed out."); | |
| await ctx.RespondAsync($"{ctx.User.Mention} your rolemenu setup has timed out!"); | |
| } | |
| async Task RemoveOptionAsync() | |
| { | |
| if (!buttons.Any()) | |
| await buttonInput.Result.Interaction.CreateFollowupMessageAsync(followupMessageBuilder.WithContent("You shouldn't be able to do this!")); | |
| builder.Clear(); | |
| builder.WithContent($"Alright, what would you like to remove? Or type cancel to cancel. \n{string.Join('\n', zipList.Select((z, i) => $"**{i}**: {z.Item1} → {z.Item2.Mention}"))}"); | |
| var chnk = buttons.Chunk(5); | |
| foreach (var chunk in chnk) | |
| builder.WithComponents(chunk.ToArray()); | |
| await currentMessage.ModifyAsync(builder); | |
| Task<InteractivityResult<DiscordMessage>> cancelInput = input.WaitForMessageAsync(m => string.Equals("cancel", m.Content, StringComparison.OrdinalIgnoreCase) && m.Author == ctx.User, UserInteractionWaitTimeout); | |
| Task<InteractivityResult<ComponentInteractionCreateEventArgs>> buttonInputTask = input.WaitForButtonAsync(currentMessage); | |
| Task inputResult = await Task.WhenAny(cancelInput, buttonInputTask); | |
| if (inputResult == cancelInput) | |
| { | |
| await currentMessage.ModifyAsync(m => m.WithContent("Alright.")); | |
| return; | |
| } | |
| if (inputResult == buttonInputTask) | |
| { | |
| buttonInput = await buttonInputTask; | |
| DiscordComponent componentToRemove = buttons.Single(b => b.CustomId == buttonInput.Result.Id); | |
| buttons.Remove(componentToRemove); | |
| await currentMessage.ModifyAsync(m => m.WithContent("Alright! Done.")); | |
| await Task.Delay(MessageUserReadWaitDelay / 2); | |
| } | |
| if (buttons.Count is 0) | |
| { | |
| remove.Disabled = true; | |
| publish.Disabled = true; | |
| } | |
| } | |
| async Task PreviewRoleMenuAsync() | |
| { | |
| List<List<DiscordComponent>> opts = buttons.Chunk(5); | |
| followupMessageBuilder.Clear(); | |
| followupMessageBuilder.WithContent($"Your menu looks like this so far: \n{roleMenuTitle}\n{roleMenuMessage}\n{string.Join('\n', zipList.Select(z => $"{z.Item1} → {z.Item2.Mention}"))}").AsEphemeral(true); | |
| foreach (var componentList in opts) | |
| followupMessageBuilder.WithComponents(componentList); | |
| await buttonInput.Result.Interaction.CreateFollowupMessageAsync(followupMessageBuilder); | |
| } | |
| async Task TryAddOptionAsync() | |
| { | |
| do | |
| { | |
| await currentMessage.ModifyAsync(m => m.WithContent("What would you like to add?\n\n Note: the format`<emoji> <role>`! Place a space in between or I will not add it!")); | |
| messageInput = await input.WaitForMessageAsync(m => m.Author == ctx.User); | |
| if (messageInput.TimedOut) | |
| { | |
| await currentMessage.ModifyAsync("Sorry, but you took too long!"); | |
| return; | |
| } | |
| string[] rSplit = messageInput.Result.Content.Split(' ', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); | |
| if (rSplit.Length is not 2) | |
| { | |
| await currentMessage.ModifyAsync("Sorry but you provided an incorrect amount of paremeters! Did you forgot a space?"); | |
| await Task.Delay(MessageUserReadWaitDelay / 2); | |
| continue; | |
| } | |
| var emojiRaw = rSplit[0]; | |
| var roleRaw = rSplit[1]; | |
| var emojiRes = await econ.ConvertAsync(emojiRaw, ctx); | |
| var roleRes = await rcon.ConvertAsync(roleRaw, ctx); | |
| if (!emojiRes.HasValue) | |
| { | |
| await currentMessage.ModifyAsync("Sorry but that's not a valid emoji! Please try again."); | |
| await Task.Delay(MessageUserReadWaitDelay / 2); | |
| continue; | |
| } | |
| if (!roleRes.HasValue) | |
| { | |
| await currentMessage.ModifyAsync("Sorry but that's not a valid role! Please try again."); | |
| await Task.Delay(MessageUserReadWaitDelay / 2); | |
| continue; | |
| } | |
| if (roleRes.Value.Position >= ctx.Guild.CurrentMember.Hierarchy) | |
| { | |
| await currentMessage.ModifyAsync("Sorry, but I can't assign that role!"); | |
| await Task.Delay(MessageUserReadWaitDelay / 2); | |
| continue; | |
| } | |
| if (zipList.Any(r => r.Item1 == emojiRes.Value || r.Item2 == roleRes.Value)) | |
| { | |
| await currentMessage.ModifyAsync("You've already used that role or emoji!"); | |
| await Task.Delay(MessageUserReadWaitDelay / 2); | |
| continue; | |
| } | |
| if (!emojiRes.Value.IsAvailable && emojiRes.Value.Id is not 0) | |
| { | |
| await currentMessage.ModifyAsync(m => m.WithContent("That's a custom emote from a server I'm not in! It won't render in the message, but the button will still work. \nDo you want to use it anyway?").WithComponents(yes, no)); | |
| buttonInput = await input.WaitForButtonAsync(currentMessage, UserInteractionWaitTimeout); | |
| if (buttonInput.TimedOut) | |
| { | |
| await currentMessage.ModifyAsync("Sorry, but you took to long to respond!"); | |
| await Task.Delay(MessageUserReadWaitDelay / 2); | |
| return; | |
| } | |
| await buttonInput.Result.Interaction.CreateResponseAsync(InteractionResponseType.DefferedMessageUpdate); | |
| if (buttonInput.Result.Id.EndsWith("decline")) | |
| { | |
| await currentMessage.ModifyAsync(m => m.WithContent("Alright then.")); | |
| } | |
| else | |
| { | |
| await currentMessage.ModifyAsync(m => m.WithContent("Alright!")); | |
| await messageInput.Result.DeleteAsync(); | |
| await Task.Delay(MessageUserReadWaitDelay / 4); | |
| zipList.Add((emojiRes.Value, roleRes.Value)); | |
| buttons.Add(new DiscordButtonComponent(ButtonStyle.Success, $"rolemenu assign {roleRes.Value.Mention}", "", emoji: new(emojiRes.Value.Id))); | |
| add.Label = $"Add option ({buttons.Count}/25)"; | |
| remove.Disabled = false; | |
| update.Disabled = false; | |
| publish.Disabled = false; | |
| if (buttons.Count is 25) | |
| add.Disabled = true; | |
| return; | |
| } | |
| } | |
| else | |
| { | |
| await currentMessage.ModifyAsync("Alright!"); | |
| await messageInput.Result.DeleteAsync(); | |
| await Task.Delay(MessageUserReadWaitDelay / 3); | |
| zipList.Add((emojiRes.Value, roleRes.Value)); | |
| buttons.Add(new DiscordButtonComponent(ButtonStyle.Success, $"rolemenu assign {roleRes.Value.Mention}", "", emoji: new() {Id = emojiRes.Value.Id, Name = emojiRes.Value})); | |
| remove.Disabled = false; | |
| update.Disabled = false; | |
| publish.Disabled = false; | |
| add.Label = $"Add option ({buttons.Count}/25)"; | |
| if (buttons.Count is 25) | |
| add.Disabled = true; | |
| await Task.Delay(MessageUserReadWaitDelay / 2); | |
| return; | |
| } | |
| } while (true); | |
| } | |
| // Returns false if the user cancels // | |
| async Task<bool> GetRoleMenuNameAsync() | |
| { | |
| while (true) | |
| { | |
| messageInput = await input.WaitForMessageAsync(m => m.Author == ctx.User, InteractionTimeout); | |
| if (messageInput.TimedOut) | |
| { | |
| await ctx.RespondAsync($"{ctx.User.Mention} your setup has timed out."); | |
| return false; // return; | |
| } | |
| currentMessage = await buttonInput.Result.Interaction.EditFollowupMessageAsync(currentMessage.Id, new DiscordWebhookBuilder().WithContent("Are you sure?").WithComponents(YNC)); | |
| buttonInput = await input.WaitForButtonAsync(currentMessage); | |
| if (buttonInput.TimedOut) | |
| { | |
| await ctx.RespondAsync($"{ctx.User.Mention} your role menu setup has timed out."); | |
| return false; // return; | |
| } | |
| await buttonInput.Result.Interaction.CreateResponseAsync(InteractionResponseType.DefferedMessageUpdate); | |
| if (buttonInput.Result.Id.EndsWith("decline")) | |
| { | |
| await currentMessage.ModifyAsync(m => m.WithContent("All good role menus start with a name. What's this one's?")); | |
| ; // continue; | |
| } | |
| if (buttonInput.Result.Id.EndsWith("abort")) | |
| { | |
| await currentMessage.ModifyAsync(m => m.WithContent("Aborted.")); | |
| return false; // return; | |
| } | |
| if (!buttonInput.Result.Id.EndsWith("confirm")) | |
| { | |
| continue; // continue; | |
| } | |
| roleMenuTitle = messageInput.Result.Content; | |
| await messageInput.Result.DeleteAsync(); | |
| await currentMessage.ModifyAsync(m => m.WithContent("Alright. Got it. Now on to emojis and roles. \n(I will delete your message, so avoid pinging roles in a public channel!)")); | |
| return true; // break; | |
| } | |
| } | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment