Add all other discord selector types

This commit is contained in:
Toastie 2024-12-26 20:51:41 +13:00
parent f1ce93ae86
commit f03473ba14
Signed by: toastie_t0ast
GPG key ID: 27F3B6855AFD40A4
2 changed files with 130 additions and 28 deletions

View file

@ -239,9 +239,45 @@ public static class EventHandler
Logger.Warn("Unknown form input received! '" + e.Id + "'"); Logger.Warn("Unknown form input received! '" + e.Id + "'");
return; return;
case DiscordComponentType.UserSelect: 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: 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: 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: 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: default:
Logger.Warn("Unknown interaction type received! '" + e.Interaction.Data.ComponentType + "'"); Logger.Warn("Unknown interaction type received! '" + e.Interaction.Data.ComponentType + "'");
break; break;

View file

@ -15,14 +15,19 @@ namespace SupportChild;
public static class Interviewer 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 public enum QuestionType
{ {
// Support multiselector as separate type, with only one subpath supported
ERROR, ERROR,
END_WITH_SUMMARY, END_WITH_SUMMARY,
END_WITHOUT_SUMMARY, END_WITHOUT_SUMMARY,
BUTTONS, BUTTONS,
TEXT_SELECTOR, TEXT_SELECTOR,
USER_SELECTOR,
ROLE_SELECTOR,
MENTIONABLE_SELECTOR, // User or role
CHANNEL_SELECTOR,
TEXT_INPUT 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. // The entire interview tree is serialized and stored in the database in order to record responses as they are made.
public class InterviewQuestion public class InterviewQuestion
{ {
// TODO: Add selector placeholder
// Title of the message embed. // Title of the message embed.
[JsonProperty("title")] [JsonProperty("title")]
public string title; public string title;
@ -123,6 +129,11 @@ public static class Interviewer
public void GetSummary(ref OrderedDictionary summary) public void GetSummary(ref OrderedDictionary summary)
{ {
if (messageID == 0)
{
return;
}
if (!string.IsNullOrWhiteSpace(summaryField)) if (!string.IsNullOrWhiteSpace(summaryField))
{ {
summary.Add(summaryField, answer); summary.Add(summaryField, answer);
@ -274,7 +285,7 @@ public static class Interviewer
} }
// Ignore if option was deselected. // 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); await interaction.CreateResponseAsync(DiscordInteractionResponseType.UpdateMessage);
return; return;
@ -314,13 +325,43 @@ public static class Interviewer
return; 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. // Parse the response index from the button/selector.
string componentID = ""; string componentID = "";
string answer = "";
switch (interaction.Data.ComponentType) 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: case DiscordComponentType.StringSelect:
componentID = interaction.Data.Values[0]; componentID = interaction.Data.Values[0];
break; break;
@ -331,21 +372,37 @@ public static class Interviewer
throw new ArgumentOutOfRangeException(); 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); // TODO: Handle multipaths
return; 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) public static async Task ProcessResponseMessage(DiscordMessage answerMessage)
@ -409,7 +466,7 @@ public static class Interviewer
// Skip to the first matching path. // Skip to the first matching path.
if (!Regex.IsMatch(answerMessage.Content, questionString)) continue; 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; return;
} }
@ -422,32 +479,26 @@ public static class Interviewer
currentQuestion.AddRelatedMessageIDs(errorMessage.Id); currentQuestion.AddRelatedMessageIDs(errorMessage.Id);
} }
private static async Task HandleAnswer(string questionString, private static async Task HandleAnswer(string answer,
InterviewQuestion nextQuestion, InterviewQuestion nextQuestion,
InterviewQuestion interviewRoot, InterviewQuestion interviewRoot,
InterviewQuestion previousQuestion, InterviewQuestion previousQuestion,
DiscordChannel channel, DiscordChannel channel,
DiscordMessage answerMessage = null) DiscordMessage answerMessage = null)
{ {
// The error message type should not alter anything about the interview
if (nextQuestion.type != QuestionType.ERROR) if (nextQuestion.type != QuestionType.ERROR)
{ {
// The answer was provided using a button or selector previousQuestion.answer = answer;
if (answerMessage == null) if (answerMessage == null)
{ {
previousQuestion.answer = questionString; // The answer was provided using a button or selector
previousQuestion.answerID = 0; previousQuestion.answerID = 0;
} }
else else
{ {
previousQuestion.answer = answerMessage.Content;
previousQuestion.answerID = answerMessage.Id; previousQuestion.answerID = answerMessage.Id;
} }
// Remove any other paths from the previous question.
previousQuestion.paths = new Dictionary<string, InterviewQuestion>
{
{ questionString, nextQuestion }
};
} }
// Create next question, or finish the interview. // Create next question, or finish the interview.
@ -456,6 +507,10 @@ public static class Interviewer
case QuestionType.TEXT_INPUT: case QuestionType.TEXT_INPUT:
case QuestionType.BUTTONS: case QuestionType.BUTTONS:
case QuestionType.TEXT_SELECTOR: case QuestionType.TEXT_SELECTOR:
case QuestionType.ROLE_SELECTOR:
case QuestionType.USER_SELECTOR:
case QuestionType.CHANNEL_SELECTOR:
case QuestionType.MENTIONABLE_SELECTOR:
await CreateQuestion(channel, nextQuestion); await CreateQuestion(channel, nextQuestion);
Database.SaveInterview(channel.Id, interviewRoot); Database.SaveInterview(channel.Id, interviewRoot);
break; break;
@ -485,7 +540,6 @@ public static class Interviewer
ReloadInterviews(); ReloadInterviews();
return; return;
case QuestionType.END_WITHOUT_SUMMARY: case QuestionType.END_WITHOUT_SUMMARY:
// TODO: Add command to restart interview.
await channel.SendMessageAsync(new DiscordEmbedBuilder() await channel.SendMessageAsync(new DiscordEmbedBuilder()
{ {
Color = Utilities.StringToColor(nextQuestion.color), Color = Utilities.StringToColor(nextQuestion.color),
@ -583,11 +637,23 @@ public static class Interviewer
{ {
categoryOptions.Add(new DiscordSelectComponentOption(question.paths.ToArray()[selectionOptions].Key, selectionOptions.ToString())); 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); msgBuilder.AddComponents(selectionComponents);
break; 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: case QuestionType.TEXT_INPUT:
embed.WithFooter("Reply to this message with your answer. You cannot include images or files."); embed.WithFooter("Reply to this message with your answer. You cannot include images or files.");
break; break;