From f03473ba1490cde1dafcaf3524694a9fb8c71528 Mon Sep 17 00:00:00 2001 From: Toastie Date: Thu, 26 Dec 2024 20:51:41 +1300 Subject: [PATCH] Add all other discord selector types --- EventHandler.cs | 36 ++++++++++++++ Interviewer.cs | 122 +++++++++++++++++++++++++++++++++++++----------- 2 files changed, 130 insertions(+), 28 deletions(-) diff --git a/EventHandler.cs b/EventHandler.cs index 9db6e5b..ac2455a 100644 --- a/EventHandler.cs +++ b/EventHandler.cs @@ -239,9 +239,45 @@ public static class EventHandler Logger.Warn("Unknown form input received! '" + e.Id + "'"); return; case DiscordComponentType.UserSelect: + switch (e.Id) + { + case not null when e.Id.StartsWith("supportchild_interviewuserselector"): + await Interviewer.ProcessButtonOrSelectorResponse(e.Interaction); + return; + default: + Logger.Warn("Unknown selection box option received! '" + e.Id + "'"); + return; + } case DiscordComponentType.RoleSelect: + switch (e.Id) + { + case not null when e.Id.StartsWith("supportchild_interviewroleselector"): + await Interviewer.ProcessButtonOrSelectorResponse(e.Interaction); + return; + default: + Logger.Warn("Unknown selection box option received! '" + e.Id + "'"); + return; + } case DiscordComponentType.MentionableSelect: + switch (e.Id) + { + case not null when e.Id.StartsWith("supportchild_interviewmentionableselector"): + await Interviewer.ProcessButtonOrSelectorResponse(e.Interaction); + return; + default: + Logger.Warn("Unknown selection box option received! '" + e.Id + "'"); + return; + } case DiscordComponentType.ChannelSelect: + switch (e.Id) + { + case not null when e.Id.StartsWith("supportchild_interviewchannelselector"): + await Interviewer.ProcessButtonOrSelectorResponse(e.Interaction); + return; + default: + Logger.Warn("Unknown selection box option received! '" + e.Id + "'"); + return; + } default: Logger.Warn("Unknown interaction type received! '" + e.Interaction.Data.ComponentType + "'"); break; diff --git a/Interviewer.cs b/Interviewer.cs index fa6ccd6..474c494 100644 --- a/Interviewer.cs +++ b/Interviewer.cs @@ -15,14 +15,19 @@ namespace SupportChild; public static class Interviewer { - // TODO: Investigate other types of selectors + // TODO: Validate that the different types have the appropriate amount of subpaths public enum QuestionType { + // Support multiselector as separate type, with only one subpath supported ERROR, END_WITH_SUMMARY, END_WITHOUT_SUMMARY, BUTTONS, TEXT_SELECTOR, + USER_SELECTOR, + ROLE_SELECTOR, + MENTIONABLE_SELECTOR, // User or role + CHANNEL_SELECTOR, TEXT_INPUT } @@ -41,6 +46,7 @@ public static class Interviewer // The entire interview tree is serialized and stored in the database in order to record responses as they are made. public class InterviewQuestion { + // TODO: Add selector placeholder // Title of the message embed. [JsonProperty("title")] public string title; @@ -123,6 +129,11 @@ public static class Interviewer public void GetSummary(ref OrderedDictionary summary) { + if (messageID == 0) + { + return; + } + if (!string.IsNullOrWhiteSpace(summaryField)) { summary.Add(summaryField, answer); @@ -274,7 +285,7 @@ public static class Interviewer } // Ignore if option was deselected. - if (interaction.Data.ComponentType == DiscordComponentType.StringSelect && interaction.Data.Values.Length == 0) + if (interaction.Data.ComponentType is not DiscordComponentType.Button && interaction.Data.Values.Length == 0) { await interaction.CreateResponseAsync(DiscordInteractionResponseType.UpdateMessage); return; @@ -314,13 +325,43 @@ public static class Interviewer return; } - await interaction.CreateResponseAsync(DiscordInteractionResponseType.UpdateMessage); + try + { + // TODO: Debug this with the new selectors + await interaction.CreateResponseAsync(DiscordInteractionResponseType.UpdateMessage); + } + catch (Exception e) + { + Logger.Error("Could not update original message:", e); + } // Parse the response index from the button/selector. string componentID = ""; + string answer = ""; switch (interaction.Data.ComponentType) { + case DiscordComponentType.UserSelect: + case DiscordComponentType.RoleSelect: + case DiscordComponentType.ChannelSelect: + case DiscordComponentType.MentionableSelect: + if (interaction.Data.Resolved?.Roles?.Any() ?? false) + { + answer = interaction.Data.Resolved.Roles.First().Value.Mention; + } + else if (interaction.Data.Resolved?.Users?.Any() ?? false) + { + answer = interaction.Data.Resolved.Users.First().Value.Mention; + } + else if (interaction.Data.Resolved?.Channels?.Any() ?? false) + { + answer = interaction.Data.Resolved.Channels.First().Value.Mention; + } + else if (interaction.Data.Resolved?.Messages?.Any() ?? false) + { + answer = interaction.Data.Resolved.Messages.First().Value.Id.ToString(); + } + break; case DiscordComponentType.StringSelect: componentID = interaction.Data.Values[0]; break; @@ -331,21 +372,37 @@ public static class Interviewer throw new ArgumentOutOfRangeException(); } - if (!int.TryParse(componentID, out int pathIndex)) + // The different mentionable selectors provide the actual answer, while the others just return the ID. + if (componentID == "") { - Logger.Error("Invalid interview button/selector index: " + componentID); - return; + // TODO: Handle multipaths + if (currentQuestion.paths.Count != 1) + { + Logger.Error("The interview for channel " + interaction.Channel.Id + " has a question of type " + currentQuestion.type + " and it must have exactly one subpath."); + return; + } + + (string _, InterviewQuestion nextQuestion) = currentQuestion.paths.First(); + await HandleAnswer(answer, nextQuestion, interviewRoot, currentQuestion, interaction.Channel); + } + else + { + if (!int.TryParse(componentID, out int pathIndex)) + { + Logger.Error("Invalid interview button/selector index: " + componentID); + return; + } + + if (pathIndex >= currentQuestion.paths.Count || pathIndex < 0) + { + Logger.Error("Invalid interview button/selector index: " + pathIndex); + return; + } + + (string questionString, InterviewQuestion nextQuestion) = currentQuestion.paths.ElementAt(pathIndex); + await HandleAnswer(questionString, nextQuestion, interviewRoot, currentQuestion, interaction.Channel); } - if (pathIndex >= currentQuestion.paths.Count || pathIndex < 0) - { - Logger.Error("Invalid interview button/selector index: " + pathIndex); - return; - } - - (string questionString, InterviewQuestion nextQuestion) = currentQuestion.paths.ElementAt(pathIndex); - - await HandleAnswer(questionString, nextQuestion, interviewRoot, currentQuestion, interaction.Channel); } public static async Task ProcessResponseMessage(DiscordMessage answerMessage) @@ -409,7 +466,7 @@ public static class Interviewer // Skip to the first matching path. if (!Regex.IsMatch(answerMessage.Content, questionString)) continue; - await HandleAnswer(questionString, nextQuestion, interviewRoot, currentQuestion, answerMessage.Channel, answerMessage); + await HandleAnswer(answerMessage.Content, nextQuestion, interviewRoot, currentQuestion, answerMessage.Channel, answerMessage); return; } @@ -422,32 +479,26 @@ public static class Interviewer currentQuestion.AddRelatedMessageIDs(errorMessage.Id); } - private static async Task HandleAnswer(string questionString, + private static async Task HandleAnswer(string answer, InterviewQuestion nextQuestion, InterviewQuestion interviewRoot, InterviewQuestion previousQuestion, DiscordChannel channel, DiscordMessage answerMessage = null) { + // The error message type should not alter anything about the interview if (nextQuestion.type != QuestionType.ERROR) { - // The answer was provided using a button or selector + previousQuestion.answer = answer; if (answerMessage == null) { - previousQuestion.answer = questionString; + // The answer was provided using a button or selector previousQuestion.answerID = 0; } else { - previousQuestion.answer = answerMessage.Content; previousQuestion.answerID = answerMessage.Id; } - - // Remove any other paths from the previous question. - previousQuestion.paths = new Dictionary - { - { questionString, nextQuestion } - }; } // Create next question, or finish the interview. @@ -456,6 +507,10 @@ public static class Interviewer case QuestionType.TEXT_INPUT: case QuestionType.BUTTONS: case QuestionType.TEXT_SELECTOR: + case QuestionType.ROLE_SELECTOR: + case QuestionType.USER_SELECTOR: + case QuestionType.CHANNEL_SELECTOR: + case QuestionType.MENTIONABLE_SELECTOR: await CreateQuestion(channel, nextQuestion); Database.SaveInterview(channel.Id, interviewRoot); break; @@ -485,7 +540,6 @@ public static class Interviewer ReloadInterviews(); return; case QuestionType.END_WITHOUT_SUMMARY: - // TODO: Add command to restart interview. await channel.SendMessageAsync(new DiscordEmbedBuilder() { Color = Utilities.StringToColor(nextQuestion.color), @@ -583,11 +637,23 @@ public static class Interviewer { categoryOptions.Add(new DiscordSelectComponentOption(question.paths.ToArray()[selectionOptions].Key, selectionOptions.ToString())); } - selectionComponents.Add(new DiscordSelectComponent("supportchild_interviewselector " + selectionBoxes, "Select an option...", categoryOptions, false, 0, 1)); + selectionComponents.Add(new DiscordSelectComponent("supportchild_interviewselector " + selectionBoxes, "Select an option...", categoryOptions)); } msgBuilder.AddComponents(selectionComponents); break; + case QuestionType.ROLE_SELECTOR: + msgBuilder.AddComponents(new DiscordRoleSelectComponent("supportchild_interviewroleselector", "Select a role...")); + break; + case QuestionType.USER_SELECTOR: + msgBuilder.AddComponents(new DiscordUserSelectComponent("supportchild_interviewuserselector", "Select a user...")); + break; + case QuestionType.CHANNEL_SELECTOR: + msgBuilder.AddComponents(new DiscordChannelSelectComponent("supportchild_interviewchannelselector", "Select a channel...")); + break; + case QuestionType.MENTIONABLE_SELECTOR: + msgBuilder.AddComponents(new DiscordMentionableSelectComponent("supportchild_interviewmentionableselector", "Select a mentionable...")); + break; case QuestionType.TEXT_INPUT: embed.WithFooter("Reply to this message with your answer. You cannot include images or files."); break;