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