Added step references, ability to jump to other steps depending on user actions

This commit is contained in:
Toastie 2025-02-04 16:32:21 +13:00
parent da5cca1f78
commit 61b57d0afc
Signed by: toastie_t0ast
GPG key ID: 0861BE54AD481DC7
6 changed files with 315 additions and 106 deletions

View file

@ -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
{

View file

@ -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
{

View file

@ -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;
}

View file

@ -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;
}

View file

@ -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:

View file

@ -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
}