SupportChild/Interviewer.cs

210 lines
7.9 KiB
C#
Raw Normal View History

2024-12-26 04:55:00 +00:00
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
2024-12-26 04:55:00 +00:00
using System.Linq;
using DSharpPlus;
using DSharpPlus.Entities;
using Newtonsoft.Json;
using Newtonsoft.Json.Converters;
2024-12-26 04:55:00 +00:00
using Newtonsoft.Json.Linq;
using Newtonsoft.Json.Serialization;
2024-12-26 04:55:00 +00:00
namespace SupportChild;
public static class Interviewer
{
public enum QuestionType
{
FAIL,
DONE,
2024-12-26 04:55:00 +00:00
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
2024-12-26 04:55:00 +00:00
{
// Message contents sent to the user.
[JsonProperty("message")]
2024-12-26 04:55:00 +00:00
public string message;
// The type of question.
[JsonConverter(typeof(StringEnumConverter))]
[JsonProperty("type")]
2024-12-26 04:55:00 +00:00
public QuestionType type;
// Colour of the message embed.
[JsonProperty("color")]
2024-12-26 04:55:00 +00:00
public string color;
// The ID of this message where the bot asked this question,
// populated after it has been sent.
[JsonProperty("message-id")]
2024-12-26 04:55:00 +00:00
public ulong messageID;
// Used as label for this question in the post-interview summary.
[JsonProperty("summary-field")]
2024-12-26 04:55:00 +00:00
public string summaryField;
// The user's response to the question.
[JsonProperty("answer")]
2024-12-26 04:55:00 +00:00
public string answer;
// The ID of the user's answer message, populated after it has been received.
[JsonProperty("answer-id")]
2024-12-26 04:55:00 +00:00
public ulong answerID;
// Possible questions to ask next, or DONE/FAIL type in order to finish interview.
[JsonProperty("paths")]
2024-12-26 04:55:00 +00:00
public Dictionary<string, InterviewQuestion> 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<string, ValidatedInterviewQuestion> paths;
}
2024-12-26 04:55:00 +00:00
private static Dictionary<ulong, InterviewQuestion> categoryInterviews = [];
private static Dictionary<ulong, InterviewQuestion> activeInterviews = [];
public static void ParseTemplates(JToken interviewConfig)
2024-12-26 04:55:00 +00:00
{
categoryInterviews = JsonConvert.DeserializeObject<Dictionary<ulong, InterviewQuestion>>(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;
}
});
2024-12-26 04:55:00 +00:00
}
public static void LoadActiveInterviews()
{
activeInterviews = Database.GetAllInterviews();
}
2024-12-26 04:55:00 +00:00
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);
2024-12-26 04:55:00 +00:00
}
}
public static void ProcessResponse(DiscordMessage message)
2024-12-26 04:55:00 +00:00
{
// 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);
2024-12-26 04:55:00 +00:00
}
private static async void CreateQuestion(DiscordChannel channel, InterviewQuestion question)
{
DiscordMessageBuilder msgBuilder = new DiscordMessageBuilder();
DiscordEmbedBuilder embed = new DiscordEmbedBuilder()
2024-12-26 04:55:00 +00:00
{
Color = Utilities.StringToColor(question.color),
Description = question.message
2024-12-26 04:55:00 +00:00
};
switch (question.type)
{
case QuestionType.BUTTONS:
int nrOfButtons = 0;
for (int nrOfButtonRows = 0; nrOfButtonRows < 5 && nrOfButtons < question.paths.Count; nrOfButtonRows++)
{
List<DiscordButtonComponent> 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));
2024-12-26 04:55:00 +00:00
}
msgBuilder.AddComponents(buttonRow);
}
break;
case QuestionType.SELECTOR:
List<DiscordSelectComponent> selectionComponents = [];
int selectionOptions = 0;
for (int selectionBoxes = 0; selectionBoxes < 5 && selectionOptions < question.paths.Count; selectionBoxes++)
{
List<DiscordSelectComponentOption> 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));
2024-12-26 04:55:00 +00:00
}
msgBuilder.AddComponents(selectionComponents);
break;
case QuestionType.TEXT_INPUT:
embed.WithFooter("Reply to this message with your answer.");
2024-12-26 04:55:00 +00:00
break;
case QuestionType.DONE:
case QuestionType.FAIL:
default:
break;
}
msgBuilder.AddEmbed(embed);
2024-12-26 04:55:00 +00:00
DiscordMessage message = await channel.SendMessageAsync(msgBuilder);
question.messageID = message.Id;
}
public static void CreateSummary()
{
}
}