Added step references, ability to jump to other steps depending on user actions
This commit is contained in:
parent
da5cca1f78
commit
61b57d0afc
6 changed files with 315 additions and 106 deletions
|
@ -62,7 +62,7 @@ public class InterviewCommands
|
|||
return;
|
||||
}
|
||||
|
||||
if (!Database.TryGetInterview(command.Channel.Id, out InterviewStep interviewRoot))
|
||||
if (!Database.TryGetInterview(command.Channel.Id, out Interview _))
|
||||
{
|
||||
await command.RespondAsync(new DiscordEmbedBuilder
|
||||
{
|
||||
|
|
|
@ -102,17 +102,34 @@ public class InterviewTemplateCommands
|
|||
JsonSerializer serializer = new();
|
||||
Template template = serializer.Deserialize<Template>(validatingReader);
|
||||
|
||||
DiscordChannel category = await SupportChild.client.GetChannelAsync(template.categoryID);
|
||||
DiscordChannel category;
|
||||
try
|
||||
{
|
||||
category = await SupportChild.client.GetChannelAsync(template.categoryID);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
await command.RespondAsync(new DiscordEmbedBuilder
|
||||
{
|
||||
Color = DiscordColor.Red,
|
||||
Description = "Could not get the category from the ID in the uploaded JSON structure."
|
||||
}, true);
|
||||
Logger.Warn("Failed to get template category from ID: " + template.categoryID, e);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!category.IsCategory)
|
||||
{
|
||||
await command.RespondAsync(new DiscordEmbedBuilder
|
||||
{
|
||||
Color = DiscordColor.Red,
|
||||
Description = "The category ID in the uploaded JSON structure is not a valid category."
|
||||
Description = "The channel ID in the uploaded JSON structure is not a category."
|
||||
}, true);
|
||||
return;
|
||||
}
|
||||
|
||||
//TODO: Validate that any references with reference end steps have an after reference step
|
||||
|
||||
List<string> errors = [];
|
||||
List<string> warnings = [];
|
||||
template.interview.Validate(ref errors, ref warnings, "interview", 0, 0);
|
||||
|
@ -208,7 +225,7 @@ public class InterviewTemplateCommands
|
|||
return;
|
||||
}
|
||||
|
||||
if (!Database.TryGetInterviewTemplate(category.Id, out InterviewStep _))
|
||||
if (!Database.TryGetInterviewFromTemplate(category.Id, 0, out Interview _))
|
||||
{
|
||||
await command.RespondAsync(new DiscordEmbedBuilder
|
||||
{
|
||||
|
|
39
Database.cs
39
Database.cs
|
@ -113,7 +113,8 @@ public static class Database
|
|||
using MySqlCommand createInterviews = new MySqlCommand(
|
||||
"CREATE TABLE IF NOT EXISTS interviews(" +
|
||||
"channel_id BIGINT UNSIGNED NOT NULL PRIMARY KEY," +
|
||||
"interview JSON NOT NULL)",
|
||||
"interview JSON NOT NULL," +
|
||||
"definitions JSON NOT NULL)",
|
||||
c);
|
||||
using MySqlCommand createInterviewTemplates = new MySqlCommand(
|
||||
"CREATE TABLE IF NOT EXISTS interview_templates(" +
|
||||
|
@ -753,7 +754,7 @@ public static class Database
|
|||
return templates;
|
||||
}
|
||||
|
||||
public static bool TryGetInterviewTemplate(ulong categoryID, out Interviews.InterviewStep template)
|
||||
public static bool TryGetInterviewFromTemplate(ulong categoryID, ulong channelID, out Interviews.Interview interview)
|
||||
{
|
||||
using MySqlConnection c = GetConnection();
|
||||
c.Open();
|
||||
|
@ -765,7 +766,7 @@ public static class Database
|
|||
// Check if messages exist in the database
|
||||
if (!results.Read())
|
||||
{
|
||||
template = null;
|
||||
interview = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
|
@ -774,7 +775,7 @@ public static class Database
|
|||
|
||||
try
|
||||
{
|
||||
template = JsonConvert.DeserializeObject<Interviews.Template>(templateString, new JsonSerializerSettings
|
||||
Template template = JsonConvert.DeserializeObject<Template>(templateString, new JsonSerializerSettings
|
||||
{
|
||||
Error = delegate (object sender, ErrorEventArgs args)
|
||||
{
|
||||
|
@ -782,21 +783,22 @@ public static class Database
|
|||
Logger.Debug("Detailed exception:", args.ErrorContext.Error);
|
||||
args.ErrorContext.Handled = false;
|
||||
}
|
||||
}).interview;
|
||||
});
|
||||
interview = new Interview(channelID, template.interview, template.definitions);
|
||||
return true;
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
template = null;
|
||||
interview = null;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public static bool SetInterviewTemplate(Interviews.Template template)
|
||||
public static bool SetInterviewTemplate(Template template)
|
||||
{
|
||||
try
|
||||
{
|
||||
string templateString = JsonConvert.SerializeObject(template, new JsonSerializerSettings()
|
||||
string templateString = JsonConvert.SerializeObject(template, new JsonSerializerSettings
|
||||
{
|
||||
NullValueHandling = NullValueHandling.Ignore,
|
||||
MissingMemberHandling = MissingMemberHandling.Error,
|
||||
|
@ -805,7 +807,7 @@ public static class Database
|
|||
});
|
||||
|
||||
string query;
|
||||
if (TryGetInterviewTemplate(template.categoryID, out _))
|
||||
if (TryGetInterviewFromTemplate(template.categoryID, 0, out _))
|
||||
{
|
||||
query = "UPDATE interview_templates SET template = @template WHERE category_id=@category_id";
|
||||
}
|
||||
|
@ -845,25 +847,26 @@ public static class Database
|
|||
}
|
||||
}
|
||||
|
||||
public static bool SaveInterview(ulong channelID, Interviews.InterviewStep interview)
|
||||
public static bool SaveInterview(Interview interview)
|
||||
{
|
||||
try
|
||||
{
|
||||
string query;
|
||||
if (TryGetInterview(channelID, out _))
|
||||
if (TryGetInterview(interview.channelID, out _))
|
||||
{
|
||||
query = "UPDATE interviews SET interview = @interview WHERE channel_id = @channel_id";
|
||||
query = "UPDATE interviews SET interview = @interview, definitions = @definitions WHERE channel_id = @channel_id";
|
||||
}
|
||||
else
|
||||
{
|
||||
query = "INSERT INTO interviews (channel_id,interview) VALUES (@channel_id, @interview)";
|
||||
query = "INSERT INTO interviews (channel_id,interview, definitions) VALUES (@channel_id, @interview, @definitions)";
|
||||
}
|
||||
|
||||
using MySqlConnection c = GetConnection();
|
||||
c.Open();
|
||||
using MySqlCommand cmd = new MySqlCommand(query, c);
|
||||
cmd.Parameters.AddWithValue("@channel_id", channelID);
|
||||
cmd.Parameters.AddWithValue("@interview", JsonConvert.SerializeObject(interview, Formatting.Indented));
|
||||
cmd.Parameters.AddWithValue("@channel_id", interview.channelID);
|
||||
cmd.Parameters.AddWithValue("@interview", JsonConvert.SerializeObject(interview.interviewRoot, Formatting.Indented));
|
||||
cmd.Parameters.AddWithValue("@definitions", JsonConvert.SerializeObject(interview.definitions, Formatting.Indented));
|
||||
cmd.Prepare();
|
||||
return cmd.ExecuteNonQuery() > 0;
|
||||
}
|
||||
|
@ -873,7 +876,7 @@ public static class Database
|
|||
}
|
||||
}
|
||||
|
||||
public static bool TryGetInterview(ulong channelID, out Interviews.InterviewStep interview)
|
||||
public static bool TryGetInterview(ulong channelID, out Interview interview)
|
||||
{
|
||||
using MySqlConnection c = GetConnection();
|
||||
c.Open();
|
||||
|
@ -888,7 +891,9 @@ public static class Database
|
|||
interview = null;
|
||||
return false;
|
||||
}
|
||||
interview = JsonConvert.DeserializeObject<Interviews.InterviewStep>(results.GetString("interview"));
|
||||
interview = new Interview(channelID,
|
||||
JsonConvert.DeserializeObject<InterviewStep>(results.GetString("interview")),
|
||||
JsonConvert.DeserializeObject<Dictionary<string, InterviewStep>>(results.GetString("definitions")));
|
||||
results.Close();
|
||||
return true;
|
||||
}
|
||||
|
|
|
@ -3,6 +3,7 @@ using System.Collections.Generic;
|
|||
using System.Collections.Specialized;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Text.RegularExpressions;
|
||||
using DSharpPlus.Entities;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Converters;
|
||||
|
@ -22,7 +23,8 @@ public enum MessageType
|
|||
ROLE_SELECTOR,
|
||||
MENTIONABLE_SELECTOR, // User or role
|
||||
CHANNEL_SELECTOR,
|
||||
TEXT_INPUT
|
||||
TEXT_INPUT,
|
||||
REFERENCE_END
|
||||
}
|
||||
|
||||
public enum ButtonType
|
||||
|
@ -33,6 +35,45 @@ public enum ButtonType
|
|||
DANGER
|
||||
}
|
||||
|
||||
public class ReferencedInterviewStep
|
||||
{
|
||||
[JsonProperty("id")]
|
||||
public string id;
|
||||
|
||||
// If this step is on a button, give it this style.
|
||||
[JsonConverter(typeof(StringEnumConverter))]
|
||||
[JsonProperty("button-style")]
|
||||
public ButtonType? buttonStyle;
|
||||
|
||||
// If this step is in a selector, give it this description.
|
||||
[JsonProperty("selector-description")]
|
||||
public string selectorDescription;
|
||||
|
||||
// Runs at the end of the reference
|
||||
[JsonProperty("after-reference-step")]
|
||||
public InterviewStep afterReferenceStep;
|
||||
|
||||
public DiscordButtonStyle GetButtonStyle()
|
||||
{
|
||||
return InterviewStep.GetButtonStyle(buttonStyle);
|
||||
}
|
||||
|
||||
public bool TryGetReferencedStep(Interview interview, out InterviewStep step)
|
||||
{
|
||||
if (!interview.definitions.TryGetValue(id, out step))
|
||||
{
|
||||
Logger.Error("Could not find referenced step '" + id + "' in interview for channel '" + interview.channelID + "'");
|
||||
return false;
|
||||
}
|
||||
|
||||
step.buttonStyle = buttonStyle;
|
||||
step.selectorDescription = selectorDescription;
|
||||
step.afterReferenceStep = afterReferenceStep;
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// 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.
|
||||
// Additional components not specified in the config file are populated as the interview progresses.
|
||||
|
@ -54,7 +95,7 @@ public class InterviewStep
|
|||
|
||||
// Colour of the message embed.
|
||||
[JsonProperty("color")]
|
||||
public string color;
|
||||
public string color = "CYAN";
|
||||
|
||||
// Used as label for this answer in the post-interview summary.
|
||||
[JsonProperty("summary-field")]
|
||||
|
@ -65,11 +106,11 @@ public class InterviewStep
|
|||
[JsonProperty("button-style")]
|
||||
public ButtonType? buttonStyle;
|
||||
|
||||
// If this step is on a selector, give it this placeholder.
|
||||
// If this step is a selector, give it this placeholder.
|
||||
[JsonProperty("selector-placeholder")]
|
||||
public string selectorPlaceholder;
|
||||
|
||||
// If this step is on a selector, give it this description.
|
||||
// If this step is in a selector, give it this description.
|
||||
[JsonProperty("selector-description")]
|
||||
public string selectorDescription;
|
||||
|
||||
|
@ -81,6 +122,10 @@ public class InterviewStep
|
|||
[JsonProperty("min-length")]
|
||||
public int? minLength;
|
||||
|
||||
// References to steps defined elsewhere in the template
|
||||
[JsonProperty("step-references")]
|
||||
public Dictionary<string, ReferencedInterviewStep> references = new();
|
||||
|
||||
// Possible questions to ask next, an error message, or the end of the interview.
|
||||
[JsonProperty("steps")]
|
||||
public Dictionary<string, InterviewStep> steps = new();
|
||||
|
@ -105,12 +150,23 @@ public class InterviewStep
|
|||
[JsonProperty("related-message-ids")]
|
||||
public List<ulong> relatedMessageIDs;
|
||||
|
||||
public bool TryGetCurrentStep(out InterviewStep step)
|
||||
// This is only set when the user gets to a referenced step
|
||||
[JsonProperty("after-reference-step")]
|
||||
public InterviewStep afterReferenceStep = null;
|
||||
|
||||
public bool TryGetCurrentStep(out InterviewStep currentStep)
|
||||
{
|
||||
bool result = TryGetTakenSteps(out List<InterviewStep> previousSteps);
|
||||
currentStep = previousSteps.FirstOrDefault();
|
||||
return result;
|
||||
}
|
||||
|
||||
public bool TryGetTakenSteps(out List<InterviewStep> previousSteps)
|
||||
{
|
||||
// This object has not been initialized, we have checked too deep.
|
||||
if (messageID == 0)
|
||||
{
|
||||
step = null;
|
||||
previousSteps = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
|
@ -118,14 +174,15 @@ public class InterviewStep
|
|||
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.
|
||||
if (childStep.Value.TryGetCurrentStep(out step))
|
||||
if (childStep.Value.TryGetTakenSteps(out previousSteps))
|
||||
{
|
||||
previousSteps.Add(this);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// This object is the deepest object with a message ID set, meaning it is the latest asked question.
|
||||
step = this;
|
||||
previousSteps = new List<InterviewStep> { this };
|
||||
return true;
|
||||
}
|
||||
|
||||
|
@ -138,7 +195,8 @@ public class InterviewStep
|
|||
|
||||
if (!string.IsNullOrWhiteSpace(summaryField))
|
||||
{
|
||||
summary.Add(summaryField, answer);
|
||||
// TODO: Add option to merge answers
|
||||
summary[summaryField] = answer;
|
||||
}
|
||||
|
||||
// This will always contain exactly one or zero children.
|
||||
|
@ -184,18 +242,6 @@ public class InterviewStep
|
|||
}
|
||||
}
|
||||
|
||||
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 void Validate(ref List<string> errors,
|
||||
ref List<string> warnings,
|
||||
string stepID,
|
||||
|
@ -237,7 +283,7 @@ public class InterviewStep
|
|||
|
||||
if (messageType is MessageType.ERROR or MessageType.END_WITH_SUMMARY or MessageType.END_WITHOUT_SUMMARY)
|
||||
{
|
||||
if (steps.Count > 0)
|
||||
if (steps.Count > 0 || references.Count > 0)
|
||||
{
|
||||
warnings.Add("Steps of the type '" + messageType + "' cannot have child steps.\n\n" + stepID + ".message-type");
|
||||
}
|
||||
|
@ -247,7 +293,7 @@ public class InterviewStep
|
|||
warnings.Add("Steps of the type '" + messageType + "' cannot have summary field names.\n\n" + stepID + ".summary-field");
|
||||
}
|
||||
}
|
||||
else if (steps.Count == 0)
|
||||
else if (steps.Count == 0 && references.Count == 0)
|
||||
{
|
||||
errors.Add("Steps of the type '" + messageType + "' must have at least one child step.\n\n" + stepID + ".message-type");
|
||||
}
|
||||
|
@ -304,6 +350,23 @@ public class InterviewStep
|
|||
}
|
||||
}
|
||||
|
||||
public DiscordButtonStyle GetButtonStyle()
|
||||
{
|
||||
return GetButtonStyle(buttonStyle);
|
||||
}
|
||||
|
||||
public static DiscordButtonStyle GetButtonStyle(ButtonType? buttonStyle)
|
||||
{
|
||||
return buttonStyle switch
|
||||
{
|
||||
ButtonType.PRIMARY => DiscordButtonStyle.Primary,
|
||||
ButtonType.SECONDARY => DiscordButtonStyle.Secondary,
|
||||
ButtonType.SUCCESS => DiscordButtonStyle.Success,
|
||||
ButtonType.DANGER => DiscordButtonStyle.Danger,
|
||||
_ => DiscordButtonStyle.Secondary
|
||||
};
|
||||
}
|
||||
|
||||
public class StripInternalPropertiesResolver : DefaultContractResolver
|
||||
{
|
||||
private static readonly HashSet<string> ignoreProps =
|
||||
|
@ -326,11 +389,21 @@ public class InterviewStep
|
|||
}
|
||||
}
|
||||
|
||||
public class Template(ulong categoryID, InterviewStep interview)
|
||||
public class Interview(ulong channelID, InterviewStep interviewRoot, Dictionary<string, InterviewStep> definitions)
|
||||
{
|
||||
public ulong channelID = channelID;
|
||||
public InterviewStep interviewRoot = interviewRoot;
|
||||
public Dictionary<string, InterviewStep> definitions = definitions;
|
||||
}
|
||||
|
||||
public class Template(ulong categoryID, InterviewStep interview, Dictionary<string, InterviewStep> definitions)
|
||||
{
|
||||
[JsonProperty("category-id", Required = Required.Always)]
|
||||
public ulong categoryID = categoryID;
|
||||
|
||||
[JsonProperty("interview", Required = Required.Always)]
|
||||
public InterviewStep interview = interview;
|
||||
|
||||
[JsonProperty("definitions", Required = Required.Default)]
|
||||
public Dictionary<string, InterviewStep> definitions = definitions;
|
||||
}
|
|
@ -14,22 +14,22 @@ public static class Interviewer
|
|||
{
|
||||
public static async Task<bool> StartInterview(DiscordChannel channel)
|
||||
{
|
||||
if (!Database.TryGetInterviewTemplate(channel.Parent.Id, out InterviewStep template))
|
||||
if (!Database.TryGetInterviewFromTemplate(channel.Parent.Id, channel.Id, out Interview interview))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
await SendNextMessage(channel, template);
|
||||
return Database.SaveInterview(channel.Id, template);
|
||||
await SendNextMessage(channel, interview.interviewRoot);
|
||||
return Database.SaveInterview(interview);
|
||||
}
|
||||
|
||||
public static async Task<bool> RestartInterview(DiscordChannel channel)
|
||||
{
|
||||
if (Database.TryGetInterview(channel.Id, out InterviewStep interviewRoot))
|
||||
if (Database.TryGetInterview(channel.Id, out Interview interview))
|
||||
{
|
||||
if (Config.deleteMessagesAfterNoSummary)
|
||||
{
|
||||
await DeletePreviousMessages(interviewRoot, channel);
|
||||
await DeletePreviousMessages(interview, channel);
|
||||
}
|
||||
|
||||
if (!Database.TryDeleteInterview(channel.Id))
|
||||
|
@ -43,11 +43,11 @@ public static class Interviewer
|
|||
|
||||
public static async Task<bool> StopInterview(DiscordChannel channel)
|
||||
{
|
||||
if (Database.TryGetInterview(channel.Id, out InterviewStep interviewRoot))
|
||||
if (Database.TryGetInterview(channel.Id, out Interview interview))
|
||||
{
|
||||
if (Config.deleteMessagesAfterNoSummary)
|
||||
{
|
||||
await DeletePreviousMessages(interviewRoot, channel);
|
||||
await DeletePreviousMessages(interview, channel);
|
||||
}
|
||||
|
||||
if (!Database.TryDeleteInterview(channel.Id))
|
||||
|
@ -61,7 +61,7 @@ public static class Interviewer
|
|||
|
||||
public static async Task ProcessButtonOrSelectorResponse(DiscordInteraction interaction)
|
||||
{
|
||||
if (interaction?.Channel == null || interaction?.Message == null)
|
||||
if (interaction?.Channel == null || interaction.Message == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
@ -74,7 +74,7 @@ public static class Interviewer
|
|||
}
|
||||
|
||||
// Return if there is no active interview in this channel
|
||||
if (!Database.TryGetInterview(interaction.Channel.Id, out InterviewStep interviewRoot))
|
||||
if (!Database.TryGetInterview(interaction.Channel.Id, out Interview interview))
|
||||
{
|
||||
await interaction.CreateResponseAsync(DiscordInteractionResponseType.ChannelMessageWithSource, new DiscordInteractionResponseBuilder()
|
||||
.AddEmbed(new DiscordEmbedBuilder()
|
||||
|
@ -85,7 +85,7 @@ public static class Interviewer
|
|||
}
|
||||
|
||||
// Return if the current question cannot be found in the interview.
|
||||
if (!interviewRoot.TryGetCurrentStep(out InterviewStep currentStep))
|
||||
if (!interview.interviewRoot.TryGetCurrentStep(out InterviewStep currentStep))
|
||||
{
|
||||
await interaction.CreateResponseAsync(DiscordInteractionResponseType.ChannelMessageWithSource, new DiscordInteractionResponseBuilder()
|
||||
.AddEmbed(new DiscordEmbedBuilder()
|
||||
|
@ -126,19 +126,19 @@ public static class Interviewer
|
|||
case DiscordComponentType.RoleSelect:
|
||||
case DiscordComponentType.ChannelSelect:
|
||||
case DiscordComponentType.MentionableSelect:
|
||||
if (interaction.Data.Resolved?.Roles?.Any() ?? false)
|
||||
if (interaction.Data.Resolved.Roles.Any())
|
||||
{
|
||||
answer = interaction.Data.Resolved.Roles.First().Value.Mention;
|
||||
}
|
||||
else if (interaction.Data.Resolved?.Users?.Any() ?? false)
|
||||
else if (interaction.Data.Resolved.Users.Any())
|
||||
{
|
||||
answer = interaction.Data.Resolved.Users.First().Value.Mention;
|
||||
}
|
||||
else if (interaction.Data.Resolved?.Channels?.Any() ?? false)
|
||||
else if (interaction.Data.Resolved.Channels.Any())
|
||||
{
|
||||
answer = interaction.Data.Resolved.Channels.First().Value.Mention;
|
||||
}
|
||||
else if (interaction.Data.Resolved?.Messages?.Any() ?? false)
|
||||
else if (interaction.Data.Resolved.Messages.Any())
|
||||
{
|
||||
answer = interaction.Data.Resolved.Messages.First().Value.Id.ToString();
|
||||
}
|
||||
|
@ -147,7 +147,7 @@ public static class Interviewer
|
|||
componentID = interaction.Data.Values[0];
|
||||
break;
|
||||
case DiscordComponentType.Button:
|
||||
componentID = interaction.Data.CustomId.Replace("supportchild_interviewbutton ", "");
|
||||
componentID = interaction.Data.CustomId.Replace("supportboi_interviewbutton ", "");
|
||||
break;
|
||||
case DiscordComponentType.ActionRow:
|
||||
case DiscordComponentType.FormInput:
|
||||
|
@ -158,12 +158,27 @@ public static class Interviewer
|
|||
// The different mentionable selectors provide the actual answer, while the others just return the ID.
|
||||
if (componentID == "")
|
||||
{
|
||||
foreach (KeyValuePair<string, ReferencedInterviewStep> reference in currentStep.references)
|
||||
{
|
||||
// Skip to the first matching step.
|
||||
if (Regex.IsMatch(answer, reference.Key))
|
||||
{
|
||||
if (TryGetStepFromReference(interview, reference.Value, out InterviewStep referencedStep))
|
||||
{
|
||||
currentStep.steps.Add(reference.Key, referencedStep);
|
||||
await HandleAnswer(answer, referencedStep, interview, currentStep, interaction.Channel);
|
||||
}
|
||||
currentStep.references.Remove(reference.Key);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
foreach (KeyValuePair<string, InterviewStep> step in currentStep.steps)
|
||||
{
|
||||
// Skip to the first matching step.
|
||||
if (Regex.IsMatch(answer, step.Key))
|
||||
{
|
||||
await HandleAnswer(answer, step.Value, interviewRoot, currentStep, interaction.Channel);
|
||||
await HandleAnswer(answer, step.Value, interview, currentStep, interaction.Channel);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
@ -175,7 +190,7 @@ 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."
|
||||
}).AsEphemeral());
|
||||
currentStep.AddRelatedMessageIDs(followupMessage.Id);
|
||||
Database.SaveInterview(interaction.Channel.Id, interviewRoot);
|
||||
Database.SaveInterview(interview);
|
||||
}
|
||||
else
|
||||
{
|
||||
|
@ -192,10 +207,31 @@ public static class Interviewer
|
|||
}
|
||||
|
||||
(string stepString, InterviewStep nextStep) = currentStep.steps.ElementAt(stepIndex);
|
||||
await HandleAnswer(stepString, nextStep, interviewRoot, currentStep, interaction.Channel);
|
||||
await HandleAnswer(stepString, nextStep, interview, currentStep, interaction.Channel);
|
||||
}
|
||||
}
|
||||
|
||||
public static bool TryGetStepFromReference(Interview interview, ReferencedInterviewStep reference, out InterviewStep step)
|
||||
{
|
||||
foreach (KeyValuePair<string, InterviewStep> definition in interview.definitions)
|
||||
{
|
||||
if (reference.id == definition.Key)
|
||||
{
|
||||
step = definition.Value;
|
||||
step.buttonStyle = reference.buttonStyle;
|
||||
step.selectorDescription = reference.selectorDescription;
|
||||
if (step.messageType != MessageType.ERROR)
|
||||
{
|
||||
step.afterReferenceStep = reference.afterReferenceStep;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
step = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
public static async Task ProcessResponseMessage(DiscordMessage answerMessage)
|
||||
{
|
||||
// Either the message or the referenced message is null.
|
||||
|
@ -205,12 +241,13 @@ public static class Interviewer
|
|||
}
|
||||
|
||||
// The channel does not have an active interview.
|
||||
if (!Database.TryGetInterview(answerMessage.ReferencedMessage.Channel.Id, out InterviewStep interviewRoot))
|
||||
if (!Database.TryGetInterview(answerMessage.ReferencedMessage.Channel.Id,
|
||||
out Interview interview))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (!interviewRoot.TryGetCurrentStep(out InterviewStep currentStep))
|
||||
if (!interview.interviewRoot.TryGetCurrentStep(out InterviewStep currentStep))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
@ -238,7 +275,7 @@ public static class Interviewer
|
|||
Color = DiscordColor.Red
|
||||
});
|
||||
currentStep.AddRelatedMessageIDs(answerMessage.Id, lengthMessage.Id);
|
||||
Database.SaveInterview(answerMessage.Channel.Id, interviewRoot);
|
||||
Database.SaveInterview(interview);
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -250,7 +287,7 @@ public static class Interviewer
|
|||
Color = DiscordColor.Red
|
||||
});
|
||||
currentStep.AddRelatedMessageIDs(answerMessage.Id, lengthMessage.Id);
|
||||
Database.SaveInterview(answerMessage.Channel.Id, interviewRoot);
|
||||
Database.SaveInterview(interview);
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -262,7 +299,7 @@ public static class Interviewer
|
|||
continue;
|
||||
}
|
||||
|
||||
await HandleAnswer(answerMessage.Content, nextStep, interviewRoot, currentStep, answerMessage.Channel, answerMessage);
|
||||
await HandleAnswer(answerMessage.Content, nextStep, interview, currentStep, answerMessage.Channel, answerMessage);
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -273,12 +310,12 @@ public static class Interviewer
|
|||
Color = DiscordColor.Red
|
||||
});
|
||||
currentStep.AddRelatedMessageIDs(answerMessage.Id, errorMessage.Id);
|
||||
Database.SaveInterview(answerMessage.Channel.Id, interviewRoot);
|
||||
Database.SaveInterview(interview);
|
||||
}
|
||||
|
||||
private static async Task HandleAnswer(string answer,
|
||||
InterviewStep nextStep,
|
||||
InterviewStep interviewRoot,
|
||||
Interview interview,
|
||||
InterviewStep previousStep,
|
||||
DiscordChannel channel,
|
||||
DiscordMessage answerMessage = null)
|
||||
|
@ -302,14 +339,37 @@ public static class Interviewer
|
|||
case MessageType.USER_SELECTOR:
|
||||
case MessageType.CHANNEL_SELECTOR:
|
||||
case MessageType.MENTIONABLE_SELECTOR:
|
||||
foreach ((string stepPattern, ReferencedInterviewStep reference) in nextStep.references)
|
||||
{
|
||||
if (!reference.TryGetReferencedStep(interview, out InterviewStep step))
|
||||
{
|
||||
if (answerMessage != null)
|
||||
{
|
||||
DiscordMessage lengthMessage = await answerMessage.RespondAsync(new DiscordEmbedBuilder
|
||||
{
|
||||
Description = "Error: The referenced step id '" + reference.id + "' does not exist in the step definitions.",
|
||||
Color = DiscordColor.Red
|
||||
});
|
||||
nextStep.AddRelatedMessageIDs(answerMessage.Id, lengthMessage.Id);
|
||||
previousStep.answer = null;
|
||||
previousStep.answerID = 0;
|
||||
Database.SaveInterview(interview);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
nextStep.steps.Add(stepPattern, step);
|
||||
}
|
||||
nextStep.references.Clear();
|
||||
|
||||
await SendNextMessage(channel, nextStep);
|
||||
Database.SaveInterview(channel.Id, interviewRoot);
|
||||
Database.SaveInterview(interview);
|
||||
break;
|
||||
case MessageType.END_WITH_SUMMARY:
|
||||
OrderedDictionary summaryFields = new OrderedDictionary();
|
||||
interviewRoot.GetSummary(ref summaryFields);
|
||||
interview.interviewRoot.GetSummary(ref summaryFields);
|
||||
|
||||
DiscordEmbedBuilder embed = new DiscordEmbedBuilder()
|
||||
DiscordEmbedBuilder embed = new DiscordEmbedBuilder
|
||||
{
|
||||
Color = Utilities.StringToColor(nextStep.color),
|
||||
Title = nextStep.heading,
|
||||
|
@ -318,14 +378,14 @@ public static class Interviewer
|
|||
|
||||
foreach (DictionaryEntry entry in summaryFields)
|
||||
{
|
||||
embed.AddField((string)entry.Key, (string)entry.Value);
|
||||
embed.AddField((string)entry.Key, (string)entry.Value ?? string.Empty);
|
||||
}
|
||||
|
||||
await channel.SendMessageAsync(embed);
|
||||
|
||||
if (Config.deleteMessagesAfterSummary)
|
||||
{
|
||||
await DeletePreviousMessages(interviewRoot, channel);
|
||||
await DeletePreviousMessages(interview, channel);
|
||||
}
|
||||
|
||||
if (!Database.TryDeleteInterview(channel.Id))
|
||||
|
@ -334,7 +394,7 @@ public static class Interviewer
|
|||
}
|
||||
return;
|
||||
case MessageType.END_WITHOUT_SUMMARY:
|
||||
await channel.SendMessageAsync(new DiscordEmbedBuilder()
|
||||
await channel.SendMessageAsync(new DiscordEmbedBuilder
|
||||
{
|
||||
Color = Utilities.StringToColor(nextStep.color),
|
||||
Title = nextStep.heading,
|
||||
|
@ -343,7 +403,7 @@ public static class Interviewer
|
|||
|
||||
if (Config.deleteMessagesAfterNoSummary)
|
||||
{
|
||||
await DeletePreviousMessages(interviewRoot, channel);
|
||||
await DeletePreviousMessages(interview, channel);
|
||||
}
|
||||
|
||||
if (!Database.TryDeleteInterview(channel.Id))
|
||||
|
@ -351,40 +411,88 @@ public static class Interviewer
|
|||
Logger.Error("Could not delete interview from database. Channel ID: " + channel.Id);
|
||||
}
|
||||
break;
|
||||
case MessageType.ERROR:
|
||||
default:
|
||||
case MessageType.REFERENCE_END:
|
||||
// TODO: What is happening with the summaries?
|
||||
if (interview.interviewRoot.TryGetTakenSteps(out List<InterviewStep> previousSteps))
|
||||
{
|
||||
foreach (InterviewStep step in previousSteps)
|
||||
{
|
||||
if (step.afterReferenceStep != null)
|
||||
{
|
||||
// If the referenced step is also a reference end, skip it and try to find another.
|
||||
if (step.afterReferenceStep.messageType == MessageType.REFERENCE_END)
|
||||
{
|
||||
step.afterReferenceStep = null;
|
||||
}
|
||||
else
|
||||
{
|
||||
nextStep = step.afterReferenceStep;
|
||||
step.afterReferenceStep = null;
|
||||
|
||||
previousStep.steps.Clear();
|
||||
previousStep.steps.Add(answer, nextStep);
|
||||
await HandleAnswer(answer,
|
||||
nextStep,
|
||||
interview,
|
||||
previousStep,
|
||||
channel,
|
||||
answerMessage);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
DiscordEmbedBuilder error = new DiscordEmbedBuilder
|
||||
{
|
||||
Color = DiscordColor.Red,
|
||||
Description = "An error occured while trying to find the next interview step."
|
||||
};
|
||||
|
||||
if (answerMessage == null)
|
||||
{
|
||||
DiscordMessage errorMessage = await channel.SendMessageAsync(new DiscordEmbedBuilder()
|
||||
{
|
||||
Color = Utilities.StringToColor(nextStep.color),
|
||||
Title = nextStep.heading,
|
||||
Description = nextStep.message
|
||||
});
|
||||
DiscordMessage errorMessage = await channel.SendMessageAsync(error);
|
||||
previousStep.AddRelatedMessageIDs(errorMessage.Id);
|
||||
}
|
||||
else
|
||||
{
|
||||
DiscordMessageBuilder errorMessageBuilder = new DiscordMessageBuilder()
|
||||
.AddEmbed(new DiscordEmbedBuilder()
|
||||
{
|
||||
Color = Utilities.StringToColor(nextStep.color),
|
||||
Title = nextStep.heading,
|
||||
Description = nextStep.message
|
||||
}).WithReply(answerMessage.Id);
|
||||
DiscordMessage errorMessage = await answerMessage.RespondAsync(errorMessageBuilder);
|
||||
DiscordMessage errorMessage = await answerMessage.RespondAsync(error);
|
||||
previousStep.AddRelatedMessageIDs(errorMessage.Id, answerMessage.Id);
|
||||
}
|
||||
|
||||
Database.SaveInterview(channel.Id, interviewRoot);
|
||||
Database.SaveInterview(interview);
|
||||
|
||||
Logger.Error("Could not find a step to return to after a reference step in channel " + channel.Id);
|
||||
return;
|
||||
case MessageType.ERROR:
|
||||
default:
|
||||
DiscordEmbedBuilder err = new DiscordEmbedBuilder
|
||||
{
|
||||
Color = Utilities.StringToColor(nextStep.color),
|
||||
Title = nextStep.heading,
|
||||
Description = nextStep.message
|
||||
};
|
||||
|
||||
if (answerMessage == null)
|
||||
{
|
||||
DiscordMessage errorMessage = await channel.SendMessageAsync(err);
|
||||
previousStep.AddRelatedMessageIDs(errorMessage.Id);
|
||||
}
|
||||
else
|
||||
{
|
||||
DiscordMessage errorMessage = await answerMessage.RespondAsync(err);
|
||||
previousStep.AddRelatedMessageIDs(errorMessage.Id, answerMessage.Id);
|
||||
}
|
||||
|
||||
Database.SaveInterview(interview);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private static async Task DeletePreviousMessages(InterviewStep interviewRoot, DiscordChannel channel)
|
||||
private static async Task DeletePreviousMessages(Interview interview, DiscordChannel channel)
|
||||
{
|
||||
List<ulong> previousMessages = [];
|
||||
interviewRoot.GetMessageIDs(ref previousMessages);
|
||||
interview.interviewRoot.GetMessageIDs(ref previousMessages);
|
||||
|
||||
foreach (ulong previousMessageID in previousMessages)
|
||||
{
|
||||
|
@ -420,7 +528,7 @@ public static class Interviewer
|
|||
for (; nrOfButtons < 5 * (nrOfButtonRows + 1) && nrOfButtons < step.steps.Count; nrOfButtons++)
|
||||
{
|
||||
(string stepPattern, InterviewStep nextStep) = step.steps.ToArray()[nrOfButtons];
|
||||
buttonRow.Add(new DiscordButtonComponent(nextStep.GetButtonStyle(), "supportchild_interviewbutton " + nrOfButtons, stepPattern));
|
||||
buttonRow.Add(new DiscordButtonComponent(nextStep.GetButtonStyle(), "supportboi_interviewbutton " + nrOfButtons, stepPattern));
|
||||
}
|
||||
msgBuilder.AddComponents(buttonRow);
|
||||
}
|
||||
|
@ -438,26 +546,26 @@ public static class Interviewer
|
|||
categoryOptions.Add(new DiscordSelectComponentOption(stepPattern, selectionOptions.ToString(), nextStep.selectorDescription));
|
||||
}
|
||||
|
||||
selectionComponents.Add(new DiscordSelectComponent("supportchild_interviewselector " + selectionBoxes, string.IsNullOrWhiteSpace(step.selectorPlaceholder)
|
||||
selectionComponents.Add(new DiscordSelectComponent("supportboi_interviewselector " + selectionBoxes, string.IsNullOrWhiteSpace(step.selectorPlaceholder)
|
||||
? "Select an option..." : step.selectorPlaceholder, categoryOptions));
|
||||
}
|
||||
|
||||
msgBuilder.AddComponents(selectionComponents);
|
||||
break;
|
||||
case MessageType.ROLE_SELECTOR:
|
||||
msgBuilder.AddComponents(new DiscordRoleSelectComponent("supportchild_interviewroleselector", string.IsNullOrWhiteSpace(step.selectorPlaceholder)
|
||||
msgBuilder.AddComponents(new DiscordRoleSelectComponent("supportboi_interviewroleselector", string.IsNullOrWhiteSpace(step.selectorPlaceholder)
|
||||
? "Select a role..." : step.selectorPlaceholder));
|
||||
break;
|
||||
case MessageType.USER_SELECTOR:
|
||||
msgBuilder.AddComponents(new DiscordUserSelectComponent("supportchild_interviewuserselector", string.IsNullOrWhiteSpace(step.selectorPlaceholder)
|
||||
msgBuilder.AddComponents(new DiscordUserSelectComponent("supportboi_interviewuserselector", string.IsNullOrWhiteSpace(step.selectorPlaceholder)
|
||||
? "Select a user..." : step.selectorPlaceholder));
|
||||
break;
|
||||
case MessageType.CHANNEL_SELECTOR:
|
||||
msgBuilder.AddComponents(new DiscordChannelSelectComponent("supportchild_interviewchannelselector", string.IsNullOrWhiteSpace(step.selectorPlaceholder)
|
||||
msgBuilder.AddComponents(new DiscordChannelSelectComponent("supportboi_interviewchannelselector", string.IsNullOrWhiteSpace(step.selectorPlaceholder)
|
||||
? "Select a channel..." : step.selectorPlaceholder));
|
||||
break;
|
||||
case MessageType.MENTIONABLE_SELECTOR:
|
||||
msgBuilder.AddComponents(new DiscordMentionableSelectComponent("supportchild_interviewmentionableselector", string.IsNullOrWhiteSpace(step.selectorPlaceholder)
|
||||
msgBuilder.AddComponents(new DiscordMentionableSelectComponent("supportboi_interviewmentionableselector", string.IsNullOrWhiteSpace(step.selectorPlaceholder)
|
||||
? "Select a user or role..." : step.selectorPlaceholder));
|
||||
break;
|
||||
case MessageType.TEXT_INPUT:
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
"definitions": {
|
||||
"reference": {
|
||||
"type": "object",
|
||||
"title": "Interview Step",
|
||||
"title": "Interview Step Reference",
|
||||
"description": "Contains a reference to a step defined in the 'definitions' property. You can also specify a button-style and selector-description here to override the one from the referenced step.",
|
||||
"properties": {
|
||||
"id": {
|
||||
|
@ -31,6 +31,12 @@
|
|||
"title": "Selector Description",
|
||||
"description": "Description for this option in the parent step's selection box. Requires that the parent step is a 'TEXT_SELECTOR'.",
|
||||
"minLength": 1
|
||||
},
|
||||
"after-reference-step": {
|
||||
"type": "object",
|
||||
"title": "Steps",
|
||||
"description": "Any REFERENCE_END steps in the reference will continue to this step.",
|
||||
"$ref": "#/definitions/step"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
|
@ -69,7 +75,8 @@
|
|||
"ROLE_SELECTOR",
|
||||
"MENTIONABLE_SELECTOR",
|
||||
"CHANNEL_SELECTOR",
|
||||
"TEXT_INPUT"
|
||||
"TEXT_INPUT",
|
||||
"REFERENCE_END"
|
||||
]
|
||||
},
|
||||
"color": {
|
||||
|
@ -186,8 +193,7 @@
|
|||
},
|
||||
"required": [
|
||||
"message",
|
||||
"message-type",
|
||||
"color"
|
||||
"message-type"
|
||||
],
|
||||
"unevaluatedProperties": false
|
||||
}
|
||||
|
|
Loading…
Add table
Reference in a new issue