From 88205499271ff712c2c2412948bb268d8a8e857a Mon Sep 17 00:00:00 2001 From: Toastie Date: Fri, 27 Dec 2024 17:23:03 +1300 Subject: [PATCH] Normalised interview terminology for "question", "path", "step" to use "step" everywhere --- Commands/CloseCommand.cs | 6 +- Commands/InterviewCommands.cs | 2 +- Commands/InterviewTemplateCommands.cs | 4 +- Commands/TranscriptCommand.cs | 6 +- Database.cs | 12 +- Interviews/Interview.cs | 144 +++++++++------ Interviews/Interviewer.cs | 206 +++++++++++----------- Interviews/interview_template.schema.json | 12 +- docs/InterviewTemplates.md | 14 +- 9 files changed, 222 insertions(+), 184 deletions(-) diff --git a/Commands/CloseCommand.cs b/Commands/CloseCommand.cs index fdcde0b..48bbf2d 100644 --- a/Commands/CloseCommand.cs +++ b/Commands/CloseCommand.cs @@ -106,13 +106,13 @@ public class CloseCommand // If the zip transcript doesn't exist, use the html file. try { - FileInfo fi = new FileInfo(filePath); - if (!fi.Exists || fi.Length >= 26214400) + FileInfo fileInfo = new FileInfo(filePath); + if (!fileInfo.Exists || fileInfo.Length >= 26214400) { fileName = Transcriber.GetHTMLFilename(ticket.id); filePath = Transcriber.GetHtmlPath(ticket.id); } - zipSize = fi.Length; + zipSize = fileInfo.Length; } catch (Exception e) { diff --git a/Commands/InterviewCommands.cs b/Commands/InterviewCommands.cs index 83c4742..c4d0688 100644 --- a/Commands/InterviewCommands.cs +++ b/Commands/InterviewCommands.cs @@ -78,7 +78,7 @@ public class InterviewCommands return; } - if (!Database.TryGetInterview(command.Channel.Id, out InterviewQuestion interviewRoot)) + if (!Database.TryGetInterview(command.Channel.Id, out InterviewStep interviewRoot)) { await command.RespondAsync(new DiscordEmbedBuilder { diff --git a/Commands/InterviewTemplateCommands.cs b/Commands/InterviewTemplateCommands.cs index f72b66d..b80b493 100644 --- a/Commands/InterviewTemplateCommands.cs +++ b/Commands/InterviewTemplateCommands.cs @@ -49,7 +49,7 @@ public class InterviewTemplateCommands " \"message\": \"\",\n" + " \"message-type\": \"\",\n" + " \"color\": \"\",\n" + - " \"paths\":\n" + + " \"steps\":\n" + " {\n" + " \n" + " }\n" + @@ -214,7 +214,7 @@ public class InterviewTemplateCommands return; } - if (!Database.TryGetInterviewTemplate(category.Id, out InterviewQuestion _)) + if (!Database.TryGetInterviewTemplate(category.Id, out InterviewStep _)) { await command.RespondAsync(new DiscordEmbedBuilder { diff --git a/Commands/TranscriptCommand.cs b/Commands/TranscriptCommand.cs index 150dcef..adcd011 100644 --- a/Commands/TranscriptCommand.cs +++ b/Commands/TranscriptCommand.cs @@ -88,13 +88,13 @@ public class TranscriptCommand // If the zip transcript doesn't exist, use the html file. try { - FileInfo fi = new FileInfo(filePath); - if (!fi.Exists || fi.Length >= 26214400) + FileInfo fileInfo = new FileInfo(filePath); + if (!fileInfo.Exists || fileInfo.Length >= 26214400) { fileName = Transcriber.GetHTMLFilename(ticket.id); filePath = Transcriber.GetHtmlPath(ticket.id); } - zipSize = fi.Length; + zipSize = fileInfo.Length; } catch (Exception e) { diff --git a/Database.cs b/Database.cs index e9d06f7..c7bb26f 100644 --- a/Database.cs +++ b/Database.cs @@ -7,6 +7,8 @@ using MySqlConnector; using Newtonsoft.Json; using Newtonsoft.Json.Serialization; using SupportChild.Interviews; +using SupportChild.Interviews; +using SupportChild; namespace SupportChild; @@ -759,7 +761,7 @@ public static class Database return templates; } - public static bool TryGetInterviewTemplate(ulong categoryID, out Interviews.InterviewQuestion template) + public static bool TryGetInterviewTemplate(ulong categoryID, out Interviews.InterviewStep template) { using MySqlConnection c = GetConnection(); c.Open(); @@ -807,7 +809,7 @@ public static class Database NullValueHandling = NullValueHandling.Ignore, MissingMemberHandling = MissingMemberHandling.Error, Formatting = Formatting.Indented, - ContractResolver = new InterviewQuestion.StripInternalPropertiesResolver() + ContractResolver = new InterviewStep.StripInternalPropertiesResolver() }); string query; @@ -851,7 +853,7 @@ public static class Database } } - public static bool SaveInterview(ulong channelID, Interviews.InterviewQuestion interview) + public static bool SaveInterview(ulong channelID, Interviews.InterviewStep interview) { try { @@ -879,7 +881,7 @@ public static class Database } } - public static bool TryGetInterview(ulong channelID, out Interviews.InterviewQuestion interview) + public static bool TryGetInterview(ulong channelID, out Interviews.InterviewStep interview) { using MySqlConnection c = GetConnection(); c.Open(); @@ -894,7 +896,7 @@ public static class Database interview = null; return false; } - interview = JsonConvert.DeserializeObject(results.GetString("interview")); + interview = JsonConvert.DeserializeObject(results.GetString("interview")); results.Close(); return true; } diff --git a/Interviews/Interview.cs b/Interviews/Interview.cs index d925947..505e7c1 100644 --- a/Interviews/Interview.cs +++ b/Interviews/Interview.cs @@ -10,9 +10,9 @@ using Newtonsoft.Json.Serialization; namespace SupportChild.Interviews; -public enum QuestionType +public enum MessageType { - // TODO: Support multiselector as separate type, with only one subpath supported + // TODO: Support multiselector as separate type ERROR, END_WITH_SUMMARY, END_WITHOUT_SUMMARY, @@ -33,43 +33,43 @@ public enum ButtonType DANGER } -// A tree of questions representing an interview. +// A tree of steps representing an interview. // The tree is generated by the config file when a new ticket is opened or the restart interview command is used. // Additional components not specified in the config file are populated as the interview progresses. // The entire interview tree is serialized and stored in the database to record responses as they are made. -public class InterviewQuestion +public class InterviewStep { // Title of the message embed. - [JsonProperty("title")] - public string title; + [JsonProperty("heading")] + public string heading; // Message contents sent to the user. [JsonProperty("message")] public string message; - // The type of question. + // The type of message. [JsonConverter(typeof(StringEnumConverter))] [JsonProperty("message-type")] - public QuestionType type; + public MessageType messageType; // Colour of the message embed. [JsonProperty("color")] public string color; - // Used as label for this question in the post-interview summary. + // Used as label for this answer in the post-interview summary. [JsonProperty("summary-field")] public string summaryField; - // If this question is on a button, give it this style. + // If this step is on a button, give it this style. [JsonConverter(typeof(StringEnumConverter))] [JsonProperty("button-style")] public ButtonType? buttonStyle; - // If this question is on a selector, give it this placeholder. + // If this step is on a selector, give it this placeholder. [JsonProperty("selector-placeholder")] public string selectorPlaceholder; - // If this question is on a selector, give it this description. + // If this step is on a selector, give it this description. [JsonProperty("selector-description")] public string selectorDescription; @@ -78,12 +78,12 @@ public class InterviewQuestion public int? maxLength; // The minimum length of a text input. - [JsonProperty("min-length", Required = Required.Default)] + [JsonProperty("min-length")] public int? minLength; // Possible questions to ask next, an error message, or the end of the interview. - [JsonProperty("paths")] - public Dictionary paths = new(); + [JsonProperty("steps")] + public Dictionary steps = new(); // //////////////////////////////////////////////////////////////////////////// // The following parameters are populated by the bot, not the json template. // @@ -105,27 +105,27 @@ public class InterviewQuestion [JsonProperty("related-message-ids")] public List relatedMessageIDs; - public bool TryGetCurrentQuestion(out InterviewQuestion question) + public bool TryGetCurrentStep(out InterviewStep step) { // This object has not been initialized, we have checked too deep. if (messageID == 0) { - question = null; + step = null; return false; } // Check children. - foreach (KeyValuePair path in paths) + foreach (KeyValuePair childStep in steps) { // This child either is the one we are looking for or contains the one we are looking for. - if (path.Value.TryGetCurrentQuestion(out question)) + if (childStep.Value.TryGetCurrentStep(out step)) { return true; } } // This object is the deepest object with a message ID set, meaning it is the latest asked question. - question = this; + step = this; return true; } @@ -142,9 +142,9 @@ public class InterviewQuestion } // This will always contain exactly one or zero children. - foreach (KeyValuePair path in paths) + foreach (KeyValuePair step in steps) { - path.Value.GetSummary(ref summary); + step.Value.GetSummary(ref summary); } } @@ -166,9 +166,9 @@ public class InterviewQuestion } // This will always contain exactly one or zero children. - foreach (KeyValuePair path in paths) + foreach (KeyValuePair step in steps) { - path.Value.GetMessageIDs(ref messageIDs); + step.Value.GetMessageIDs(ref messageIDs); } } @@ -188,69 +188,74 @@ public class InterviewQuestion { return buttonStyle switch { - ButtonType.PRIMARY => DiscordButtonStyle.Primary, + ButtonType.PRIMARY => DiscordButtonStyle.Primary, ButtonType.SECONDARY => DiscordButtonStyle.Secondary, - ButtonType.SUCCESS => DiscordButtonStyle.Success, - ButtonType.DANGER => DiscordButtonStyle.Danger, - _ => DiscordButtonStyle.Secondary + ButtonType.SUCCESS => DiscordButtonStyle.Success, + ButtonType.DANGER => DiscordButtonStyle.Danger, + _ => DiscordButtonStyle.Secondary }; } - public void Validate(ref List errors, ref List warnings, string stepID, int summaryFieldCount, int summaryMaxLength) + public void Validate(ref List errors, + ref List warnings, + string stepID, + int summaryFieldCount = 0, + int summaryMaxLength = 0, + InterviewStep parent = null) { if (!string.IsNullOrWhiteSpace(summaryField)) { ++summaryFieldCount; summaryMaxLength += summaryField.Length; - switch (type) + switch (messageType) { - case QuestionType.BUTTONS: - case QuestionType.TEXT_SELECTOR: + case MessageType.BUTTONS: + case MessageType.TEXT_SELECTOR: // Get the longest button/selector text - if (paths.Count > 0) + if (steps.Count > 0) { - summaryMaxLength += paths.Max(kv => kv.Key.Length); + summaryMaxLength += steps.Max(kv => kv.Key.Length); } break; - case QuestionType.USER_SELECTOR: - case QuestionType.ROLE_SELECTOR: - case QuestionType.MENTIONABLE_SELECTOR: - case QuestionType.CHANNEL_SELECTOR: + case MessageType.USER_SELECTOR: + case MessageType.ROLE_SELECTOR: + case MessageType.MENTIONABLE_SELECTOR: + case MessageType.CHANNEL_SELECTOR: // Approximate length of a mention summaryMaxLength += 23; break; - case QuestionType.TEXT_INPUT: + case MessageType.TEXT_INPUT: summaryMaxLength += Math.Min(maxLength ?? 1024, 1024); break; - case QuestionType.END_WITH_SUMMARY: - case QuestionType.END_WITHOUT_SUMMARY: - case QuestionType.ERROR: + case MessageType.END_WITH_SUMMARY: + case MessageType.END_WITHOUT_SUMMARY: + case MessageType.ERROR: default: break; } } - if (type is QuestionType.ERROR or QuestionType.END_WITH_SUMMARY or QuestionType.END_WITHOUT_SUMMARY) + if (messageType is MessageType.ERROR or MessageType.END_WITH_SUMMARY or MessageType.END_WITHOUT_SUMMARY) { - if (paths.Count > 0) + if (steps.Count > 0) { - warnings.Add("'" + type + "' paths cannot have child paths.\n\n" + stepID + ".message-type"); + warnings.Add("Steps of the type '" + messageType + "' cannot have child steps.\n\n" + stepID + ".message-type"); } if (!string.IsNullOrWhiteSpace(summaryField)) { - warnings.Add("'" + type + "' paths cannot have summary field names.\n\n" + stepID + ".summary-field"); + warnings.Add("Steps of the type '" + messageType + "' cannot have summary field names.\n\n" + stepID + ".summary-field"); } } - else if (paths.Count == 0) + else if (steps.Count == 0) { - errors.Add("'" + type + "' paths must have at least one child path.\n\n" + stepID + ".message-type"); + errors.Add("Steps of the type '" + messageType + "' must have at least one child step.\n\n" + stepID + ".message-type"); } - if (type is QuestionType.END_WITH_SUMMARY) + if (messageType is MessageType.END_WITH_SUMMARY) { summaryMaxLength += message?.Length ?? 0; - summaryMaxLength += title?.Length ?? 0; + summaryMaxLength += heading?.Length ?? 0; if (summaryFieldCount > 25) { errors.Add("A summary cannot contain more than 25 fields, but you have " + summaryFieldCount + " fields in this branch.\n\n" + stepID); @@ -262,15 +267,40 @@ public class InterviewQuestion } } - foreach (KeyValuePair path in paths) + if (parent?.messageType is not MessageType.BUTTONS && buttonStyle != null) + { + warnings.Add("Button styles have no effect on child steps of a '" + parent?.messageType + "' step.\n\n" + stepID + ".button-style"); + } + + if (parent?.messageType is not MessageType.TEXT_SELECTOR && selectorDescription != null) + { + warnings.Add("Selector descriptions have no effect on child steps of a '" + parent?.messageType + "' step.\n\n" + stepID + ".selector-description"); + } + + if (messageType is not MessageType.TEXT_SELECTOR && selectorPlaceholder != null) + { + warnings.Add("Selector placeholders have no effect on steps of the type '" + messageType + "'.\n\n" + stepID + ".selector-placeholder"); + } + + if (messageType is not MessageType.TEXT_INPUT && maxLength != null) + { + warnings.Add("Max length has no effect on steps of the type '" + messageType + "'.\n\n" + stepID + ".max-length"); + } + + if (messageType is not MessageType.TEXT_INPUT && minLength != null) + { + warnings.Add("Min length has no effect on steps of the type '" + messageType + "'.\n\n" + stepID + ".min-length"); + } + + foreach (KeyValuePair step in steps) { // The JSON schema error messages use this format for the JSON path, so we use it here too. string nextStepID = stepID; - nextStepID += path.Key.ContainsAny('.', ' ', '[', ']', '(', ')', '/', '\\') - ? ".paths['" + path.Key + "']" - : ".paths." + path.Key; + nextStepID += step.Key.ContainsAny('.', ' ', '[', ']', '(', ')', '/', '\\') + ? ".steps['" + step.Key + "']" + : ".steps." + step.Key; - path.Value.Validate(ref errors, ref warnings, nextStepID, summaryFieldCount, summaryMaxLength); + step.Value.Validate(ref errors, ref warnings, nextStepID, summaryFieldCount, summaryMaxLength, this); } } @@ -296,11 +326,11 @@ public class InterviewQuestion } } -public class Template(ulong categoryID, InterviewQuestion interview) +public class Template(ulong categoryID, InterviewStep interview) { [JsonProperty("category-id", Required = Required.Always)] public ulong categoryID = categoryID; [JsonProperty("interview", Required = Required.Always)] - public InterviewQuestion interview = interview; + public InterviewStep interview = interview; } \ No newline at end of file diff --git a/Interviews/Interviewer.cs b/Interviews/Interviewer.cs index 2405772..fb5ed0b 100644 --- a/Interviews/Interviewer.cs +++ b/Interviews/Interviewer.cs @@ -14,18 +14,18 @@ public static class Interviewer { public static async Task StartInterview(DiscordChannel channel) { - if (!Database.TryGetInterviewTemplate(channel.Parent.Id, out InterviewQuestion template)) + if (!Database.TryGetInterviewTemplate(channel.Parent.Id, out InterviewStep template)) { return false; } - await CreateQuestion(channel, template); + await SendNextMessage(channel, template); return Database.SaveInterview(channel.Id, template); } public static async Task RestartInterview(DiscordChannel channel) { - if (Database.TryGetInterview(channel.Id, out InterviewQuestion interviewRoot)) + if (Database.TryGetInterview(channel.Id, out InterviewStep interviewRoot)) { if (Config.deleteMessagesAfterNoSummary) { @@ -43,7 +43,7 @@ public static class Interviewer public static async Task StopInterview(DiscordChannel channel) { - if (Database.TryGetInterview(channel.Id, out InterviewQuestion interviewRoot)) + if (Database.TryGetInterview(channel.Id, out InterviewStep interviewRoot)) { if (Config.deleteMessagesAfterNoSummary) { @@ -74,7 +74,7 @@ public static class Interviewer } // Return if there is no active interview in this channel - if (!Database.TryGetInterview(interaction.Channel.Id, out InterviewQuestion interviewRoot)) + if (!Database.TryGetInterview(interaction.Channel.Id, out InterviewStep interviewRoot)) { await interaction.CreateResponseAsync(DiscordInteractionResponseType.ChannelMessageWithSource, new DiscordInteractionResponseBuilder() .AddEmbed(new DiscordEmbedBuilder() @@ -85,19 +85,19 @@ public static class Interviewer } // Return if the current question cannot be found in the interview. - if (!interviewRoot.TryGetCurrentQuestion(out InterviewQuestion currentQuestion)) + if (!interviewRoot.TryGetCurrentStep(out InterviewStep currentStep)) { await interaction.CreateResponseAsync(DiscordInteractionResponseType.ChannelMessageWithSource, new DiscordInteractionResponseBuilder() .AddEmbed(new DiscordEmbedBuilder() .WithColor(DiscordColor.Red) .WithDescription("Error: Something seems to have broken in this interview, you may want to restart it.")) .AsEphemeral()); - Logger.Error("The interview for channel " + interaction.Channel.Id + " exists but does not have a message ID set for it's root question"); + Logger.Error("The interview for channel " + interaction.Channel.Id + " exists but does not have a message ID set for it's root interview step"); return; } // Check if this button/selector is for an older question. - if (interaction.Message.Id != currentQuestion.messageID) + if (interaction.Message.Id != currentStep.messageID) { await interaction.CreateResponseAsync(DiscordInteractionResponseType.ChannelMessageWithSource, new DiscordInteractionResponseBuilder() .AddEmbed(new DiscordEmbedBuilder() @@ -158,41 +158,41 @@ public static class Interviewer // The different mentionable selectors provide the actual answer, while the others just return the ID. if (componentID == "") { - foreach (KeyValuePair path in currentQuestion.paths) + foreach (KeyValuePair step in currentStep.steps) { - // Skip to the first matching path. - if (Regex.IsMatch(answer, path.Key)) + // Skip to the first matching step. + if (Regex.IsMatch(answer, step.Key)) { - await HandleAnswer(answer, path.Value, interviewRoot, currentQuestion, interaction.Channel); + await HandleAnswer(answer, step.Value, interviewRoot, currentStep, interaction.Channel); return; } } - Logger.Error("The interview for channel " + interaction.Channel.Id + " reached a question of type " + currentQuestion.type + " which has no valid next question. Their selection was:\n" + answer); + Logger.Error("The interview for channel " + interaction.Channel.Id + " reached a step of type " + currentStep.messageType + " which has no valid next step. Their selection was:\n" + answer); DiscordMessage followupMessage = await interaction.CreateFollowupMessageAsync(new DiscordFollowupMessageBuilder().AddEmbed(new DiscordEmbedBuilder { Color = DiscordColor.Red, Description = "Error: Could not determine the next question based on your answer. Check your response and ask an admin to check the bot logs if this seems incorrect." }).AsEphemeral()); - currentQuestion.AddRelatedMessageIDs(followupMessage.Id); + currentStep.AddRelatedMessageIDs(followupMessage.Id); Database.SaveInterview(interaction.Channel.Id, interviewRoot); } else { - if (!int.TryParse(componentID, out int pathIndex)) + if (!int.TryParse(componentID, out int stepIndex)) { Logger.Error("Invalid interview button/selector index: " + componentID); return; } - if (pathIndex >= currentQuestion.paths.Count || pathIndex < 0) + if (stepIndex >= currentStep.steps.Count || stepIndex < 0) { - Logger.Error("Invalid interview button/selector index: " + pathIndex); + Logger.Error("Invalid interview button/selector index: " + stepIndex); return; } - (string questionString, InterviewQuestion nextQuestion) = currentQuestion.paths.ElementAt(pathIndex); - await HandleAnswer(questionString, nextQuestion, interviewRoot, currentQuestion, interaction.Channel); + (string stepString, InterviewStep nextStep) = currentStep.steps.ElementAt(stepIndex); + await HandleAnswer(stepString, nextStep, interviewRoot, currentStep, interaction.Channel); } } @@ -205,30 +205,30 @@ public static class Interviewer } // The channel does not have an active interview. - if (!Database.TryGetInterview(answerMessage.ReferencedMessage.Channel.Id, out InterviewQuestion interviewRoot)) + if (!Database.TryGetInterview(answerMessage.ReferencedMessage.Channel.Id, out InterviewStep interviewRoot)) { return; } - if (!interviewRoot.TryGetCurrentQuestion(out InterviewQuestion currentQuestion)) + if (!interviewRoot.TryGetCurrentStep(out InterviewStep currentStep)) { return; } // The user responded to something other than the latest interview question. - if (answerMessage.ReferencedMessage.Id != currentQuestion.messageID) + if (answerMessage.ReferencedMessage.Id != currentStep.messageID) { return; } // The user responded to a question which does not take a text response. - if (currentQuestion.type != QuestionType.TEXT_INPUT) + if (currentStep.messageType != MessageType.TEXT_INPUT) { return; } // The length requirement is less than 1024 characters, and must be less than the configurable limit if it is set. - int maxLength = Math.Min(currentQuestion.maxLength ?? 1024, 1024); + int maxLength = Math.Min(currentStep.maxLength ?? 1024, 1024); if (answerMessage.Content.Length > maxLength) { @@ -237,83 +237,83 @@ public static class Interviewer Description = "Error: Your answer cannot be more than " + maxLength + " characters (" + answerMessage.Content.Length + "/" + maxLength + ").", Color = DiscordColor.Red }); - currentQuestion.AddRelatedMessageIDs(answerMessage.Id, lengthMessage.Id); + currentStep.AddRelatedMessageIDs(answerMessage.Id, lengthMessage.Id); Database.SaveInterview(answerMessage.Channel.Id, interviewRoot); return; } - if (answerMessage.Content.Length < (currentQuestion.minLength ?? 0)) + if (answerMessage.Content.Length < (currentStep.minLength ?? 0)) { DiscordMessage lengthMessage = await answerMessage.RespondAsync(new DiscordEmbedBuilder { - Description = "Error: Your answer must be at least " + currentQuestion.minLength + " characters (" + answerMessage.Content.Length + "/" + currentQuestion.minLength + ").", + Description = "Error: Your answer must be at least " + currentStep.minLength + " characters (" + answerMessage.Content.Length + "/" + currentStep.minLength + ").", Color = DiscordColor.Red }); - currentQuestion.AddRelatedMessageIDs(answerMessage.Id, lengthMessage.Id); + currentStep.AddRelatedMessageIDs(answerMessage.Id, lengthMessage.Id); Database.SaveInterview(answerMessage.Channel.Id, interviewRoot); return; } - foreach ((string questionString, InterviewQuestion nextQuestion) in currentQuestion.paths) + foreach ((string stepPattern, InterviewStep nextStep) in currentStep.steps) { - // Skip to the first matching path. - if (!Regex.IsMatch(answerMessage.Content, questionString)) + // Skip to the first matching step. + if (!Regex.IsMatch(answerMessage.Content, stepPattern)) { continue; } - await HandleAnswer(answerMessage.Content, nextQuestion, interviewRoot, currentQuestion, answerMessage.Channel, answerMessage); + await HandleAnswer(answerMessage.Content, nextStep, interviewRoot, currentStep, answerMessage.Channel, answerMessage); return; } - Logger.Error("The interview for channel " + answerMessage.Channel.Id + " reached a question of type " + currentQuestion.type + " which has no valid next question. Their message was:\n" + answerMessage.Content); + Logger.Error("The interview for channel " + answerMessage.Channel.Id + " reached a step of type " + currentStep.messageType + " which has no valid next step. Their message was:\n" + answerMessage.Content); DiscordMessage errorMessage = await answerMessage.RespondAsync(new DiscordEmbedBuilder { Description = "Error: Could not determine the next question based on your answer. Check your response and ask an admin to check the bot logs if this seems incorrect.", Color = DiscordColor.Red }); - currentQuestion.AddRelatedMessageIDs(answerMessage.Id, errorMessage.Id); + currentStep.AddRelatedMessageIDs(answerMessage.Id, errorMessage.Id); Database.SaveInterview(answerMessage.Channel.Id, interviewRoot); } private static async Task HandleAnswer(string answer, - InterviewQuestion nextQuestion, - InterviewQuestion interviewRoot, - InterviewQuestion previousQuestion, + InterviewStep nextStep, + InterviewStep interviewRoot, + InterviewStep previousStep, DiscordChannel channel, DiscordMessage answerMessage = null) { // The error message type should not alter anything about the interview. - if (nextQuestion.type != QuestionType.ERROR) + if (nextStep.messageType != MessageType.ERROR) { - previousQuestion.answer = answer; + previousStep.answer = answer; - // There is no message ID if the question is not a text input. - previousQuestion.answerID = answerMessage == null ? 0 : answerMessage.Id; + // There is no message ID if the step is not a text input. + previousStep.answerID = answerMessage == null ? 0 : answerMessage.Id; } - // Create next question, or finish the interview. - switch (nextQuestion.type) + // Create next step, or finish the interview. + switch (nextStep.messageType) { - 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); + case MessageType.TEXT_INPUT: + case MessageType.BUTTONS: + case MessageType.TEXT_SELECTOR: + case MessageType.ROLE_SELECTOR: + case MessageType.USER_SELECTOR: + case MessageType.CHANNEL_SELECTOR: + case MessageType.MENTIONABLE_SELECTOR: + await SendNextMessage(channel, nextStep); Database.SaveInterview(channel.Id, interviewRoot); break; - case QuestionType.END_WITH_SUMMARY: + case MessageType.END_WITH_SUMMARY: OrderedDictionary summaryFields = new OrderedDictionary(); interviewRoot.GetSummary(ref summaryFields); DiscordEmbedBuilder embed = new DiscordEmbedBuilder() { - Color = Utilities.StringToColor(nextQuestion.color), - Title = nextQuestion.title, - Description = nextQuestion.message, + Color = Utilities.StringToColor(nextStep.color), + Title = nextStep.heading, + Description = nextStep.message, }; foreach (DictionaryEntry entry in summaryFields) @@ -333,12 +333,12 @@ public static class Interviewer Logger.Error("Could not delete interview from database. Channel ID: " + channel.Id); } return; - case QuestionType.END_WITHOUT_SUMMARY: + case MessageType.END_WITHOUT_SUMMARY: await channel.SendMessageAsync(new DiscordEmbedBuilder() { - Color = Utilities.StringToColor(nextQuestion.color), - Title = nextQuestion.title, - Description = nextQuestion.message + Color = Utilities.StringToColor(nextStep.color), + Title = nextStep.heading, + Description = nextStep.message }); if (Config.deleteMessagesAfterNoSummary) @@ -351,29 +351,29 @@ public static class Interviewer Logger.Error("Could not delete interview from database. Channel ID: " + channel.Id); } break; - case QuestionType.ERROR: + case MessageType.ERROR: default: if (answerMessage == null) { DiscordMessage errorMessage = await channel.SendMessageAsync(new DiscordEmbedBuilder() { - Color = Utilities.StringToColor(nextQuestion.color), - Title = nextQuestion.title, - Description = nextQuestion.message + Color = Utilities.StringToColor(nextStep.color), + Title = nextStep.heading, + Description = nextStep.message }); - previousQuestion.AddRelatedMessageIDs(errorMessage.Id); + previousStep.AddRelatedMessageIDs(errorMessage.Id); } else { DiscordMessageBuilder errorMessageBuilder = new DiscordMessageBuilder() .AddEmbed(new DiscordEmbedBuilder() { - Color = Utilities.StringToColor(nextQuestion.color), - Title = nextQuestion.title, - Description = nextQuestion.message + Color = Utilities.StringToColor(nextStep.color), + Title = nextStep.heading, + Description = nextStep.message }).WithReply(answerMessage.Id); DiscordMessage errorMessage = await answerMessage.RespondAsync(errorMessageBuilder); - previousQuestion.AddRelatedMessageIDs(errorMessage.Id, answerMessage.Id); + previousStep.AddRelatedMessageIDs(errorMessage.Id, answerMessage.Id); } Database.SaveInterview(channel.Id, interviewRoot); @@ -381,7 +381,7 @@ public static class Interviewer } } - private static async Task DeletePreviousMessages(InterviewQuestion interviewRoot, DiscordChannel channel) + private static async Task DeletePreviousMessages(InterviewStep interviewRoot, DiscordChannel channel) { List previousMessages = []; interviewRoot.GetMessageIDs(ref previousMessages); @@ -400,78 +400,78 @@ public static class Interviewer } } - private static async Task CreateQuestion(DiscordChannel channel, InterviewQuestion question) + private static async Task SendNextMessage(DiscordChannel channel, InterviewStep step) { DiscordMessageBuilder msgBuilder = new(); DiscordEmbedBuilder embed = new() { - Color = Utilities.StringToColor(question.color), - Title = question.title, - Description = question.message + Color = Utilities.StringToColor(step.color), + Title = step.heading, + Description = step.message }; - switch (question.type) + switch (step.messageType) { - case QuestionType.BUTTONS: + case MessageType.BUTTONS: int nrOfButtons = 0; - for (int nrOfButtonRows = 0; nrOfButtonRows < 5 && nrOfButtons < question.paths.Count; nrOfButtonRows++) + for (int nrOfButtonRows = 0; nrOfButtonRows < 5 && nrOfButtons < step.steps.Count; nrOfButtonRows++) { List buttonRow = []; - for (; nrOfButtons < 5 * (nrOfButtonRows + 1) && nrOfButtons < question.paths.Count; nrOfButtons++) + for (; nrOfButtons < 5 * (nrOfButtonRows + 1) && nrOfButtons < step.steps.Count; nrOfButtons++) { - (string questionString, InterviewQuestion nextQuestion) = question.paths.ToArray()[nrOfButtons]; - buttonRow.Add(new DiscordButtonComponent(nextQuestion.GetButtonStyle(), "supportchild_interviewbutton " + nrOfButtons, questionString)); + (string stepPattern, InterviewStep nextStep) = step.steps.ToArray()[nrOfButtons]; + buttonRow.Add(new DiscordButtonComponent(nextStep.GetButtonStyle(), "supportchild_interviewbutton " + nrOfButtons, stepPattern)); } msgBuilder.AddComponents(buttonRow); } break; - case QuestionType.TEXT_SELECTOR: + case MessageType.TEXT_SELECTOR: List selectionComponents = []; int selectionOptions = 0; - for (int selectionBoxes = 0; selectionBoxes < 5 && selectionOptions < question.paths.Count; selectionBoxes++) + for (int selectionBoxes = 0; selectionBoxes < 5 && selectionOptions < step.steps.Count; selectionBoxes++) { List categoryOptions = []; - for (; selectionOptions < 25 * (selectionBoxes + 1) && selectionOptions < question.paths.Count; selectionOptions++) + for (; selectionOptions < 25 * (selectionBoxes + 1) && selectionOptions < step.steps.Count; selectionOptions++) { - (string questionString, InterviewQuestion nextQuestion) = question.paths.ToArray()[selectionOptions]; - categoryOptions.Add(new DiscordSelectComponentOption(questionString, selectionOptions.ToString(), nextQuestion.selectorDescription)); + (string stepPattern, InterviewStep nextStep) = step.steps.ToArray()[selectionOptions]; + categoryOptions.Add(new DiscordSelectComponentOption(stepPattern, selectionOptions.ToString(), nextStep.selectorDescription)); } - selectionComponents.Add(new DiscordSelectComponent("supportchild_interviewselector " + selectionBoxes, string.IsNullOrWhiteSpace(question.selectorPlaceholder) - ? "Select an option..." : question.selectorPlaceholder, categoryOptions)); + selectionComponents.Add(new DiscordSelectComponent("supportchild_interviewselector " + selectionBoxes, string.IsNullOrWhiteSpace(step.selectorPlaceholder) + ? "Select an option..." : step.selectorPlaceholder, categoryOptions)); } msgBuilder.AddComponents(selectionComponents); break; - case QuestionType.ROLE_SELECTOR: - msgBuilder.AddComponents(new DiscordRoleSelectComponent("supportchild_interviewroleselector", string.IsNullOrWhiteSpace(question.selectorPlaceholder) - ? "Select a role..." : question.selectorPlaceholder)); + case MessageType.ROLE_SELECTOR: + msgBuilder.AddComponents(new DiscordRoleSelectComponent("supportchild_interviewroleselector", string.IsNullOrWhiteSpace(step.selectorPlaceholder) + ? "Select a role..." : step.selectorPlaceholder)); break; - case QuestionType.USER_SELECTOR: - msgBuilder.AddComponents(new DiscordUserSelectComponent("supportchild_interviewuserselector", string.IsNullOrWhiteSpace(question.selectorPlaceholder) - ? "Select a user..." : question.selectorPlaceholder)); + case MessageType.USER_SELECTOR: + msgBuilder.AddComponents(new DiscordUserSelectComponent("supportchild_interviewuserselector", string.IsNullOrWhiteSpace(step.selectorPlaceholder) + ? "Select a user..." : step.selectorPlaceholder)); break; - case QuestionType.CHANNEL_SELECTOR: - msgBuilder.AddComponents(new DiscordChannelSelectComponent("supportchild_interviewchannelselector", string.IsNullOrWhiteSpace(question.selectorPlaceholder) - ? "Select a channel..." : question.selectorPlaceholder)); + case MessageType.CHANNEL_SELECTOR: + msgBuilder.AddComponents(new DiscordChannelSelectComponent("supportchild_interviewchannelselector", string.IsNullOrWhiteSpace(step.selectorPlaceholder) + ? "Select a channel..." : step.selectorPlaceholder)); break; - case QuestionType.MENTIONABLE_SELECTOR: - msgBuilder.AddComponents(new DiscordMentionableSelectComponent("supportchild_interviewmentionableselector", string.IsNullOrWhiteSpace(question.selectorPlaceholder) - ? "Select a user or role..." : question.selectorPlaceholder)); + case MessageType.MENTIONABLE_SELECTOR: + msgBuilder.AddComponents(new DiscordMentionableSelectComponent("supportchild_interviewmentionableselector", string.IsNullOrWhiteSpace(step.selectorPlaceholder) + ? "Select a user or role..." : step.selectorPlaceholder)); break; - case QuestionType.TEXT_INPUT: + case MessageType.TEXT_INPUT: embed.WithFooter("Reply to this message with your answer. You cannot include images or files."); break; - case QuestionType.END_WITH_SUMMARY: - case QuestionType.END_WITHOUT_SUMMARY: - case QuestionType.ERROR: + case MessageType.END_WITH_SUMMARY: + case MessageType.END_WITHOUT_SUMMARY: + case MessageType.ERROR: default: break; } msgBuilder.AddEmbed(embed); DiscordMessage message = await channel.SendMessageAsync(msgBuilder); - question.messageID = message.Id; + step.messageID = message.Id; } } \ No newline at end of file diff --git a/Interviews/interview_template.schema.json b/Interviews/interview_template.schema.json index 9d714a0..fa81913 100644 --- a/Interviews/interview_template.schema.json +++ b/Interviews/interview_template.schema.json @@ -88,10 +88,10 @@ ], "minLength": 1 }, - "paths": { + "steps": { "type": "object", - "title": "Paths", - "description": "The possible next steps, the name of the path is matched against the user input, use \".*\" to match anything.", + "title": "Steps", + "description": "The possible next steps, the name of the step is matched against the user input using regex, use \".*\" to match anything.", "patternProperties": { ".*": { "$ref": "#/definitions/step" @@ -121,6 +121,12 @@ "description": "The description of the selector.", "minLength": 1 }, + "selector-placeholder": { + "type": "string", + "title": "Selector Placeholder", + "description": "The placeholder shown before an option is selected.", + "minLength": 1 + }, "max-length": { "type": "number", "title": "Max Length", diff --git a/docs/InterviewTemplates.md b/docs/InterviewTemplates.md index ff5242c..6016978 100644 --- a/docs/InterviewTemplates.md +++ b/docs/InterviewTemplates.md @@ -87,7 +87,7 @@ Here is a simple example of an interview asking a user for their favourite colou "message-type": "BUTTONS", "color": "BLUE", "summary-field": "Favourite colour", - "paths": + "steps": { "Blue": { @@ -95,7 +95,7 @@ Here is a simple example of an interview asking a user for their favourite colou "message-type": "END_WITH_SUMMARY", "color": "BLUE", "button-style": "PRIMARY", - "paths": {} + "steps": {} }, "Gray": { @@ -103,7 +103,7 @@ Here is a simple example of an interview asking a user for their favourite colou "message-type": "END_WITH_SUMMARY", "color": "GRAY", "button-style": "SECONDARY", - "paths": {} + "steps": {} }, "Green": { @@ -111,7 +111,7 @@ Here is a simple example of an interview asking a user for their favourite colou "message-type": "END_WITH_SUMMARY", "color": "GREEN", "button-style": "SUCCESS", - "paths": {} + "steps": {} }, "Red": { @@ -119,7 +119,7 @@ Here is a simple example of an interview asking a user for their favourite colou "message-type": "END_WITH_SUMMARY", "color": "RED", "button-style": "DANGER", - "paths": {} + "steps": {} } } } @@ -131,7 +131,7 @@ Here is a simple example of an interview asking a user for their favourite colou | Property            | Required | Description | |----------------------------------------------------------------------------|----------|--------------------------------------------------------------------------------------------------------------------------------------| | `category-id` | Yes | The id of the category this template applies to. You can change this and re-upload the template to apply it to a different category. | -| `interview` | Yes | Contains the interview conversation tree, starting with one path which branches into many. | +| `interview` | Yes | Contains the interview conversation tree, starting with one interview step which branches into many. | ### Interview Paths @@ -183,7 +183,7 @@ Colour of the message embed. You can either enter a colour name or a hexadecimal -`paths` +`steps` No Steps