using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Linq; using DSharpPlus; using DSharpPlus.Entities; using Newtonsoft.Json; using Newtonsoft.Json.Converters; using Newtonsoft.Json.Linq; using Newtonsoft.Json.Serialization; namespace SupportChild; public static class Interviewer { public enum QuestionType { FAIL, DONE, BUTTONS, SELECTOR, TEXT_INPUT } // A tree of questions 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 in order to record responses as they are made. public class InterviewQuestion { // Message contents sent to the user. [JsonProperty("message")] public string message; // The type of question. [JsonConverter(typeof(StringEnumConverter))] [JsonProperty("type")] public QuestionType type; // Colour of the message embed. [JsonProperty("color")] public string color; // The ID of this message where the bot asked this question, // populated after it has been sent. [JsonProperty("message-id")] public ulong messageID; // Used as label for this question in the post-interview summary. [JsonProperty("summary-field")] public string summaryField; // The user's response to the question. [JsonProperty("answer")] public string answer; // The ID of the user's answer message, populated after it has been received. [JsonProperty("answer-id")] public ulong answerID; // Possible questions to ask next, or DONE/FAIL type in order to finish interview. [JsonProperty("paths")] public Dictionary paths; } // This class is identical to the one above and just exists as a hack to get JSON validation when // new entries are entered but not when read from database in order to be more lenient with old interviews. // I might do this in a more proper way at some point. public class ValidatedInterviewQuestion { // Message contents sent to the user. [JsonProperty("message", Required = Required.Always)] public string message; // The type of question. [JsonConverter(typeof(StringEnumConverter))] [JsonProperty("type", Required = Required.Always)] public QuestionType type; // Colour of the message embed. [JsonProperty("color", Required = Required.Always)] public string color; // The ID of this message where the bot asked this question, // populated after it has been sent. [JsonProperty("message-id", Required = Required.Default)] public ulong messageID; // Used as label for this question in the post-interview summary. [JsonProperty("summary-field", Required = Required.Default)] public string summaryField; // The user's response to the question. [JsonProperty("answer", Required = Required.Default)] public string answer; // The ID of the user's answer message, populated after it has been received. [JsonProperty("answer-id", Required = Required.Default)] public ulong answerID; // Possible questions to ask next, or DONE/FAIL type in order to finish interview. [JsonProperty("paths", Required = Required.Always)] public Dictionary paths; } private static Dictionary categoryInterviews = []; private static Dictionary activeInterviews = []; public static void ParseTemplates(JToken interviewConfig) { categoryInterviews = JsonConvert.DeserializeObject>(interviewConfig.ToString(), new JsonSerializerSettings { Error = delegate (object sender, ErrorEventArgs args) { Logger.Error("Exception occured when trying to read interview from database:\n" + args.ErrorContext.Error.Message); Logger.Debug("Detailed exception:", args.ErrorContext.Error); args.ErrorContext.Handled = true; } }); } public static void LoadActiveInterviews() { activeInterviews = Database.GetAllInterviews(); } public static void StartInterview(DiscordChannel channel) { if (channel.Parent == null) { return; } if (categoryInterviews.TryGetValue(channel.Parent.Id, out InterviewQuestion interview)) { CreateQuestion(channel, interview); Database.SaveInterview(channel.Id, interview); } } public static void ProcessResponse(DiscordMessage message) { // TODO: Find if channel has open interview. // TODO: Find if message is replying to interview message. // TODO: Handle other interactions like button presses. // TODO: Find out where in the interview tree we are. // TODO: Handle FAIL event, cancelling the interview. // TODO: Handle DONE event, creating a summary. //Database.SaveInterview(channel.Id, interview); } private static async void CreateQuestion(DiscordChannel channel, InterviewQuestion question) { DiscordMessageBuilder msgBuilder = new DiscordMessageBuilder(); DiscordEmbedBuilder embed = new DiscordEmbedBuilder() { Color = Utilities.StringToColor(question.color), Description = question.message }; switch (question.type) { case QuestionType.BUTTONS: int nrOfButtons = 0; for (int nrOfButtonRows = 0; nrOfButtonRows < 5 && nrOfButtons < question.paths.Count; nrOfButtonRows++) { List buttonRow = []; for (; nrOfButtons < 5 * (nrOfButtonRows + 1) && nrOfButtons < question.paths.Count; nrOfButtons++) { buttonRow.Add(new DiscordButtonComponent(DiscordButtonStyle.Primary, "supportchild_interviewbutton " + nrOfButtons, question.paths.ToArray()[nrOfButtons].Key)); } msgBuilder.AddComponents(buttonRow); } break; case QuestionType.SELECTOR: List selectionComponents = []; int selectionOptions = 0; for (int selectionBoxes = 0; selectionBoxes < 5 && selectionOptions < question.paths.Count; selectionBoxes++) { List categoryOptions = []; for (; selectionOptions < 25 * (selectionBoxes + 1) && selectionOptions < question.paths.Count; selectionOptions++) { 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)); } msgBuilder.AddComponents(selectionComponents); break; case QuestionType.TEXT_INPUT: embed.WithFooter("Reply to this message with your answer."); break; case QuestionType.DONE: case QuestionType.FAIL: default: break; } msgBuilder.AddEmbed(embed); DiscordMessage message = await channel.SendMessageAsync(msgBuilder); question.messageID = message.Id; } public static void CreateSummary() { } }