Created functioning interview system PoC
This commit is contained in:
parent
327f4d6de6
commit
32776826ba
6 changed files with 377 additions and 77 deletions
|
@ -221,14 +221,14 @@ public class AdminCommands
|
||||||
Description = "Reloading bot application..."
|
Description = "Reloading bot application..."
|
||||||
});
|
});
|
||||||
Logger.Log("Reloading bot...");
|
Logger.Log("Reloading bot...");
|
||||||
SupportChild.Reload();
|
await SupportChild.Reload();
|
||||||
}
|
}
|
||||||
|
|
||||||
[Command("getinterviewtemplates")]
|
[Command("getinterviewtemplates")]
|
||||||
[Description("Provides a copy of the interview templates which you can edit and then reupload.")]
|
[Description("Provides a copy of the interview templates which you can edit and then reupload.")]
|
||||||
public async Task GetInterviewTemplates(SlashCommandContext command)
|
public async Task GetInterviewTemplates(SlashCommandContext command)
|
||||||
{
|
{
|
||||||
MemoryStream stream = new MemoryStream(Encoding.UTF8.GetBytes(Database.GetInterviewTemplates()));
|
MemoryStream stream = new(Encoding.UTF8.GetBytes(Database.GetInterviewTemplatesJSON()));
|
||||||
await command.RespondAsync(new DiscordInteractionResponseBuilder().AddFile("interview-templates.json", stream));
|
await command.RespondAsync(new DiscordInteractionResponseBuilder().AddFile("interview-templates.json", stream));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -133,6 +133,7 @@ public class CloseCommand
|
||||||
}
|
}
|
||||||
|
|
||||||
Database.ArchiveTicket(ticket);
|
Database.ArchiveTicket(ticket);
|
||||||
|
Database.TryDeleteInterview(channelID);
|
||||||
|
|
||||||
await interaction.EditOriginalResponseAsync(new DiscordWebhookBuilder().AddEmbed(new DiscordEmbedBuilder
|
await interaction.EditOriginalResponseAsync(new DiscordWebhookBuilder().AddEmbed(new DiscordEmbedBuilder
|
||||||
{
|
{
|
||||||
|
|
48
Database.cs
48
Database.cs
|
@ -5,6 +5,7 @@ using System.Security.Cryptography;
|
||||||
using DSharpPlus;
|
using DSharpPlus;
|
||||||
using MySqlConnector;
|
using MySqlConnector;
|
||||||
using Newtonsoft.Json;
|
using Newtonsoft.Json;
|
||||||
|
using Newtonsoft.Json.Serialization;
|
||||||
|
|
||||||
namespace SupportChild;
|
namespace SupportChild;
|
||||||
|
|
||||||
|
@ -731,7 +732,7 @@ public static class Database
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static string GetInterviewTemplates()
|
public static string GetInterviewTemplatesJSON()
|
||||||
{
|
{
|
||||||
using MySqlConnection c = GetConnection();
|
using MySqlConnection c = GetConnection();
|
||||||
c.Open();
|
c.Open();
|
||||||
|
@ -750,6 +751,34 @@ public static class Database
|
||||||
return templates;
|
return templates;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static Dictionary<ulong, Interviewer.InterviewQuestion> GetInterviewTemplates()
|
||||||
|
{
|
||||||
|
using MySqlConnection c = GetConnection();
|
||||||
|
c.Open();
|
||||||
|
using MySqlCommand selection = new MySqlCommand("SELECT * FROM interviews WHERE channel_id=0", c);
|
||||||
|
selection.Prepare();
|
||||||
|
MySqlDataReader results = selection.ExecuteReader();
|
||||||
|
|
||||||
|
// Check if messages exist in the database
|
||||||
|
if (!results.Read())
|
||||||
|
{
|
||||||
|
return new Dictionary<ulong, Interviewer.InterviewQuestion>();
|
||||||
|
}
|
||||||
|
|
||||||
|
string templates = results.GetString("interview");
|
||||||
|
results.Close();
|
||||||
|
|
||||||
|
return JsonConvert.DeserializeObject<Dictionary<ulong, Interviewer.InterviewQuestion>>(templates, 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 bool SetInterviewTemplates(string templates)
|
public static bool SetInterviewTemplates(string templates)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
|
@ -854,6 +883,23 @@ public static class Database
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static bool TryDeleteInterview(ulong channelID)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using MySqlConnection c = GetConnection();
|
||||||
|
c.Open();
|
||||||
|
using MySqlCommand deletion = new MySqlCommand(@"DELETE FROM interviews WHERE channel_id=@channel_id", c);
|
||||||
|
deletion.Parameters.AddWithValue("@channel_id", channelID);
|
||||||
|
deletion.Prepare();
|
||||||
|
return deletion.ExecuteNonQuery() > 0;
|
||||||
|
}
|
||||||
|
catch (MySqlException)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public class Ticket
|
public class Ticket
|
||||||
{
|
{
|
||||||
public uint id;
|
public uint id;
|
||||||
|
|
|
@ -58,12 +58,27 @@ public static class EventHandler
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if ticket exists in the database and ticket notifications are enabled
|
// Ignore messages outside of tickets.
|
||||||
if (!Database.TryGetOpenTicket(e.Channel.Id, out Database.Ticket ticket) || !Config.ticketUpdatedNotifications)
|
if (!Database.TryGetOpenTicket(e.Channel.Id, out Database.Ticket ticket))
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Send staff notification if applicable.
|
||||||
|
if (Config.ticketUpdatedNotifications)
|
||||||
|
{
|
||||||
|
await SendTicketUpdatedMessage(e, ticket);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try to process the message as an interview response if the ticket owner replied to this bot.
|
||||||
|
if (ticket.creatorID == e.Author.Id && e.Message.ReferencedMessage?.Author == client.CurrentUser)
|
||||||
|
{
|
||||||
|
await Interviewer.ProcessResponseMessage(e.Message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static async Task SendTicketUpdatedMessage(MessageCreatedEventArgs e, Database.Ticket ticket)
|
||||||
|
{
|
||||||
// Ignore staff messages
|
// Ignore staff messages
|
||||||
if (Database.IsStaff(e.Author.Id))
|
if (Database.IsStaff(e.Author.Id))
|
||||||
{
|
{
|
||||||
|
@ -114,7 +129,7 @@ public static class EventHandler
|
||||||
catch (DiscordException ex)
|
catch (DiscordException ex)
|
||||||
{
|
{
|
||||||
Logger.Error("Exception occurred trying to add channel permissions: " + ex);
|
Logger.Error("Exception occurred trying to add channel permissions: " + ex);
|
||||||
Logger.Error("JsomMessage: " + ex.JsonMessage);
|
Logger.Error("JsonMessage: " + ex.JsonMessage);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -183,19 +198,18 @@ public static class EventHandler
|
||||||
await CloseCommand.OnConfirmed(e.Interaction);
|
await CloseCommand.OnConfirmed(e.Interaction);
|
||||||
return;
|
return;
|
||||||
case not null when e.Id.StartsWith("supportchild_newcommandbutton"):
|
case not null when e.Id.StartsWith("supportchild_newcommandbutton"):
|
||||||
await OnCategorySelection(e.Interaction);
|
await OnNewTicketSelectorUsed(e.Interaction);
|
||||||
return;
|
return;
|
||||||
case not null when e.Id.StartsWith("supportchild_newticketbutton"):
|
case not null when e.Id.StartsWith("supportchild_newticketbutton"):
|
||||||
await OnButtonUsed(e.Interaction);
|
await OnNewTicketButtonUsed(e.Interaction);
|
||||||
|
return;
|
||||||
|
case not null when e.Id.StartsWith("supportchild_interviewbutton"):
|
||||||
|
await Interviewer.ProcessButtonResponse(e.Interaction);
|
||||||
return;
|
return;
|
||||||
case "right":
|
case "right":
|
||||||
return;
|
|
||||||
case "left":
|
case "left":
|
||||||
return;
|
|
||||||
case "rightskip":
|
case "rightskip":
|
||||||
return;
|
|
||||||
case "leftskip":
|
case "leftskip":
|
||||||
return;
|
|
||||||
case "stop":
|
case "stop":
|
||||||
return;
|
return;
|
||||||
default:
|
default:
|
||||||
|
@ -206,7 +220,7 @@ public static class EventHandler
|
||||||
switch (e.Id)
|
switch (e.Id)
|
||||||
{
|
{
|
||||||
case not null when e.Id.StartsWith("supportchild_newcommandselector"):
|
case not null when e.Id.StartsWith("supportchild_newcommandselector"):
|
||||||
await OnCategorySelection(e.Interaction);
|
await OnNewTicketSelectorUsed(e.Interaction);
|
||||||
return;
|
return;
|
||||||
case not null when e.Id.StartsWith("supportchild_newticketselector"):
|
case not null when e.Id.StartsWith("supportchild_newticketselector"):
|
||||||
await CreateSelectionBoxPanelCommand.OnSelectionMenuUsed(e.Interaction);
|
await CreateSelectionBoxPanelCommand.OnSelectionMenuUsed(e.Interaction);
|
||||||
|
@ -241,13 +255,13 @@ public static class EventHandler
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static async Task OnButtonUsed(DiscordInteraction interaction)
|
private static async Task OnNewTicketButtonUsed(DiscordInteraction interaction)
|
||||||
{
|
{
|
||||||
await interaction.CreateResponseAsync(DiscordInteractionResponseType.DeferredChannelMessageWithSource, new DiscordInteractionResponseBuilder().AsEphemeral());
|
await interaction.CreateResponseAsync(DiscordInteractionResponseType.DeferredChannelMessageWithSource, new DiscordInteractionResponseBuilder().AsEphemeral());
|
||||||
|
|
||||||
if (!ulong.TryParse(interaction.Data.CustomId.Replace("supportchild_newticketbutton ", ""), out ulong categoryID) || categoryID == 0)
|
if (!ulong.TryParse(interaction.Data.CustomId.Replace("supportchild_newticketbutton ", ""), out ulong categoryID) || categoryID == 0)
|
||||||
{
|
{
|
||||||
Logger.Warn("Invalid ID: " + interaction.Data.CustomId.Replace("supportchild_newticketbutton ", ""));
|
Logger.Warn("Invalid ticket button ID: " + interaction.Data.CustomId.Replace("supportchild_newticketbutton ", ""));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -271,7 +285,7 @@ public static class EventHandler
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static async Task OnCategorySelection(DiscordInteraction interaction)
|
private static async Task OnNewTicketSelectorUsed(DiscordInteraction interaction)
|
||||||
{
|
{
|
||||||
string stringID;
|
string stringID;
|
||||||
switch (interaction.Data.ComponentType)
|
switch (interaction.Data.ComponentType)
|
||||||
|
|
359
Interviewer.cs
359
Interviewer.cs
|
@ -1,12 +1,13 @@
|
||||||
using System.Collections.Generic;
|
using System;
|
||||||
using System.Diagnostics.CodeAnalysis;
|
using System.Collections;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Collections.Specialized;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using DSharpPlus;
|
using System.Text.RegularExpressions;
|
||||||
|
using System.Threading.Tasks;
|
||||||
using DSharpPlus.Entities;
|
using DSharpPlus.Entities;
|
||||||
using Newtonsoft.Json;
|
using Newtonsoft.Json;
|
||||||
using Newtonsoft.Json.Converters;
|
using Newtonsoft.Json.Converters;
|
||||||
using Newtonsoft.Json.Linq;
|
|
||||||
using Newtonsoft.Json.Serialization;
|
|
||||||
|
|
||||||
namespace SupportChild;
|
namespace SupportChild;
|
||||||
|
|
||||||
|
@ -14,7 +15,8 @@ public static class Interviewer
|
||||||
{
|
{
|
||||||
public enum QuestionType
|
public enum QuestionType
|
||||||
{
|
{
|
||||||
FAIL,
|
ERROR,
|
||||||
|
CANCEL,
|
||||||
DONE,
|
DONE,
|
||||||
BUTTONS,
|
BUTTONS,
|
||||||
SELECTOR,
|
SELECTOR,
|
||||||
|
@ -27,6 +29,8 @@ 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: Optional properties: embed title, button style
|
||||||
|
|
||||||
// Message contents sent to the user.
|
// Message contents sent to the user.
|
||||||
[JsonProperty("message")]
|
[JsonProperty("message")]
|
||||||
public string message;
|
public string message;
|
||||||
|
@ -40,26 +44,84 @@ public static class Interviewer
|
||||||
[JsonProperty("color")]
|
[JsonProperty("color")]
|
||||||
public string 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.
|
// Used as label for this question in the post-interview summary.
|
||||||
[JsonProperty("summary-field")]
|
[JsonProperty("summary-field")]
|
||||||
public string summaryField;
|
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.
|
// Possible questions to ask next, or DONE/FAIL type in order to finish interview.
|
||||||
[JsonProperty("paths")]
|
[JsonProperty("paths")]
|
||||||
public Dictionary<string, InterviewQuestion> paths;
|
public Dictionary<string, InterviewQuestion> paths;
|
||||||
|
|
||||||
|
// The following parameters are populated by the bot, not the json template.
|
||||||
|
|
||||||
|
// The ID of this message where the bot asked this question.
|
||||||
|
[JsonProperty("message-id")]
|
||||||
|
public ulong messageID;
|
||||||
|
|
||||||
|
// The contents of the user's answer.
|
||||||
|
[JsonProperty("answer")]
|
||||||
|
public string answer;
|
||||||
|
|
||||||
|
// The ID of the user's answer message if this is a TEXT_INPUT type.
|
||||||
|
[JsonProperty("answer-id")]
|
||||||
|
public ulong answerID;
|
||||||
|
|
||||||
|
public bool TryGetCurrentQuestion(out InterviewQuestion question)
|
||||||
|
{
|
||||||
|
// This object has not been initialized, we have checked too deep.
|
||||||
|
if (messageID == 0)
|
||||||
|
{
|
||||||
|
question = null;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check children.
|
||||||
|
foreach (KeyValuePair<string, InterviewQuestion> path in paths)
|
||||||
|
{
|
||||||
|
// This child either is the one we are looking for or contains the one we are looking for.
|
||||||
|
if (path.Value.TryGetCurrentQuestion(out question))
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// This object is the deepest object with a message ID set, meaning it is the latest asked question.
|
||||||
|
question = this;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void GetSummary(ref OrderedDictionary summary)
|
||||||
|
{
|
||||||
|
if (!string.IsNullOrWhiteSpace(summaryField))
|
||||||
|
{
|
||||||
|
summary.Add(summaryField, answer);
|
||||||
|
}
|
||||||
|
|
||||||
|
// This will always contain exactly one or zero children.
|
||||||
|
foreach (KeyValuePair<string, InterviewQuestion> path in paths)
|
||||||
|
{
|
||||||
|
path.Value.GetSummary(ref summary);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void GetMessageIDs(ref List<ulong> messageIDs)
|
||||||
|
{
|
||||||
|
if (messageID != 0)
|
||||||
|
{
|
||||||
|
messageIDs.Add(messageID);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (answerID != 0)
|
||||||
|
{
|
||||||
|
messageIDs.Add(answerID);
|
||||||
|
}
|
||||||
|
|
||||||
|
// This will always contain exactly one or zero children.
|
||||||
|
foreach (KeyValuePair<string, InterviewQuestion> path in paths)
|
||||||
|
{
|
||||||
|
path.Value.GetMessageIDs(ref messageIDs);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// This class is identical to the one above and just exists as a hack to get JSON validation when
|
// This class is identical to the one above and just exists as a hack to get JSON validation when
|
||||||
|
@ -80,77 +142,256 @@ public static class Interviewer
|
||||||
[JsonProperty("color", Required = Required.Always)]
|
[JsonProperty("color", Required = Required.Always)]
|
||||||
public string color;
|
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.
|
// Used as label for this question in the post-interview summary.
|
||||||
[JsonProperty("summary-field", Required = Required.Default)]
|
[JsonProperty("summary-field", Required = Required.Always)]
|
||||||
public string summaryField;
|
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.
|
// Possible questions to ask next, or DONE/FAIL type in order to finish interview.
|
||||||
[JsonProperty("paths", Required = Required.Always)]
|
[JsonProperty("paths", Required = Required.Always)]
|
||||||
public Dictionary<string, ValidatedInterviewQuestion> paths;
|
public Dictionary<string, ValidatedInterviewQuestion> paths;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static Dictionary<ulong, InterviewQuestion> categoryInterviews = [];
|
private static Dictionary<ulong, InterviewQuestion> interviewTemplates = [];
|
||||||
|
|
||||||
private static Dictionary<ulong, InterviewQuestion> activeInterviews = [];
|
private static Dictionary<ulong, InterviewQuestion> activeInterviews = [];
|
||||||
|
|
||||||
public static void ParseTemplates(JToken interviewConfig)
|
// TODO: Maybe split into two functions?
|
||||||
{
|
public static void Reload()
|
||||||
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;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void LoadActiveInterviews()
|
|
||||||
{
|
{
|
||||||
|
interviewTemplates = Database.GetInterviewTemplates();
|
||||||
activeInterviews = Database.GetAllInterviews();
|
activeInterviews = Database.GetAllInterviews();
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void StartInterview(DiscordChannel channel)
|
public static async void StartInterview(DiscordChannel channel)
|
||||||
{
|
{
|
||||||
if (channel.Parent == null)
|
if (channel.Parent == null)
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (categoryInterviews.TryGetValue(channel.Parent.Id, out InterviewQuestion interview))
|
if (interviewTemplates.TryGetValue(channel.Parent.Id, out InterviewQuestion interview))
|
||||||
{
|
{
|
||||||
CreateQuestion(channel, interview);
|
await CreateQuestion(channel, interview);
|
||||||
Database.SaveInterview(channel.Id, interview);
|
Database.SaveInterview(channel.Id, interview);
|
||||||
|
Reload();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void ProcessResponse(DiscordMessage message)
|
public static bool IsInterviewActive(ulong channelID)
|
||||||
|
{
|
||||||
|
return activeInterviews.ContainsKey(channelID);
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Add selection box handling.
|
||||||
|
|
||||||
|
public static async Task ProcessButtonResponse(DiscordInteraction interaction)
|
||||||
|
{
|
||||||
|
// TODO: Add error responses.
|
||||||
|
|
||||||
|
if (interaction?.Channel == null || interaction?.Message == null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await interaction.CreateResponseAsync(DiscordInteractionResponseType.DeferredMessageUpdate);
|
||||||
|
|
||||||
|
// Could not find active interview.
|
||||||
|
if (!activeInterviews.TryGetValue(interaction.Channel.Id, out InterviewQuestion interviewRoot))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Could not find message id in interview.
|
||||||
|
if (!interviewRoot.TryGetCurrentQuestion(out InterviewQuestion currentQuestion))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// This button is for an older question.
|
||||||
|
if (interaction.Message.Id != currentQuestion.messageID)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse the response index from the button.
|
||||||
|
if (!int.TryParse(interaction.Data.CustomId.Replace("supportchild_interviewbutton ", ""), out int pathIndex))
|
||||||
|
{
|
||||||
|
Logger.Error("Invalid interview button index: " + interaction.Data.CustomId.Replace("supportchild_interviewbutton ", ""));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pathIndex >= currentQuestion.paths.Count || pathIndex < 0)
|
||||||
|
{
|
||||||
|
Logger.Error("Invalid interview button index: " + pathIndex);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
KeyValuePair<string, InterviewQuestion> questionPath = currentQuestion.paths.ElementAt(pathIndex);
|
||||||
|
|
||||||
|
|
||||||
|
currentQuestion.answer = interaction.Data.Name;
|
||||||
|
currentQuestion.answerID = 0;
|
||||||
|
|
||||||
|
// Create next question, or finish the interview.
|
||||||
|
InterviewQuestion nextQuestion = questionPath.Value;
|
||||||
|
switch (questionPath.Value.type)
|
||||||
|
{
|
||||||
|
case QuestionType.TEXT_INPUT:
|
||||||
|
await CreateQuestion(interaction.Channel, nextQuestion);
|
||||||
|
break;
|
||||||
|
case QuestionType.BUTTONS:
|
||||||
|
await CreateQuestion(interaction.Channel, nextQuestion);
|
||||||
|
// TODO: Remove buttons
|
||||||
|
break;
|
||||||
|
case QuestionType.SELECTOR:
|
||||||
|
await CreateQuestion(interaction.Channel, nextQuestion);
|
||||||
|
// TODO: Remove selector
|
||||||
|
break;
|
||||||
|
case QuestionType.DONE:
|
||||||
|
// TODO: Create summary.
|
||||||
|
// TODO: Remove previous interview messages.
|
||||||
|
// TODO: Remove active interview.
|
||||||
|
Logger.Error("INTERVIEW DONE");
|
||||||
|
break;
|
||||||
|
case QuestionType.CANCEL:
|
||||||
|
default:
|
||||||
|
// TODO: Post fail message.
|
||||||
|
// TODO: Remove active interview.
|
||||||
|
Logger.Error("INTERVIEW FAILED");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove other paths.
|
||||||
|
currentQuestion.paths = new Dictionary<string, InterviewQuestion>
|
||||||
|
{
|
||||||
|
{ questionPath.Key, nextQuestion }
|
||||||
|
};
|
||||||
|
|
||||||
|
await interaction.EditOriginalResponseAsync(new DiscordWebhookBuilder().AddEmbed(interaction.Message.Embeds[0]));
|
||||||
|
|
||||||
|
Database.SaveInterview(interaction.Channel.Id, interviewRoot);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static async Task ProcessResponseMessage(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: 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 FAIL event, cancelling the interview.
|
||||||
// TODO: Handle DONE event, creating a summary.
|
// TODO: Handle DONE event, creating a summary.
|
||||||
|
|
||||||
//Database.SaveInterview(channel.Id, interview);
|
// Either the message or the referenced message is null.
|
||||||
|
if (message.Channel == null || message.ReferencedMessage?.Channel == null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static async void CreateQuestion(DiscordChannel channel, InterviewQuestion question)
|
// The channel does not have an active interview.
|
||||||
|
if (!activeInterviews.TryGetValue(message.ReferencedMessage.Channel.Id, out InterviewQuestion interviewRoot))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!interviewRoot.TryGetCurrentQuestion(out InterviewQuestion currentQuestion))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The user responded to something other than the latest interview question.
|
||||||
|
if (message.ReferencedMessage.Id != currentQuestion.messageID)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The user responded to a question which does not take a text response.
|
||||||
|
if (currentQuestion.type != QuestionType.TEXT_INPUT)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ((string questionString, InterviewQuestion nextQuestion) in currentQuestion.paths)
|
||||||
|
{
|
||||||
|
// Skip to the matching path.
|
||||||
|
if (!Regex.IsMatch(message.Content, questionString)) continue;
|
||||||
|
|
||||||
|
// TODO: Refactor this into separate function to reduce duplication
|
||||||
|
|
||||||
|
currentQuestion.answer = message.Content;
|
||||||
|
currentQuestion.answerID = message.Id;
|
||||||
|
|
||||||
|
// Create next question, or finish the interview.
|
||||||
|
switch (nextQuestion.type)
|
||||||
|
{
|
||||||
|
case QuestionType.ERROR:
|
||||||
|
break;
|
||||||
|
case QuestionType.TEXT_INPUT:
|
||||||
|
case QuestionType.BUTTONS:
|
||||||
|
case QuestionType.SELECTOR:
|
||||||
|
await CreateQuestion(message.Channel, nextQuestion);
|
||||||
|
|
||||||
|
// Remove other paths.
|
||||||
|
currentQuestion.paths = new Dictionary<string, InterviewQuestion>
|
||||||
|
{
|
||||||
|
{ questionString, nextQuestion }
|
||||||
|
};
|
||||||
|
|
||||||
|
Database.SaveInterview(message.Channel.Id, interviewRoot);
|
||||||
|
break;
|
||||||
|
case QuestionType.DONE:
|
||||||
|
// TODO: Remove previous interview messages.
|
||||||
|
OrderedDictionary summaryFields = new OrderedDictionary();
|
||||||
|
interviewRoot.GetSummary(ref summaryFields);
|
||||||
|
|
||||||
|
DiscordEmbedBuilder embed = new DiscordEmbedBuilder()
|
||||||
|
{
|
||||||
|
Color = Utilities.StringToColor(nextQuestion.color),
|
||||||
|
Title = "Summary:",
|
||||||
|
Description = nextQuestion.message,
|
||||||
|
};
|
||||||
|
|
||||||
|
foreach (DictionaryEntry entry in summaryFields)
|
||||||
|
{
|
||||||
|
embed.AddField((string)entry.Key, (string)entry.Value);
|
||||||
|
}
|
||||||
|
|
||||||
|
await message.Channel.SendMessageAsync(embed);
|
||||||
|
|
||||||
|
List<ulong> previousMessages = new List<ulong> { };
|
||||||
|
interviewRoot.GetMessageIDs(ref previousMessages);
|
||||||
|
|
||||||
|
foreach (ulong previousMessageID in previousMessages)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Logger.Debug("Deleting message: " + previousMessageID);
|
||||||
|
DiscordMessage previousMessage = await message.Channel.GetMessageAsync(previousMessageID);
|
||||||
|
await message.Channel.DeleteMessageAsync(previousMessage, "Deleting old interview message.");
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
Logger.Error("Failed to delete old interview message: " + e.Message);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!Database.TryDeleteInterview(message.Channel.Id))
|
||||||
|
{
|
||||||
|
Logger.Error("Could not delete interview from database. Channel ID: " + message.Channel.Id);
|
||||||
|
}
|
||||||
|
Reload();
|
||||||
|
return;
|
||||||
|
case QuestionType.CANCEL:
|
||||||
|
default:
|
||||||
|
// TODO: Post fail message.
|
||||||
|
// TODO: Remove active interview.
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: No matching path found.
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private static async Task CreateQuestion(DiscordChannel channel, InterviewQuestion question)
|
||||||
{
|
{
|
||||||
DiscordMessageBuilder msgBuilder = new DiscordMessageBuilder();
|
DiscordMessageBuilder msgBuilder = new DiscordMessageBuilder();
|
||||||
DiscordEmbedBuilder embed = new DiscordEmbedBuilder()
|
DiscordEmbedBuilder embed = new DiscordEmbedBuilder()
|
||||||
|
@ -190,10 +431,10 @@ public static class Interviewer
|
||||||
msgBuilder.AddComponents(selectionComponents);
|
msgBuilder.AddComponents(selectionComponents);
|
||||||
break;
|
break;
|
||||||
case QuestionType.TEXT_INPUT:
|
case QuestionType.TEXT_INPUT:
|
||||||
embed.WithFooter("Reply to this message with your answer.");
|
embed.WithFooter("Reply to this message with your answer. You cannot include images or files.");
|
||||||
break;
|
break;
|
||||||
case QuestionType.DONE:
|
case QuestionType.DONE:
|
||||||
case QuestionType.FAIL:
|
case QuestionType.CANCEL:
|
||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
|
@ -152,8 +152,6 @@ internal static class SupportChild
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
Logger.Log("Loading interviews from database...");
|
Logger.Log("Loading interviews from database...");
|
||||||
Interviewer.ParseTemplates(Database.GetInterviewTemplates());
|
|
||||||
Interviewer.LoadActiveInterviews();
|
|
||||||
}
|
}
|
||||||
catch (Exception e)
|
catch (Exception e)
|
||||||
{
|
{
|
||||||
|
|
Loading…
Reference in a new issue