Moved interview templates to own table
This splits them into different entries making it possible to separately edit different ones at the same time.
This commit is contained in:
parent
24defe0128
commit
414dbae62f
8 changed files with 515 additions and 425 deletions
|
@ -244,19 +244,72 @@ public class AdminCommands
|
||||||
}
|
}
|
||||||
|
|
||||||
[RequireGuild]
|
[RequireGuild]
|
||||||
[Command("getinterviewtemplates")]
|
[Command("getinterviewtemplate")]
|
||||||
[Description("Provides a copy of the interview templates which you can edit and then reupload.")]
|
[Description("Provides a copy of the interview template for a category which you can edit and then reupload.")]
|
||||||
public async Task GetInterviewTemplates(SlashCommandContext command)
|
public async Task GetInterviewTemplate(SlashCommandContext command,
|
||||||
|
[Parameter("category")][Description("The category to get the template for.")] DiscordChannel category)
|
||||||
{
|
{
|
||||||
MemoryStream stream = new(Encoding.UTF8.GetBytes(Database.GetInterviewTemplatesJSON()));
|
if (!category?.IsCategory ?? true)
|
||||||
await command.RespondAsync(new DiscordInteractionResponseBuilder().AddFile("interview-templates.json", stream).AsEphemeral());
|
{
|
||||||
|
await command.RespondAsync(new DiscordEmbedBuilder
|
||||||
|
{
|
||||||
|
Color = DiscordColor.Red,
|
||||||
|
Description = "That channel is not a category."
|
||||||
|
}, true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!Database.TryGetCategory(category.Id, out Database.Category categoryData))
|
||||||
|
{
|
||||||
|
await command.RespondAsync(new DiscordEmbedBuilder
|
||||||
|
{
|
||||||
|
Color = DiscordColor.Red,
|
||||||
|
Description = "That category is not registered with the bot."
|
||||||
|
}, true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
string interviewTemplateJSON = Database.GetInterviewTemplateJSON(category.Id);
|
||||||
|
if (interviewTemplateJSON == null)
|
||||||
|
{
|
||||||
|
string defaultTemplate =
|
||||||
|
"{\n" +
|
||||||
|
" \"category-id\": \"" + category.Id + "\",\n" +
|
||||||
|
" \"interview\":\n" +
|
||||||
|
" {\n" +
|
||||||
|
" \"message\": \"\",\n" +
|
||||||
|
" \"type\": \"\",\n" +
|
||||||
|
" \"color\": \"\",\n" +
|
||||||
|
" \"paths\":\n" +
|
||||||
|
" {\n" +
|
||||||
|
" \n" +
|
||||||
|
" }\n" +
|
||||||
|
" }\n" +
|
||||||
|
"}";
|
||||||
|
MemoryStream stream = new(Encoding.UTF8.GetBytes(defaultTemplate));
|
||||||
|
|
||||||
|
DiscordInteractionResponseBuilder response = new DiscordInteractionResponseBuilder().AddEmbed(new DiscordEmbedBuilder
|
||||||
|
{
|
||||||
|
Color = DiscordColor.Green,
|
||||||
|
Description = "No interview template found for this category. A default template has been generated."
|
||||||
|
}).AddFile("interview-template-" + category.Id + ".json", stream).AsEphemeral();
|
||||||
|
await command.RespondAsync(response);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
MemoryStream stream = new(Encoding.UTF8.GetBytes(interviewTemplateJSON));
|
||||||
|
await command.RespondAsync(new DiscordInteractionResponseBuilder().AddFile("interview-template-" + category.Id + ".json", stream).AsEphemeral());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
[RequireGuild]
|
[RequireGuild]
|
||||||
[Command("setinterviewtemplates")]
|
[Command("setinterviewtemplate")]
|
||||||
[Description("Uploads an interview template file.")]
|
[Description("Uploads an interview template file.")]
|
||||||
public async Task SetInterviewTemplates(SlashCommandContext command, [Parameter("file")] DiscordAttachment file)
|
public async Task SetInterviewTemplate(SlashCommandContext command,
|
||||||
|
[Parameter("file")][Description("The file containing the template.")] DiscordAttachment file)
|
||||||
{
|
{
|
||||||
|
await command.DeferResponseAsync(true);
|
||||||
|
|
||||||
if (!file.MediaType?.Contains("application/json") ?? false)
|
if (!file.MediaType?.Contains("application/json") ?? false)
|
||||||
{
|
{
|
||||||
await command.RespondAsync(new DiscordEmbedBuilder
|
await command.RespondAsync(new DiscordEmbedBuilder
|
||||||
|
@ -275,7 +328,7 @@ public class AdminCommands
|
||||||
List<string> errors = [];
|
List<string> errors = [];
|
||||||
|
|
||||||
// Convert it to an interview object to validate the template
|
// Convert it to an interview object to validate the template
|
||||||
Dictionary<ulong, Interviewer.ValidatedInterviewQuestion> interview = JsonConvert.DeserializeObject<Dictionary<ulong, Interviewer.ValidatedInterviewQuestion>>(json, new JsonSerializerSettings()
|
Interviews.ValidatedTemplate template = JsonConvert.DeserializeObject<Interviews.ValidatedTemplate>(json, new JsonSerializerSettings()
|
||||||
{
|
{
|
||||||
//NullValueHandling = NullValueHandling.Include,
|
//NullValueHandling = NullValueHandling.Include,
|
||||||
MissingMemberHandling = MissingMemberHandling.Error,
|
MissingMemberHandling = MissingMemberHandling.Error,
|
||||||
|
@ -293,26 +346,41 @@ public class AdminCommands
|
||||||
}
|
}
|
||||||
|
|
||||||
Logger.Debug("Exception occured when trying to upload interview template:\n" + args.ErrorContext.Error);
|
Logger.Debug("Exception occured when trying to upload interview template:\n" + args.ErrorContext.Error);
|
||||||
args.ErrorContext.Handled = true;
|
args.ErrorContext.Handled = false;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
if (interview != null)
|
DiscordChannel category = await SupportChild.client.GetChannelAsync(template.categoryID);
|
||||||
|
if (!category.IsCategory)
|
||||||
{
|
{
|
||||||
foreach (KeyValuePair<ulong, Interviewer.ValidatedInterviewQuestion> interviewRoot in interview)
|
await command.RespondAsync(new DiscordEmbedBuilder
|
||||||
{
|
{
|
||||||
interviewRoot.Value.Validate(ref errors, out int summaryCount, out int summaryMaxLength);
|
Color = DiscordColor.Red,
|
||||||
|
Description = "The category ID in the uploaded JSON structure is not a valid category."
|
||||||
|
}, true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!Database.TryGetCategory(category.Id, out Database.Category _))
|
||||||
|
{
|
||||||
|
await command.RespondAsync(new DiscordEmbedBuilder
|
||||||
|
{
|
||||||
|
Color = DiscordColor.Red,
|
||||||
|
Description = "The category ID in the uploaded JSON structure is not a category registered with the bot, use /addcategory first."
|
||||||
|
}, true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
template.interview.Validate(ref errors, out int summaryCount, out int summaryMaxLength);
|
||||||
|
|
||||||
if (summaryCount > 25)
|
if (summaryCount > 25)
|
||||||
{
|
{
|
||||||
errors.Add("A summary cannot contain more than 25 fields, but you have " + summaryCount + " fields in one of your interview branches.");
|
errors.Add("A summary cannot contain more than 25 fields, but you have " + summaryCount + " fields in at least one of your interview branches.");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (summaryMaxLength >= 6000)
|
if (summaryMaxLength >= 6000)
|
||||||
{
|
{
|
||||||
errors.Add("A summary cannot contain more than 6000 characters, but one of your branches has the possibility of its summary reaching " + summaryMaxLength + " characters.");
|
errors.Add("A summary cannot contain more than 6000 characters, but at least one of your branches has the possibility of its summary reaching " + summaryMaxLength + " characters.");
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (errors.Count != 0)
|
if (errors.Count != 0)
|
||||||
|
@ -335,7 +403,32 @@ public class AdminCommands
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
Database.SetInterviewTemplates(JsonConvert.SerializeObject(interview, Formatting.Indented));
|
if (!Database.SetInterviewTemplate(template))
|
||||||
|
{
|
||||||
|
await command.RespondAsync(new DiscordEmbedBuilder
|
||||||
|
{
|
||||||
|
Color = DiscordColor.Red,
|
||||||
|
Description = "An error occured trying to write the new template to database."
|
||||||
|
}, true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
MemoryStream memStream = new(Encoding.UTF8.GetBytes(Database.GetInterviewTemplateJSON(template.categoryID)));
|
||||||
|
|
||||||
|
// Log it if the log channel exists
|
||||||
|
DiscordChannel logChannel = await SupportChild.client.GetChannelAsync(Config.logChannel);
|
||||||
|
await logChannel.SendMessageAsync(new DiscordMessageBuilder().AddEmbed(new DiscordEmbedBuilder
|
||||||
|
{
|
||||||
|
Color = DiscordColor.Green,
|
||||||
|
Description = command.User.Mention + " uploaded a new interview template for the `" + category.Name + "` category."
|
||||||
|
}).AddFile("interview-template-" + template.categoryID + ".json", memStream));
|
||||||
|
}
|
||||||
|
catch (NotFoundException)
|
||||||
|
{
|
||||||
|
Logger.Error("Could not find the log channel.");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
catch (Exception e)
|
catch (Exception e)
|
||||||
{
|
{
|
||||||
|
@ -354,18 +447,5 @@ public class AdminCommands
|
||||||
Description = "Uploaded interview template."
|
Description = "Uploaded interview template."
|
||||||
}, true);
|
}, true);
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
DiscordChannel logChannel = await SupportChild.client.GetChannelAsync(Config.logChannel);
|
|
||||||
await logChannel.SendMessageAsync(new DiscordEmbedBuilder
|
|
||||||
{
|
|
||||||
Color = DiscordColor.Green,
|
|
||||||
Description = command.Channel.Mention + " uploaded new interview templates.",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
catch (NotFoundException)
|
|
||||||
{
|
|
||||||
Logger.Error("Could not find the log channel.");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -8,6 +8,7 @@ using DSharpPlus.Commands.ContextChecks;
|
||||||
using DSharpPlus.Commands.Processors.SlashCommands;
|
using DSharpPlus.Commands.Processors.SlashCommands;
|
||||||
using DSharpPlus.Entities;
|
using DSharpPlus.Entities;
|
||||||
using DSharpPlus.Exceptions;
|
using DSharpPlus.Exceptions;
|
||||||
|
using SupportChild.Interviews;
|
||||||
|
|
||||||
namespace SupportChild.Commands;
|
namespace SupportChild.Commands;
|
||||||
|
|
||||||
|
|
|
@ -4,6 +4,7 @@ using DSharpPlus.Commands;
|
||||||
using DSharpPlus.Commands.Processors.SlashCommands;
|
using DSharpPlus.Commands.Processors.SlashCommands;
|
||||||
using DSharpPlus.Entities;
|
using DSharpPlus.Entities;
|
||||||
using DSharpPlus.Exceptions;
|
using DSharpPlus.Exceptions;
|
||||||
|
using SupportChild.Interviews;
|
||||||
|
|
||||||
namespace SupportChild.Commands;
|
namespace SupportChild.Commands;
|
||||||
|
|
||||||
|
|
101
Database.cs
101
Database.cs
|
@ -114,6 +114,11 @@ public static class Database
|
||||||
"channel_id BIGINT UNSIGNED NOT NULL PRIMARY KEY," +
|
"channel_id BIGINT UNSIGNED NOT NULL PRIMARY KEY," +
|
||||||
"interview JSON NOT NULL)",
|
"interview JSON NOT NULL)",
|
||||||
c);
|
c);
|
||||||
|
using MySqlCommand createInterviewTemplates = new MySqlCommand(
|
||||||
|
"CREATE TABLE IF NOT EXISTS interview_templates(" +
|
||||||
|
"category_id BIGINT UNSIGNED NOT NULL PRIMARY KEY," +
|
||||||
|
"template JSON NOT NULL)",
|
||||||
|
c);
|
||||||
c.Open();
|
c.Open();
|
||||||
createTickets.ExecuteNonQuery();
|
createTickets.ExecuteNonQuery();
|
||||||
createBlacklisted.ExecuteNonQuery();
|
createBlacklisted.ExecuteNonQuery();
|
||||||
|
@ -122,6 +127,7 @@ public static class Database
|
||||||
createMessages.ExecuteNonQuery();
|
createMessages.ExecuteNonQuery();
|
||||||
createCategories.ExecuteNonQuery();
|
createCategories.ExecuteNonQuery();
|
||||||
createInterviews.ExecuteNonQuery();
|
createInterviews.ExecuteNonQuery();
|
||||||
|
createInterviewTemplates.ExecuteNonQuery();
|
||||||
}
|
}
|
||||||
|
|
||||||
public static bool IsOpenTicket(ulong channelID)
|
public static bool IsOpenTicket(ulong channelID)
|
||||||
|
@ -732,82 +738,91 @@ public static class Database
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static string GetInterviewTemplatesJSON()
|
public static string GetInterviewTemplateJSON(ulong categoryID)
|
||||||
{
|
{
|
||||||
using MySqlConnection c = GetConnection();
|
using MySqlConnection c = GetConnection();
|
||||||
c.Open();
|
c.Open();
|
||||||
using MySqlCommand selection = new MySqlCommand("SELECT * FROM interviews WHERE channel_id=0", c);
|
using MySqlCommand selection = new MySqlCommand("SELECT * FROM interview_templates WHERE category_id=@category_id", c);
|
||||||
|
selection.Parameters.AddWithValue("@category_id", categoryID);
|
||||||
selection.Prepare();
|
selection.Prepare();
|
||||||
MySqlDataReader results = selection.ExecuteReader();
|
MySqlDataReader results = selection.ExecuteReader();
|
||||||
|
|
||||||
// Check if messages exist in the database
|
// Return a default template if it doesn't exist.
|
||||||
if (!results.Read())
|
if (!results.Read())
|
||||||
{
|
{
|
||||||
return "{}";
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
string templates = results.GetString("interview");
|
string templates = results.GetString("template");
|
||||||
results.Close();
|
results.Close();
|
||||||
return templates;
|
return templates;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Still returns true if there are no templates, returns false if the templates are invalid.
|
public static bool TryGetInterviewTemplate(ulong categoryID, out Interviews.InterviewQuestion template)
|
||||||
public static bool TryGetInterviewTemplates(out Dictionary<ulong, Interviewer.InterviewQuestion> templates)
|
|
||||||
{
|
{
|
||||||
using MySqlConnection c = GetConnection();
|
using MySqlConnection c = GetConnection();
|
||||||
c.Open();
|
c.Open();
|
||||||
using MySqlCommand selection = new MySqlCommand("SELECT * FROM interviews WHERE channel_id=0", c);
|
using MySqlCommand selection = new MySqlCommand("SELECT * FROM interview_templates WHERE category_id=@category_id", c);
|
||||||
|
selection.Parameters.AddWithValue("@category_id", categoryID);
|
||||||
selection.Prepare();
|
selection.Prepare();
|
||||||
MySqlDataReader results = selection.ExecuteReader();
|
MySqlDataReader results = selection.ExecuteReader();
|
||||||
|
|
||||||
// Check if messages exist in the database
|
// Check if messages exist in the database
|
||||||
if (!results.Read())
|
if (!results.Read())
|
||||||
{
|
{
|
||||||
templates = new Dictionary<ulong, Interviewer.InterviewQuestion>();
|
template = null;
|
||||||
return true;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
string templatesString = results.GetString("interview");
|
string templateString = results.GetString("template");
|
||||||
results.Close();
|
results.Close();
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
templates = JsonConvert.DeserializeObject<Dictionary<ulong, Interviewer.InterviewQuestion>>(templatesString, new JsonSerializerSettings
|
template = JsonConvert.DeserializeObject<Interviews.Template>(templateString, new JsonSerializerSettings
|
||||||
{
|
{
|
||||||
Error = delegate (object sender, ErrorEventArgs args)
|
Error = delegate (object sender, ErrorEventArgs args)
|
||||||
{
|
{
|
||||||
Logger.Error("Error occured when trying to read interview templates from database: " + args.ErrorContext.Error.Message);
|
Logger.Error("Error occured when trying to read interview template '" + categoryID + "' from database: " + args.ErrorContext.Error.Message);
|
||||||
Logger.Debug("Detailed exception:", args.ErrorContext.Error);
|
Logger.Debug("Detailed exception:", args.ErrorContext.Error);
|
||||||
args.ErrorContext.Handled = false;
|
args.ErrorContext.Handled = false;
|
||||||
}
|
}
|
||||||
});
|
}).interview;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
catch (Exception)
|
catch (Exception)
|
||||||
{
|
{
|
||||||
templates = null;
|
template = null;
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static bool SetInterviewTemplates(string templates)
|
public static bool SetInterviewTemplate(Interviews.ValidatedTemplate template)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
string query;
|
string templateString = JsonConvert.SerializeObject(template, new JsonSerializerSettings()
|
||||||
if (TryGetInterview(0, out _))
|
|
||||||
{
|
{
|
||||||
query = "UPDATE interviews SET interview = @interview WHERE channel_id = 0";
|
NullValueHandling = NullValueHandling.Ignore,
|
||||||
|
MissingMemberHandling = MissingMemberHandling.Error,
|
||||||
|
Formatting = Formatting.Indented
|
||||||
|
});
|
||||||
|
|
||||||
|
string query;
|
||||||
|
if (TryGetInterviewTemplate(template.categoryID, out _))
|
||||||
|
{
|
||||||
|
query = "UPDATE interview_templates SET template = @template WHERE category_id=@category_id";
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
query = "INSERT INTO interviews (channel_id,interview) VALUES (0, @interview)";
|
query = "INSERT INTO interview_templates (category_id,template) VALUES (@category_id, @template)";
|
||||||
}
|
}
|
||||||
|
|
||||||
using MySqlConnection c = GetConnection();
|
using MySqlConnection c = GetConnection();
|
||||||
c.Open();
|
c.Open();
|
||||||
using MySqlCommand cmd = new MySqlCommand(query, c);
|
using MySqlCommand cmd = new MySqlCommand(query, c);
|
||||||
cmd.Parameters.AddWithValue("@interview", templates);
|
cmd.Parameters.AddWithValue("@category_id", template.categoryID);
|
||||||
|
cmd.Parameters.AddWithValue("@template", templateString);
|
||||||
cmd.Prepare();
|
cmd.Prepare();
|
||||||
return cmd.ExecuteNonQuery() > 0;
|
return cmd.ExecuteNonQuery() > 0;
|
||||||
}
|
}
|
||||||
|
@ -817,45 +832,7 @@ public static class Database
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static Dictionary<ulong, Interviewer.InterviewQuestion> GetAllInterviews()
|
public static bool SaveInterview(ulong channelID, Interviews.InterviewQuestion interview)
|
||||||
{
|
|
||||||
using MySqlConnection c = GetConnection();
|
|
||||||
c.Open();
|
|
||||||
using MySqlCommand selection = new MySqlCommand("SELECT * FROM interviews", c);
|
|
||||||
selection.Prepare();
|
|
||||||
MySqlDataReader results = selection.ExecuteReader();
|
|
||||||
|
|
||||||
// Check if messages exist in the database
|
|
||||||
if (!results.Read())
|
|
||||||
{
|
|
||||||
return new Dictionary<ulong, Interviewer.InterviewQuestion>();
|
|
||||||
}
|
|
||||||
|
|
||||||
Dictionary<ulong, Interviewer.InterviewQuestion> questions = new();
|
|
||||||
do
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
// Channel ID 0 is the interview template, don't read it here.
|
|
||||||
if (results.GetUInt64("channel_id") != 0)
|
|
||||||
{
|
|
||||||
questions.Add(results.GetUInt64("channel_id"), JsonConvert.DeserializeObject<Interviewer.InterviewQuestion>(results.GetString("interview")));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (Exception e)
|
|
||||||
{
|
|
||||||
Logger.Warn("Error occured when trying to read interview from database, it will not be loaded until manually fixed in the database.\nError message: " + e.Message);
|
|
||||||
Logger.Debug("Detailed exception:", e);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
while (results.Read());
|
|
||||||
results.Close();
|
|
||||||
|
|
||||||
return questions;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static bool SaveInterview(ulong channelID, Interviewer.InterviewQuestion interview)
|
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
@ -883,7 +860,7 @@ public static class Database
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static bool TryGetInterview(ulong channelID, out Interviewer.InterviewQuestion interview)
|
public static bool TryGetInterview(ulong channelID, out Interviews.InterviewQuestion interview)
|
||||||
{
|
{
|
||||||
using MySqlConnection c = GetConnection();
|
using MySqlConnection c = GetConnection();
|
||||||
c.Open();
|
c.Open();
|
||||||
|
@ -898,7 +875,7 @@ public static class Database
|
||||||
interview = null;
|
interview = null;
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
interview = JsonConvert.DeserializeObject<Interviewer.InterviewQuestion>(results.GetString("interview"));
|
interview = JsonConvert.DeserializeObject<Interviews.InterviewQuestion>(results.GetString("interview"));
|
||||||
results.Close();
|
results.Close();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,6 +12,7 @@ using DSharpPlus.Entities;
|
||||||
using DSharpPlus.EventArgs;
|
using DSharpPlus.EventArgs;
|
||||||
using DSharpPlus.Exceptions;
|
using DSharpPlus.Exceptions;
|
||||||
using SupportChild.Commands;
|
using SupportChild.Commands;
|
||||||
|
using SupportChild.Interviews;
|
||||||
|
|
||||||
namespace SupportChild;
|
namespace SupportChild;
|
||||||
|
|
||||||
|
|
204
Interviews/Interview.cs
Normal file
204
Interviews/Interview.cs
Normal file
|
@ -0,0 +1,204 @@
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Collections.Specialized;
|
||||||
|
using System.Linq;
|
||||||
|
using DSharpPlus.Entities;
|
||||||
|
using Newtonsoft.Json;
|
||||||
|
using Newtonsoft.Json.Converters;
|
||||||
|
|
||||||
|
namespace SupportChild.Interviews;
|
||||||
|
|
||||||
|
public enum QuestionType
|
||||||
|
{
|
||||||
|
// Support multiselector as separate type, with only one subpath supported
|
||||||
|
ERROR,
|
||||||
|
END_WITH_SUMMARY,
|
||||||
|
END_WITHOUT_SUMMARY,
|
||||||
|
BUTTONS,
|
||||||
|
TEXT_SELECTOR,
|
||||||
|
USER_SELECTOR,
|
||||||
|
ROLE_SELECTOR,
|
||||||
|
MENTIONABLE_SELECTOR, // User or role
|
||||||
|
CHANNEL_SELECTOR,
|
||||||
|
TEXT_INPUT
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum ButtonType
|
||||||
|
{
|
||||||
|
PRIMARY,
|
||||||
|
SECONDARY,
|
||||||
|
SUCCESS,
|
||||||
|
DANGER
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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
|
||||||
|
{
|
||||||
|
// Title of the message embed.
|
||||||
|
[JsonProperty("title")]
|
||||||
|
public string title;
|
||||||
|
|
||||||
|
// 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;
|
||||||
|
|
||||||
|
// Used as label for this question in the post-interview summary.
|
||||||
|
[JsonProperty("summary-field")]
|
||||||
|
public string summaryField;
|
||||||
|
|
||||||
|
// If this question 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.
|
||||||
|
[JsonProperty("selector-placeholder")]
|
||||||
|
public string selectorPlaceholder;
|
||||||
|
|
||||||
|
// If this question is on a selector, give it this description.
|
||||||
|
[JsonProperty("selector-description")]
|
||||||
|
public string selectorDescription;
|
||||||
|
|
||||||
|
// The maximum length of a text input.
|
||||||
|
[JsonProperty("max-length")]
|
||||||
|
public int? maxLength;
|
||||||
|
|
||||||
|
// The minimum length of a text input.
|
||||||
|
[JsonProperty("min-length", Required = Required.Default)]
|
||||||
|
public int? minLength;
|
||||||
|
|
||||||
|
// Possible questions to ask next, an error message, or the end of the interview.
|
||||||
|
[JsonProperty("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;
|
||||||
|
|
||||||
|
// Any extra messages generated by the bot that should be removed when the interview ends.
|
||||||
|
[JsonProperty("related-message-ids")]
|
||||||
|
public List<ulong> relatedMessageIDs;
|
||||||
|
|
||||||
|
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 (messageID == 0)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (relatedMessageIDs != null)
|
||||||
|
{
|
||||||
|
messageIDs.AddRange(relatedMessageIDs);
|
||||||
|
}
|
||||||
|
|
||||||
|
// This will always contain exactly one or zero children.
|
||||||
|
foreach (KeyValuePair<string, InterviewQuestion> path in paths)
|
||||||
|
{
|
||||||
|
path.Value.GetMessageIDs(ref messageIDs);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void AddRelatedMessageIDs(params ulong[] messageIDs)
|
||||||
|
{
|
||||||
|
if (relatedMessageIDs == null)
|
||||||
|
{
|
||||||
|
relatedMessageIDs = messageIDs.ToList();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
relatedMessageIDs.AddRange(messageIDs);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public DiscordButtonStyle GetButtonStyle()
|
||||||
|
{
|
||||||
|
return buttonStyle switch
|
||||||
|
{
|
||||||
|
ButtonType.PRIMARY => DiscordButtonStyle.Primary,
|
||||||
|
ButtonType.SECONDARY => DiscordButtonStyle.Secondary,
|
||||||
|
ButtonType.SUCCESS => DiscordButtonStyle.Success,
|
||||||
|
ButtonType.DANGER => DiscordButtonStyle.Danger,
|
||||||
|
_ => DiscordButtonStyle.Secondary
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class Template(ulong categoryID, InterviewQuestion interview)
|
||||||
|
{
|
||||||
|
[JsonProperty("category-id", Required = Required.Always)]
|
||||||
|
public ulong categoryID = categoryID;
|
||||||
|
|
||||||
|
[JsonProperty("interview", Required = Required.Always)]
|
||||||
|
public InterviewQuestion interview = interview;
|
||||||
|
}
|
|
@ -7,321 +7,11 @@ using System.Text.RegularExpressions;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using DSharpPlus.Commands.Processors.SlashCommands;
|
using DSharpPlus.Commands.Processors.SlashCommands;
|
||||||
using DSharpPlus.Entities;
|
using DSharpPlus.Entities;
|
||||||
using Newtonsoft.Json;
|
|
||||||
using Newtonsoft.Json.Converters;
|
|
||||||
|
|
||||||
namespace SupportChild;
|
namespace SupportChild.Interviews;
|
||||||
|
|
||||||
public static class Interviewer
|
public static class Interviewer
|
||||||
{
|
{
|
||||||
public enum QuestionType
|
|
||||||
{
|
|
||||||
// Support multiselector as separate type, with only one subpath supported
|
|
||||||
ERROR,
|
|
||||||
END_WITH_SUMMARY,
|
|
||||||
END_WITHOUT_SUMMARY,
|
|
||||||
BUTTONS,
|
|
||||||
TEXT_SELECTOR,
|
|
||||||
USER_SELECTOR,
|
|
||||||
ROLE_SELECTOR,
|
|
||||||
MENTIONABLE_SELECTOR, // User or role
|
|
||||||
CHANNEL_SELECTOR,
|
|
||||||
TEXT_INPUT
|
|
||||||
}
|
|
||||||
|
|
||||||
public enum ButtonType
|
|
||||||
{
|
|
||||||
// Secondary first to make it the default
|
|
||||||
SECONDARY,
|
|
||||||
PRIMARY,
|
|
||||||
SUCCESS,
|
|
||||||
DANGER
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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
|
|
||||||
{
|
|
||||||
// Title of the message embed.
|
|
||||||
[JsonProperty("title")]
|
|
||||||
public string title;
|
|
||||||
|
|
||||||
// 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;
|
|
||||||
|
|
||||||
// Used as label for this question in the post-interview summary.
|
|
||||||
[JsonProperty("summary-field")]
|
|
||||||
public string summaryField;
|
|
||||||
|
|
||||||
// If this question 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.
|
|
||||||
[JsonProperty("selector-placeholder")]
|
|
||||||
public string selectorPlaceholder;
|
|
||||||
|
|
||||||
// If this question is on a selector, give it this description.
|
|
||||||
[JsonProperty("selector-description")]
|
|
||||||
public string selectorDescription;
|
|
||||||
|
|
||||||
// The maximum length of a text input.
|
|
||||||
[JsonProperty("max-length")]
|
|
||||||
public int maxLength;
|
|
||||||
|
|
||||||
// The minimum length of a text input.
|
|
||||||
[JsonProperty("min-length", Required = Required.Default)]
|
|
||||||
public int minLength;
|
|
||||||
|
|
||||||
// Possible questions to ask next, an error message, or the end of the interview.
|
|
||||||
[JsonProperty("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;
|
|
||||||
|
|
||||||
// Any extra messages generated by the bot that should be removed when the interview ends.
|
|
||||||
[JsonProperty("related-message-ids")]
|
|
||||||
public List<ulong> relatedMessageIDs;
|
|
||||||
|
|
||||||
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 (messageID == 0)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (relatedMessageIDs != null)
|
|
||||||
{
|
|
||||||
messageIDs.AddRange(relatedMessageIDs);
|
|
||||||
}
|
|
||||||
|
|
||||||
// This will always contain exactly one or zero children.
|
|
||||||
foreach (KeyValuePair<string, InterviewQuestion> path in paths)
|
|
||||||
{
|
|
||||||
path.Value.GetMessageIDs(ref messageIDs);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void AddRelatedMessageIDs(params ulong[] messageIDs)
|
|
||||||
{
|
|
||||||
if (relatedMessageIDs == null)
|
|
||||||
{
|
|
||||||
relatedMessageIDs = messageIDs.ToList();
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
relatedMessageIDs.AddRange(messageIDs);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public DiscordButtonStyle GetButtonStyle()
|
|
||||||
{
|
|
||||||
return buttonStyle switch
|
|
||||||
{
|
|
||||||
ButtonType.PRIMARY => DiscordButtonStyle.Primary,
|
|
||||||
ButtonType.SECONDARY => DiscordButtonStyle.Secondary,
|
|
||||||
ButtonType.SUCCESS => DiscordButtonStyle.Success,
|
|
||||||
ButtonType.DANGER => DiscordButtonStyle.Danger,
|
|
||||||
_ => DiscordButtonStyle.Primary
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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
|
|
||||||
{
|
|
||||||
[JsonProperty("title", Required = Required.Default)]
|
|
||||||
public string title;
|
|
||||||
|
|
||||||
[JsonProperty("message", Required = Required.Always)]
|
|
||||||
public string message;
|
|
||||||
|
|
||||||
[JsonConverter(typeof(StringEnumConverter))]
|
|
||||||
[JsonProperty("type", Required = Required.Always)]
|
|
||||||
public QuestionType type;
|
|
||||||
|
|
||||||
[JsonProperty("color", Required = Required.Always)]
|
|
||||||
public string color;
|
|
||||||
|
|
||||||
[JsonProperty("summary-field", Required = Required.Default)]
|
|
||||||
public string summaryField;
|
|
||||||
|
|
||||||
[JsonConverter(typeof(StringEnumConverter))]
|
|
||||||
[JsonProperty("button-style", Required = Required.Default)]
|
|
||||||
public ButtonType buttonStyle;
|
|
||||||
|
|
||||||
[JsonProperty("selector-placeholder", Required = Required.Default)]
|
|
||||||
public string selectorPlaceholder;
|
|
||||||
|
|
||||||
[JsonProperty("selector-description", Required = Required.Default)]
|
|
||||||
public string selectorDescription;
|
|
||||||
|
|
||||||
[JsonProperty("max-length", Required = Required.Default)]
|
|
||||||
public int maxLength;
|
|
||||||
|
|
||||||
[JsonProperty("min-length", Required = Required.Default)]
|
|
||||||
public int minLength;
|
|
||||||
|
|
||||||
[JsonProperty("paths", Required = Required.Always)]
|
|
||||||
public Dictionary<string, ValidatedInterviewQuestion> paths;
|
|
||||||
|
|
||||||
public void Validate(ref List<string> errors, out int summaryCount, out int summaryMaxLength)
|
|
||||||
{
|
|
||||||
if (string.IsNullOrWhiteSpace(message))
|
|
||||||
{
|
|
||||||
errors.Add("Message cannot be empty.");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (type is QuestionType.ERROR or QuestionType.END_WITH_SUMMARY or QuestionType.END_WITHOUT_SUMMARY)
|
|
||||||
{
|
|
||||||
if (paths.Count > 0)
|
|
||||||
{
|
|
||||||
errors.Add("'" + type + "' questions cannot have child paths.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else if (paths.Count == 0)
|
|
||||||
{
|
|
||||||
errors.Add("'" + type + "' questions must have at least one child path.");
|
|
||||||
}
|
|
||||||
|
|
||||||
List<int> summaryCounts = new List<int>();
|
|
||||||
Dictionary<string, int> childMaxLengths = new Dictionary<string, int>();
|
|
||||||
foreach (KeyValuePair<string, ValidatedInterviewQuestion> path in paths)
|
|
||||||
{
|
|
||||||
path.Value.Validate(ref errors, out int summaries, out int maxLen);
|
|
||||||
summaryCounts.Add(summaries);
|
|
||||||
childMaxLengths.Add(path.Key, maxLen);
|
|
||||||
}
|
|
||||||
|
|
||||||
summaryCount = summaryCounts.Count == 0 ? 0 : summaryCounts.Max();
|
|
||||||
|
|
||||||
string childPathString = "";
|
|
||||||
int childMaxLength = 0;
|
|
||||||
if (childMaxLengths.Count != 0)
|
|
||||||
{
|
|
||||||
(childPathString, childMaxLength) = childMaxLengths.ToArray().MaxBy(x => x.Key.Length + x.Value);
|
|
||||||
}
|
|
||||||
|
|
||||||
summaryMaxLength = childMaxLength;
|
|
||||||
|
|
||||||
if (string.IsNullOrWhiteSpace(summaryField))
|
|
||||||
{
|
|
||||||
++summaryCount;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Only count summaries that end in a summary question.
|
|
||||||
if (type == QuestionType.END_WITH_SUMMARY)
|
|
||||||
{
|
|
||||||
summaryMaxLength = message?.Length ?? 0;
|
|
||||||
summaryMaxLength += title?.Length ?? 0;
|
|
||||||
}
|
|
||||||
// Only add to the total max length if the summary field is not empty. That way we know this branch ends in a summary.
|
|
||||||
else if (summaryMaxLength > 0 && !string.IsNullOrEmpty(summaryField))
|
|
||||||
{
|
|
||||||
summaryMaxLength += summaryField.Length;
|
|
||||||
switch (type)
|
|
||||||
{
|
|
||||||
case QuestionType.BUTTONS:
|
|
||||||
case QuestionType.TEXT_SELECTOR:
|
|
||||||
summaryMaxLength += childPathString.Length;
|
|
||||||
break;
|
|
||||||
case QuestionType.USER_SELECTOR:
|
|
||||||
case QuestionType.ROLE_SELECTOR:
|
|
||||||
case QuestionType.MENTIONABLE_SELECTOR:
|
|
||||||
case QuestionType.CHANNEL_SELECTOR:
|
|
||||||
// Approximate length of a mention
|
|
||||||
summaryMaxLength += 23;
|
|
||||||
break;
|
|
||||||
case QuestionType.TEXT_INPUT:
|
|
||||||
summaryMaxLength += (maxLength == 0 ? 1024 : Math.Min(maxLength, 1024));
|
|
||||||
break;
|
|
||||||
case QuestionType.END_WITH_SUMMARY:
|
|
||||||
case QuestionType.END_WITHOUT_SUMMARY:
|
|
||||||
case QuestionType.ERROR:
|
|
||||||
default:
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static async void StartInterview(DiscordChannel channel)
|
public static async void StartInterview(DiscordChannel channel)
|
||||||
{
|
{
|
||||||
if (channel.Parent == null)
|
if (channel.Parent == null)
|
||||||
|
@ -329,22 +19,13 @@ public static class Interviewer
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!Database.TryGetInterviewTemplates(out Dictionary<ulong, InterviewQuestion> templates))
|
if (!Database.TryGetInterviewTemplate(channel.Parent.Id, out InterviewQuestion template))
|
||||||
{
|
{
|
||||||
await channel.SendMessageAsync(new DiscordEmbedBuilder
|
|
||||||
{
|
|
||||||
Description = "Attempted to create interview from template, but an error occured when reading it from the database.\n\n" +
|
|
||||||
"Tell a staff member to check the bot log and fix the template.",
|
|
||||||
Color = DiscordColor.Red
|
|
||||||
});
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (templates.TryGetValue(channel.Parent.Id, out InterviewQuestion interview))
|
await CreateQuestion(channel, template);
|
||||||
{
|
Database.SaveInterview(channel.Id, template);
|
||||||
await CreateQuestion(channel, interview);
|
|
||||||
Database.SaveInterview(channel.Id, interview);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static async Task RestartInterview(SlashCommandContext command)
|
public static async Task RestartInterview(SlashCommandContext command)
|
||||||
|
@ -474,11 +155,13 @@ public static class Interviewer
|
||||||
}
|
}
|
||||||
|
|
||||||
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 question of type " + currentQuestion.type + " which has no valid next question. Their selection was:\n" + answer);
|
||||||
await interaction.CreateFollowupMessageAsync(new DiscordFollowupMessageBuilder().AddEmbed(new DiscordEmbedBuilder
|
DiscordMessage followupMessage = await interaction.CreateFollowupMessageAsync(new DiscordFollowupMessageBuilder().AddEmbed(new DiscordEmbedBuilder
|
||||||
{
|
{
|
||||||
Color = DiscordColor.Red,
|
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."
|
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());
|
}).AsEphemeral());
|
||||||
|
currentQuestion.AddRelatedMessageIDs(followupMessage.Id);
|
||||||
|
Database.SaveInterview(interaction.Channel.Id, interviewRoot);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
@ -532,7 +215,7 @@ public static class Interviewer
|
||||||
}
|
}
|
||||||
|
|
||||||
// The length requirement is less than 1024 characters, and must be less than the configurable limit if it is set.
|
// The length requirement is less than 1024 characters, and must be less than the configurable limit if it is set.
|
||||||
int maxLength = currentQuestion.maxLength == 0 ? 1024 : Math.Min(currentQuestion.maxLength, 1024);
|
int maxLength = Math.Min(currentQuestion.maxLength ?? 1024, 1024);
|
||||||
|
|
||||||
if (answerMessage.Content.Length > maxLength)
|
if (answerMessage.Content.Length > maxLength)
|
||||||
{
|
{
|
||||||
|
@ -542,10 +225,11 @@ public static class Interviewer
|
||||||
Color = DiscordColor.Red
|
Color = DiscordColor.Red
|
||||||
});
|
});
|
||||||
currentQuestion.AddRelatedMessageIDs(answerMessage.Id, lengthMessage.Id);
|
currentQuestion.AddRelatedMessageIDs(answerMessage.Id, lengthMessage.Id);
|
||||||
|
Database.SaveInterview(answerMessage.Channel.Id, interviewRoot);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (answerMessage.Content.Length < currentQuestion.minLength)
|
if (answerMessage.Content.Length < (currentQuestion.minLength ?? 0))
|
||||||
{
|
{
|
||||||
DiscordMessage lengthMessage = await answerMessage.RespondAsync(new DiscordEmbedBuilder
|
DiscordMessage lengthMessage = await answerMessage.RespondAsync(new DiscordEmbedBuilder
|
||||||
{
|
{
|
||||||
|
@ -553,13 +237,17 @@ public static class Interviewer
|
||||||
Color = DiscordColor.Red
|
Color = DiscordColor.Red
|
||||||
});
|
});
|
||||||
currentQuestion.AddRelatedMessageIDs(answerMessage.Id, lengthMessage.Id);
|
currentQuestion.AddRelatedMessageIDs(answerMessage.Id, lengthMessage.Id);
|
||||||
|
Database.SaveInterview(answerMessage.Channel.Id, interviewRoot);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
foreach ((string questionString, InterviewQuestion nextQuestion) in currentQuestion.paths)
|
foreach ((string questionString, InterviewQuestion nextQuestion) in currentQuestion.paths)
|
||||||
{
|
{
|
||||||
// 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(answerMessage.Content, nextQuestion, interviewRoot, currentQuestion, answerMessage.Channel, answerMessage);
|
await HandleAnswer(answerMessage.Content, nextQuestion, interviewRoot, currentQuestion, answerMessage.Channel, answerMessage);
|
||||||
return;
|
return;
|
||||||
|
@ -571,7 +259,8 @@ public static class Interviewer
|
||||||
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.",
|
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
|
Color = DiscordColor.Red
|
||||||
});
|
});
|
||||||
currentQuestion.AddRelatedMessageIDs(errorMessage.Id);
|
currentQuestion.AddRelatedMessageIDs(answerMessage.Id, errorMessage.Id);
|
||||||
|
Database.SaveInterview(answerMessage.Channel.Id, interviewRoot);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static async Task HandleAnswer(string answer,
|
private static async Task HandleAnswer(string answer,
|
137
Interviews/ValidatedInterview.cs
Normal file
137
Interviews/ValidatedInterview.cs
Normal file
|
@ -0,0 +1,137 @@
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using Newtonsoft.Json;
|
||||||
|
using Newtonsoft.Json.Converters;
|
||||||
|
using SupportChild.Interviews;
|
||||||
|
|
||||||
|
namespace SupportChild.Interviews;
|
||||||
|
|
||||||
|
// This class is identical to the normal interview question 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
|
||||||
|
{
|
||||||
|
[JsonProperty("title", Required = Required.Default)]
|
||||||
|
public string title;
|
||||||
|
|
||||||
|
[JsonProperty("message", Required = Required.Always)]
|
||||||
|
public string message;
|
||||||
|
|
||||||
|
[JsonConverter(typeof(StringEnumConverter))]
|
||||||
|
[JsonProperty("type", Required = Required.Always)]
|
||||||
|
public QuestionType type;
|
||||||
|
|
||||||
|
[JsonProperty("color", Required = Required.Always)]
|
||||||
|
public string color;
|
||||||
|
|
||||||
|
[JsonProperty("summary-field", Required = Required.Default)]
|
||||||
|
public string summaryField;
|
||||||
|
|
||||||
|
[JsonConverter(typeof(StringEnumConverter))]
|
||||||
|
[JsonProperty("button-style", Required = Required.Default)]
|
||||||
|
public ButtonType? buttonStyle;
|
||||||
|
|
||||||
|
[JsonProperty("selector-placeholder", Required = Required.Default)]
|
||||||
|
public string selectorPlaceholder;
|
||||||
|
|
||||||
|
[JsonProperty("selector-description", Required = Required.Default)]
|
||||||
|
public string selectorDescription;
|
||||||
|
|
||||||
|
[JsonProperty("max-length", Required = Required.Default)]
|
||||||
|
public int? maxLength;
|
||||||
|
|
||||||
|
[JsonProperty("min-length", Required = Required.Default)]
|
||||||
|
public int? minLength;
|
||||||
|
|
||||||
|
[JsonProperty("paths", Required = Required.Always)]
|
||||||
|
public Dictionary<string, ValidatedInterviewQuestion> paths;
|
||||||
|
|
||||||
|
public void Validate(ref List<string> errors, out int summaryCount, out int summaryMaxLength)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(message))
|
||||||
|
{
|
||||||
|
errors.Add("Message cannot be empty.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (type is QuestionType.ERROR or QuestionType.END_WITH_SUMMARY or QuestionType.END_WITHOUT_SUMMARY)
|
||||||
|
{
|
||||||
|
if (paths.Count > 0)
|
||||||
|
{
|
||||||
|
errors.Add("'" + type + "' questions cannot have child paths.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (paths.Count == 0)
|
||||||
|
{
|
||||||
|
errors.Add("'" + type + "' questions must have at least one child path.");
|
||||||
|
}
|
||||||
|
|
||||||
|
List<int> summaryCounts = [];
|
||||||
|
Dictionary<string, int> childMaxLengths = new Dictionary<string, int>();
|
||||||
|
foreach (KeyValuePair<string, ValidatedInterviewQuestion> path in paths)
|
||||||
|
{
|
||||||
|
path.Value.Validate(ref errors, out int summaries, out int maxLen);
|
||||||
|
summaryCounts.Add(summaries);
|
||||||
|
childMaxLengths.Add(path.Key, maxLen);
|
||||||
|
}
|
||||||
|
|
||||||
|
summaryCount = summaryCounts.Count == 0 ? 0 : summaryCounts.Max();
|
||||||
|
|
||||||
|
string childPathString = "";
|
||||||
|
int childMaxLength = 0;
|
||||||
|
if (childMaxLengths.Count != 0)
|
||||||
|
{
|
||||||
|
(childPathString, childMaxLength) = childMaxLengths.ToArray().MaxBy(x => x.Key.Length + x.Value);
|
||||||
|
}
|
||||||
|
|
||||||
|
summaryMaxLength = childMaxLength;
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(summaryField))
|
||||||
|
{
|
||||||
|
++summaryCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only count summaries that end in a summary question.
|
||||||
|
if (type == QuestionType.END_WITH_SUMMARY)
|
||||||
|
{
|
||||||
|
summaryMaxLength = message?.Length ?? 0;
|
||||||
|
summaryMaxLength += title?.Length ?? 0;
|
||||||
|
}
|
||||||
|
// Only add to the total max length if the summary field is not empty. That way we know this branch ends in a summary.
|
||||||
|
else if (summaryMaxLength > 0 && !string.IsNullOrEmpty(summaryField))
|
||||||
|
{
|
||||||
|
summaryMaxLength += summaryField.Length;
|
||||||
|
switch (type)
|
||||||
|
{
|
||||||
|
case QuestionType.BUTTONS:
|
||||||
|
case QuestionType.TEXT_SELECTOR:
|
||||||
|
summaryMaxLength += childPathString.Length;
|
||||||
|
break;
|
||||||
|
case QuestionType.USER_SELECTOR:
|
||||||
|
case QuestionType.ROLE_SELECTOR:
|
||||||
|
case QuestionType.MENTIONABLE_SELECTOR:
|
||||||
|
case QuestionType.CHANNEL_SELECTOR:
|
||||||
|
// Approximate length of a mention
|
||||||
|
summaryMaxLength += 23;
|
||||||
|
break;
|
||||||
|
case QuestionType.TEXT_INPUT:
|
||||||
|
summaryMaxLength += Math.Min(maxLength ?? 1024, 1024);
|
||||||
|
break;
|
||||||
|
case QuestionType.END_WITH_SUMMARY:
|
||||||
|
case QuestionType.END_WITHOUT_SUMMARY:
|
||||||
|
case QuestionType.ERROR:
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class ValidatedTemplate(ulong categoryID, ValidatedInterviewQuestion interview)
|
||||||
|
{
|
||||||
|
[JsonProperty("category-id", Required = Required.Always)]
|
||||||
|
public ulong categoryID = categoryID;
|
||||||
|
|
||||||
|
[JsonProperty("interview", Required = Required.Always)]
|
||||||
|
public ValidatedInterviewQuestion interview = interview;
|
||||||
|
}
|
Loading…
Reference in a new issue