773 lines
No EOL
32 KiB
C#
773 lines
No EOL
32 KiB
C#
using System;
|
|
using System.Collections;
|
|
using System.Collections.Generic;
|
|
using System.Collections.Specialized;
|
|
using System.Linq;
|
|
using System.Text.RegularExpressions;
|
|
using System.Threading.Tasks;
|
|
using DSharpPlus.Commands.Processors.SlashCommands;
|
|
using DSharpPlus.Entities;
|
|
using Newtonsoft.Json;
|
|
using Newtonsoft.Json.Converters;
|
|
|
|
namespace SupportChild;
|
|
|
|
public static class Interviewer
|
|
{
|
|
public enum QuestionType
|
|
{
|
|
// Support multiselector as separate type, with only one subpath supported
|
|
ERROR,
|
|
END_WITH_SUMMARY,
|
|
END_WITHOUT_SUMMARY,
|
|
BUTTONS,
|
|
TEXT_SELECTOR,
|
|
USER_SELECTOR,
|
|
ROLE_SELECTOR,
|
|
MENTIONABLE_SELECTOR, // User or role
|
|
CHANNEL_SELECTOR,
|
|
TEXT_INPUT
|
|
}
|
|
|
|
public enum ButtonType
|
|
{
|
|
// Secondary first to make it the default
|
|
SECONDARY,
|
|
PRIMARY,
|
|
SUCCESS,
|
|
DANGER
|
|
}
|
|
|
|
// A tree of questions representing an interview.
|
|
// The tree is generated by the config file when a new ticket is opened or the restart interview command is used.
|
|
// Additional components not specified in the config file are populated as the interview progresses.
|
|
// The entire interview tree is serialized and stored in the database in order to record responses as they are made.
|
|
public class InterviewQuestion
|
|
{
|
|
// Title of the message embed.
|
|
[JsonProperty("title")]
|
|
public string title;
|
|
|
|
// Message contents sent to the user.
|
|
[JsonProperty("message")]
|
|
public string message;
|
|
|
|
// The type of question.
|
|
[JsonConverter(typeof(StringEnumConverter))]
|
|
[JsonProperty("type")]
|
|
public QuestionType type;
|
|
|
|
// Colour of the message embed.
|
|
[JsonProperty("color")]
|
|
public string color;
|
|
|
|
// Used as label for this question in the post-interview summary.
|
|
[JsonProperty("summary-field")]
|
|
public string summaryField;
|
|
|
|
// If this question is on a button, give it this style.
|
|
[JsonConverter(typeof(StringEnumConverter))]
|
|
[JsonProperty("button-style")]
|
|
public ButtonType buttonStyle;
|
|
|
|
// If this question is on a selector, give it this placeholder.
|
|
[JsonProperty("selector-placeholder")]
|
|
public string selectorPlaceholder;
|
|
|
|
// If this question is on a selector, give it this description.
|
|
[JsonProperty("selector-description")]
|
|
public string selectorDescription;
|
|
|
|
// The maximum length of a text input.
|
|
[JsonProperty("max-length")]
|
|
public int maxLength;
|
|
|
|
// The minimum length of a text input.
|
|
[JsonProperty("min-length", Required = Required.Default)]
|
|
public int minLength;
|
|
|
|
// Possible questions to ask next, an error message, or the end of the interview.
|
|
[JsonProperty("paths")]
|
|
public Dictionary<string, InterviewQuestion> paths;
|
|
|
|
// ////////////////////////////////////////////////////////////////////////////
|
|
// The following parameters are populated by the bot, not the json template. //
|
|
// ////////////////////////////////////////////////////////////////////////////
|
|
|
|
// The ID of this message where the bot asked this question.
|
|
[JsonProperty("message-id")]
|
|
public ulong messageID;
|
|
|
|
// The contents of the user's answer.
|
|
[JsonProperty("answer")]
|
|
public string answer;
|
|
|
|
// The ID of the user's answer message if this is a TEXT_INPUT type.
|
|
[JsonProperty("answer-id")]
|
|
public ulong answerID;
|
|
|
|
// Any extra messages generated by the bot that should be removed when the interview ends.
|
|
[JsonProperty("related-message-ids")]
|
|
public List<ulong> relatedMessageIDs;
|
|
|
|
public bool TryGetCurrentQuestion(out InterviewQuestion question)
|
|
{
|
|
// This object has not been initialized, we have checked too deep.
|
|
if (messageID == 0)
|
|
{
|
|
question = null;
|
|
return false;
|
|
}
|
|
|
|
// Check children.
|
|
foreach (KeyValuePair<string, InterviewQuestion> path in paths)
|
|
{
|
|
// This child either is the one we are looking for or contains the one we are looking for.
|
|
if (path.Value.TryGetCurrentQuestion(out question))
|
|
{
|
|
return true;
|
|
}
|
|
}
|
|
|
|
// This object is the deepest object with a message ID set, meaning it is the latest asked question.
|
|
question = this;
|
|
return true;
|
|
}
|
|
|
|
public void GetSummary(ref OrderedDictionary summary)
|
|
{
|
|
if (messageID == 0)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (!string.IsNullOrWhiteSpace(summaryField))
|
|
{
|
|
summary.Add(summaryField, answer);
|
|
}
|
|
|
|
// This will always contain exactly one or zero children.
|
|
foreach (KeyValuePair<string, InterviewQuestion> path in paths)
|
|
{
|
|
path.Value.GetSummary(ref summary);
|
|
}
|
|
}
|
|
|
|
public void GetMessageIDs(ref List<ulong> messageIDs)
|
|
{
|
|
if (messageID != 0)
|
|
{
|
|
messageIDs.Add(messageID);
|
|
}
|
|
|
|
if (answerID != 0)
|
|
{
|
|
messageIDs.Add(answerID);
|
|
}
|
|
|
|
if (relatedMessageIDs != null)
|
|
{
|
|
messageIDs.AddRange(relatedMessageIDs);
|
|
}
|
|
|
|
// This will always contain exactly one or zero children.
|
|
foreach (KeyValuePair<string, InterviewQuestion> path in paths)
|
|
{
|
|
path.Value.GetMessageIDs(ref messageIDs);
|
|
}
|
|
}
|
|
|
|
public void AddRelatedMessageIDs(params ulong[] messageIDs)
|
|
{
|
|
if (relatedMessageIDs == null)
|
|
{
|
|
relatedMessageIDs = messageIDs.ToList();
|
|
}
|
|
else
|
|
{
|
|
relatedMessageIDs.AddRange(messageIDs);
|
|
}
|
|
}
|
|
|
|
public DiscordButtonStyle GetButtonStyle()
|
|
{
|
|
return buttonStyle switch
|
|
{
|
|
ButtonType.PRIMARY => DiscordButtonStyle.Primary,
|
|
ButtonType.SECONDARY => DiscordButtonStyle.Secondary,
|
|
ButtonType.SUCCESS => DiscordButtonStyle.Success,
|
|
ButtonType.DANGER => DiscordButtonStyle.Danger,
|
|
_ => DiscordButtonStyle.Primary
|
|
};
|
|
}
|
|
}
|
|
|
|
// This class is identical to the one above and just exists as a hack to get JSON validation when
|
|
// new entries are entered but not when read from database in order to be more lenient with old interviews.
|
|
// I might do this in a more proper way at some point.
|
|
public class ValidatedInterviewQuestion
|
|
{
|
|
[JsonProperty("title", Required = Required.Default)]
|
|
public string title;
|
|
|
|
[JsonProperty("message", Required = Required.Always)]
|
|
public string message;
|
|
|
|
[JsonConverter(typeof(StringEnumConverter))]
|
|
[JsonProperty("type", Required = Required.Always)]
|
|
public QuestionType type;
|
|
|
|
[JsonProperty("color", Required = Required.Always)]
|
|
public string color;
|
|
|
|
[JsonProperty("summary-field", Required = Required.Default)]
|
|
public string summaryField;
|
|
|
|
[JsonConverter(typeof(StringEnumConverter))]
|
|
[JsonProperty("button-style", Required = Required.Default)]
|
|
public ButtonType buttonStyle;
|
|
|
|
[JsonProperty("selector-placeholder", Required = Required.Default)]
|
|
public string selectorPlaceholder;
|
|
|
|
[JsonProperty("selector-description", Required = Required.Default)]
|
|
public string selectorDescription;
|
|
|
|
[JsonProperty("max-length", Required = Required.Default)]
|
|
public int maxLength;
|
|
|
|
[JsonProperty("min-length", Required = Required.Default)]
|
|
public int minLength;
|
|
|
|
[JsonProperty("paths", Required = Required.Always)]
|
|
public Dictionary<string, ValidatedInterviewQuestion> paths;
|
|
|
|
public void Validate(ref List<string> errors, out int summaryCount, out int summaryMaxLength)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(message))
|
|
{
|
|
errors.Add("Message cannot be empty.");
|
|
}
|
|
|
|
if (type is QuestionType.ERROR or QuestionType.END_WITH_SUMMARY or QuestionType.END_WITHOUT_SUMMARY)
|
|
{
|
|
if (paths.Count > 0)
|
|
{
|
|
errors.Add("'" + type + "' questions cannot have child paths.");
|
|
}
|
|
}
|
|
else if (paths.Count == 0)
|
|
{
|
|
errors.Add("'" + type + "' questions must have at least one child path.");
|
|
}
|
|
|
|
List<int> summaryCounts = new List<int>();
|
|
Dictionary<string, int> childMaxLengths = new Dictionary<string, int>();
|
|
foreach (KeyValuePair<string, ValidatedInterviewQuestion> path in paths)
|
|
{
|
|
path.Value.Validate(ref errors, out int summaries, out int maxLen);
|
|
summaryCounts.Add(summaries);
|
|
childMaxLengths.Add(path.Key, maxLen);
|
|
}
|
|
|
|
summaryCount = summaryCounts.Count == 0 ? 0 : summaryCounts.Max();
|
|
|
|
string childPathString = "";
|
|
int childMaxLength = 0;
|
|
if (childMaxLengths.Count != 0)
|
|
{
|
|
(childPathString, childMaxLength) = childMaxLengths.ToArray().MaxBy(x => x.Key.Length + x.Value);
|
|
}
|
|
|
|
summaryMaxLength = childMaxLength;
|
|
|
|
if (string.IsNullOrWhiteSpace(summaryField))
|
|
{
|
|
++summaryCount;
|
|
}
|
|
|
|
// Only count summaries that end in a summary question.
|
|
if (type == QuestionType.END_WITH_SUMMARY)
|
|
{
|
|
summaryMaxLength = message?.Length ?? 0;
|
|
summaryMaxLength += title?.Length ?? 0;
|
|
}
|
|
// Only add to the total max length if the summary field is not empty. That way we know this branch ends in a summary.
|
|
else if (summaryMaxLength > 0 && !string.IsNullOrEmpty(summaryField))
|
|
{
|
|
summaryMaxLength += summaryField.Length;
|
|
switch (type)
|
|
{
|
|
case QuestionType.BUTTONS:
|
|
case QuestionType.TEXT_SELECTOR:
|
|
summaryMaxLength += childPathString.Length;
|
|
break;
|
|
case QuestionType.USER_SELECTOR:
|
|
case QuestionType.ROLE_SELECTOR:
|
|
case QuestionType.MENTIONABLE_SELECTOR:
|
|
case QuestionType.CHANNEL_SELECTOR:
|
|
// Approximate length of a mention
|
|
summaryMaxLength += 23;
|
|
break;
|
|
case QuestionType.TEXT_INPUT:
|
|
summaryMaxLength += (maxLength == 0 ? 1024 : Math.Min(maxLength, 1024));
|
|
break;
|
|
case QuestionType.END_WITH_SUMMARY:
|
|
case QuestionType.END_WITHOUT_SUMMARY:
|
|
case QuestionType.ERROR:
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
public static async void StartInterview(DiscordChannel channel)
|
|
{
|
|
if (channel.Parent == null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (!Database.TryGetInterviewTemplates(out Dictionary<ulong, InterviewQuestion> templates))
|
|
{
|
|
await channel.SendMessageAsync(new DiscordEmbedBuilder
|
|
{
|
|
Description = "Attempted to create interview from template, but an error occured when reading it from the database.\n\n" +
|
|
"Tell a staff member to check the bot log and fix the template.",
|
|
Color = DiscordColor.Red
|
|
});
|
|
return;
|
|
}
|
|
|
|
if (templates.TryGetValue(channel.Parent.Id, out InterviewQuestion interview))
|
|
{
|
|
await CreateQuestion(channel, interview);
|
|
Database.SaveInterview(channel.Id, interview);
|
|
}
|
|
}
|
|
|
|
public static async Task RestartInterview(SlashCommandContext command)
|
|
{
|
|
if (Database.TryGetInterview(command.Channel.Id, out InterviewQuestion interviewRoot))
|
|
{
|
|
if (Config.deleteMessagesAfterNoSummary)
|
|
{
|
|
await DeletePreviousMessages(interviewRoot, command.Channel);
|
|
}
|
|
|
|
if (!Database.TryDeleteInterview(command.Channel.Id))
|
|
{
|
|
Logger.Error("Could not delete interview from database. Channel ID: " + command.Channel.Id);
|
|
}
|
|
}
|
|
StartInterview(command.Channel);
|
|
}
|
|
|
|
public static async Task ProcessButtonOrSelectorResponse(DiscordInteraction interaction)
|
|
{
|
|
if (interaction?.Channel == null || interaction?.Message == null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
// Ignore if option was deselected.
|
|
if (interaction.Data.ComponentType is not DiscordComponentType.Button && interaction.Data.Values.Length == 0)
|
|
{
|
|
await interaction.CreateResponseAsync(DiscordInteractionResponseType.UpdateMessage);
|
|
return;
|
|
}
|
|
|
|
// Return if there is no active interview in this channel
|
|
if (!Database.TryGetInterview(interaction.Channel.Id, out InterviewQuestion interviewRoot))
|
|
{
|
|
await interaction.CreateResponseAsync(DiscordInteractionResponseType.ChannelMessageWithSource, new DiscordInteractionResponseBuilder()
|
|
.AddEmbed(new DiscordEmbedBuilder()
|
|
.WithColor(DiscordColor.Red)
|
|
.WithDescription("Error: There is no active interview in this ticket, ask an admin to check the bot logs if this seems incorrect."))
|
|
.AsEphemeral());
|
|
return;
|
|
}
|
|
|
|
// Return if the current question cannot be found in the interview.
|
|
if (!interviewRoot.TryGetCurrentQuestion(out InterviewQuestion currentQuestion))
|
|
{
|
|
await interaction.CreateResponseAsync(DiscordInteractionResponseType.ChannelMessageWithSource, new DiscordInteractionResponseBuilder()
|
|
.AddEmbed(new DiscordEmbedBuilder()
|
|
.WithColor(DiscordColor.Red)
|
|
.WithDescription("Error: Something seems to have broken in this interview, you may want to restart it."))
|
|
.AsEphemeral());
|
|
Logger.Error("The interview for channel " + interaction.Channel.Id + " exists but does not have a message ID set for it's root question");
|
|
return;
|
|
}
|
|
|
|
// Check if this button/selector is for an older question.
|
|
if (interaction.Message.Id != currentQuestion.messageID)
|
|
{
|
|
await interaction.CreateResponseAsync(DiscordInteractionResponseType.ChannelMessageWithSource, new DiscordInteractionResponseBuilder()
|
|
.AddEmbed(new DiscordEmbedBuilder()
|
|
.WithColor(DiscordColor.Red)
|
|
.WithDescription("Error: You have already replied to this question, you have to reply to the latest one."))
|
|
.AsEphemeral());
|
|
return;
|
|
}
|
|
|
|
try
|
|
{
|
|
await interaction.CreateResponseAsync(DiscordInteractionResponseType.UpdateMessage);
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
Logger.Error("Could not update original message:", e);
|
|
}
|
|
|
|
// Parse the response index from the button/selector.
|
|
string componentID = "";
|
|
string answer = "";
|
|
|
|
switch (interaction.Data.ComponentType)
|
|
{
|
|
case DiscordComponentType.UserSelect:
|
|
case DiscordComponentType.RoleSelect:
|
|
case DiscordComponentType.ChannelSelect:
|
|
case DiscordComponentType.MentionableSelect:
|
|
if (interaction.Data.Resolved?.Roles?.Any() ?? false)
|
|
{
|
|
answer = interaction.Data.Resolved.Roles.First().Value.Mention;
|
|
}
|
|
else if (interaction.Data.Resolved?.Users?.Any() ?? false)
|
|
{
|
|
answer = interaction.Data.Resolved.Users.First().Value.Mention;
|
|
}
|
|
else if (interaction.Data.Resolved?.Channels?.Any() ?? false)
|
|
{
|
|
answer = interaction.Data.Resolved.Channels.First().Value.Mention;
|
|
}
|
|
else if (interaction.Data.Resolved?.Messages?.Any() ?? false)
|
|
{
|
|
answer = interaction.Data.Resolved.Messages.First().Value.Id.ToString();
|
|
}
|
|
break;
|
|
case DiscordComponentType.StringSelect:
|
|
componentID = interaction.Data.Values[0];
|
|
break;
|
|
case DiscordComponentType.Button:
|
|
componentID = interaction.Data.CustomId.Replace("supportchild_interviewbutton ", "");
|
|
break;
|
|
case DiscordComponentType.ActionRow:
|
|
case DiscordComponentType.FormInput:
|
|
default:
|
|
throw new ArgumentOutOfRangeException("Tried to process an invalid component type: " + interaction.Data.ComponentType);
|
|
}
|
|
|
|
// The different mentionable selectors provide the actual answer, while the others just return the ID.
|
|
if (componentID == "")
|
|
{
|
|
foreach (KeyValuePair<string, InterviewQuestion> path in currentQuestion.paths)
|
|
{
|
|
// Skip to the first matching path.
|
|
if (Regex.IsMatch(answer, path.Key))
|
|
{
|
|
await HandleAnswer(answer, path.Value, interviewRoot, currentQuestion, interaction.Channel);
|
|
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);
|
|
await interaction.CreateFollowupMessageAsync(new DiscordFollowupMessageBuilder().AddEmbed(new DiscordEmbedBuilder
|
|
{
|
|
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."
|
|
}).AsEphemeral());
|
|
}
|
|
else
|
|
{
|
|
if (!int.TryParse(componentID, out int pathIndex))
|
|
{
|
|
Logger.Error("Invalid interview button/selector index: " + componentID);
|
|
return;
|
|
}
|
|
|
|
if (pathIndex >= currentQuestion.paths.Count || pathIndex < 0)
|
|
{
|
|
Logger.Error("Invalid interview button/selector index: " + pathIndex);
|
|
return;
|
|
}
|
|
|
|
(string questionString, InterviewQuestion nextQuestion) = currentQuestion.paths.ElementAt(pathIndex);
|
|
await HandleAnswer(questionString, nextQuestion, interviewRoot, currentQuestion, interaction.Channel);
|
|
}
|
|
|
|
}
|
|
|
|
public static async Task ProcessResponseMessage(DiscordMessage answerMessage)
|
|
{
|
|
// Either the message or the referenced message is null.
|
|
if (answerMessage.Channel == null || answerMessage.ReferencedMessage?.Channel == null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
// The channel does not have an active interview.
|
|
if (!Database.TryGetInterview(answerMessage.ReferencedMessage.Channel.Id, out InterviewQuestion interviewRoot))
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (!interviewRoot.TryGetCurrentQuestion(out InterviewQuestion currentQuestion))
|
|
{
|
|
return;
|
|
}
|
|
|
|
// The user responded to something other than the latest interview question.
|
|
if (answerMessage.ReferencedMessage.Id != currentQuestion.messageID)
|
|
{
|
|
return;
|
|
}
|
|
|
|
// The user responded to a question which does not take a text response.
|
|
if (currentQuestion.type != QuestionType.TEXT_INPUT)
|
|
{
|
|
return;
|
|
}
|
|
|
|
// The length requirement is less than 1024 characters, and must be less than the configurable limit if it is set.
|
|
int maxLength = currentQuestion.maxLength == 0 ? 1024 : Math.Min(currentQuestion.maxLength, 1024);
|
|
|
|
if (answerMessage.Content.Length > maxLength)
|
|
{
|
|
DiscordMessage lengthMessage = await answerMessage.RespondAsync(new DiscordEmbedBuilder
|
|
{
|
|
Description = "Error: Your answer cannot be more than " + maxLength + " characters (" + answerMessage.Content.Length + "/" + maxLength + ").",
|
|
Color = DiscordColor.Red
|
|
});
|
|
currentQuestion.AddRelatedMessageIDs(answerMessage.Id, lengthMessage.Id);
|
|
return;
|
|
}
|
|
|
|
if (answerMessage.Content.Length < currentQuestion.minLength)
|
|
{
|
|
DiscordMessage lengthMessage = await answerMessage.RespondAsync(new DiscordEmbedBuilder
|
|
{
|
|
Description = "Error: Your answer must be at least " + currentQuestion.minLength + " characters (" + answerMessage.Content.Length + "/" + currentQuestion.minLength + ").",
|
|
Color = DiscordColor.Red
|
|
});
|
|
currentQuestion.AddRelatedMessageIDs(answerMessage.Id, lengthMessage.Id);
|
|
return;
|
|
}
|
|
|
|
foreach ((string questionString, InterviewQuestion nextQuestion) in currentQuestion.paths)
|
|
{
|
|
// Skip to the first matching path.
|
|
if (!Regex.IsMatch(answerMessage.Content, questionString)) continue;
|
|
|
|
await HandleAnswer(answerMessage.Content, nextQuestion, interviewRoot, currentQuestion, answerMessage.Channel, answerMessage);
|
|
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);
|
|
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.",
|
|
Color = DiscordColor.Red
|
|
});
|
|
currentQuestion.AddRelatedMessageIDs(errorMessage.Id);
|
|
}
|
|
|
|
private static async Task HandleAnswer(string answer,
|
|
InterviewQuestion nextQuestion,
|
|
InterviewQuestion interviewRoot,
|
|
InterviewQuestion previousQuestion,
|
|
DiscordChannel channel,
|
|
DiscordMessage answerMessage = null)
|
|
{
|
|
// The error message type should not alter anything about the interview.
|
|
if (nextQuestion.type != QuestionType.ERROR)
|
|
{
|
|
previousQuestion.answer = answer;
|
|
|
|
// There is no message ID if the question is not a text input.
|
|
previousQuestion.answerID = answerMessage == null ? 0 : answerMessage.Id;
|
|
}
|
|
|
|
// Create next question, or finish the interview.
|
|
switch (nextQuestion.type)
|
|
{
|
|
case QuestionType.TEXT_INPUT:
|
|
case QuestionType.BUTTONS:
|
|
case QuestionType.TEXT_SELECTOR:
|
|
case QuestionType.ROLE_SELECTOR:
|
|
case QuestionType.USER_SELECTOR:
|
|
case QuestionType.CHANNEL_SELECTOR:
|
|
case QuestionType.MENTIONABLE_SELECTOR:
|
|
await CreateQuestion(channel, nextQuestion);
|
|
Database.SaveInterview(channel.Id, interviewRoot);
|
|
break;
|
|
case QuestionType.END_WITH_SUMMARY:
|
|
OrderedDictionary summaryFields = new OrderedDictionary();
|
|
interviewRoot.GetSummary(ref summaryFields);
|
|
|
|
DiscordEmbedBuilder embed = new DiscordEmbedBuilder()
|
|
{
|
|
Color = Utilities.StringToColor(nextQuestion.color),
|
|
Title = nextQuestion.title,
|
|
Description = nextQuestion.message,
|
|
};
|
|
|
|
foreach (DictionaryEntry entry in summaryFields)
|
|
{
|
|
embed.AddField((string)entry.Key, (string)entry.Value);
|
|
}
|
|
|
|
await channel.SendMessageAsync(embed);
|
|
|
|
if (Config.deleteMessagesAfterSummary)
|
|
{
|
|
await DeletePreviousMessages(interviewRoot, channel);
|
|
}
|
|
|
|
if (!Database.TryDeleteInterview(channel.Id))
|
|
{
|
|
Logger.Error("Could not delete interview from database. Channel ID: " + channel.Id);
|
|
}
|
|
return;
|
|
case QuestionType.END_WITHOUT_SUMMARY:
|
|
await channel.SendMessageAsync(new DiscordEmbedBuilder()
|
|
{
|
|
Color = Utilities.StringToColor(nextQuestion.color),
|
|
Title = nextQuestion.title,
|
|
Description = nextQuestion.message
|
|
});
|
|
|
|
if (Config.deleteMessagesAfterNoSummary)
|
|
{
|
|
await DeletePreviousMessages(interviewRoot, channel);
|
|
}
|
|
|
|
if (!Database.TryDeleteInterview(channel.Id))
|
|
{
|
|
Logger.Error("Could not delete interview from database. Channel ID: " + channel.Id);
|
|
}
|
|
break;
|
|
case QuestionType.ERROR:
|
|
default:
|
|
if (answerMessage == null)
|
|
{
|
|
DiscordMessage errorMessage = await channel.SendMessageAsync(new DiscordEmbedBuilder()
|
|
{
|
|
Color = Utilities.StringToColor(nextQuestion.color),
|
|
Title = nextQuestion.title,
|
|
Description = nextQuestion.message
|
|
});
|
|
previousQuestion.AddRelatedMessageIDs(errorMessage.Id);
|
|
}
|
|
else
|
|
{
|
|
DiscordMessageBuilder errorMessageBuilder = new DiscordMessageBuilder()
|
|
.AddEmbed(new DiscordEmbedBuilder()
|
|
{
|
|
Color = Utilities.StringToColor(nextQuestion.color),
|
|
Title = nextQuestion.title,
|
|
Description = nextQuestion.message
|
|
}).WithReply(answerMessage.Id);
|
|
DiscordMessage errorMessage = await answerMessage.RespondAsync(errorMessageBuilder);
|
|
previousQuestion.AddRelatedMessageIDs(errorMessage.Id, answerMessage.Id);
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
|
|
private static async Task DeletePreviousMessages(InterviewQuestion interviewRoot, DiscordChannel channel)
|
|
{
|
|
List<ulong> previousMessages = [];
|
|
interviewRoot.GetMessageIDs(ref previousMessages);
|
|
|
|
foreach (ulong previousMessageID in previousMessages)
|
|
{
|
|
try
|
|
{
|
|
DiscordMessage previousMessage = await channel.GetMessageAsync(previousMessageID);
|
|
await channel.DeleteMessageAsync(previousMessage, "Deleting old interview message.");
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
Logger.Warn("Failed to delete old interview message: ", e);
|
|
}
|
|
}
|
|
}
|
|
|
|
private static async Task CreateQuestion(DiscordChannel channel, InterviewQuestion question)
|
|
{
|
|
DiscordMessageBuilder msgBuilder = new();
|
|
DiscordEmbedBuilder embed = new()
|
|
{
|
|
Color = Utilities.StringToColor(question.color),
|
|
Title = question.title,
|
|
Description = question.message
|
|
};
|
|
|
|
switch (question.type)
|
|
{
|
|
case QuestionType.BUTTONS:
|
|
int nrOfButtons = 0;
|
|
for (int nrOfButtonRows = 0; nrOfButtonRows < 5 && nrOfButtons < question.paths.Count; nrOfButtonRows++)
|
|
{
|
|
List<DiscordButtonComponent> buttonRow = [];
|
|
for (; nrOfButtons < 5 * (nrOfButtonRows + 1) && nrOfButtons < question.paths.Count; nrOfButtons++)
|
|
{
|
|
(string questionString, InterviewQuestion nextQuestion) = question.paths.ToArray()[nrOfButtons];
|
|
buttonRow.Add(new DiscordButtonComponent(nextQuestion.GetButtonStyle(), "supportchild_interviewbutton " + nrOfButtons, questionString));
|
|
}
|
|
msgBuilder.AddComponents(buttonRow);
|
|
}
|
|
break;
|
|
case QuestionType.TEXT_SELECTOR:
|
|
List<DiscordSelectComponent> selectionComponents = [];
|
|
|
|
int selectionOptions = 0;
|
|
for (int selectionBoxes = 0; selectionBoxes < 5 && selectionOptions < question.paths.Count; selectionBoxes++)
|
|
{
|
|
List<DiscordSelectComponentOption> categoryOptions = [];
|
|
for (; selectionOptions < 25 * (selectionBoxes + 1) && selectionOptions < question.paths.Count; selectionOptions++)
|
|
{
|
|
(string questionString, InterviewQuestion nextQuestion) = question.paths.ToArray()[selectionOptions];
|
|
categoryOptions.Add(new DiscordSelectComponentOption(questionString, selectionOptions.ToString(), nextQuestion.selectorDescription));
|
|
}
|
|
|
|
selectionComponents.Add(new DiscordSelectComponent("supportchild_interviewselector " + selectionBoxes, string.IsNullOrWhiteSpace(question.selectorPlaceholder)
|
|
? "Select an option..." : question.selectorPlaceholder, categoryOptions));
|
|
}
|
|
|
|
msgBuilder.AddComponents(selectionComponents);
|
|
break;
|
|
case QuestionType.ROLE_SELECTOR:
|
|
msgBuilder.AddComponents(new DiscordRoleSelectComponent("supportchild_interviewroleselector", string.IsNullOrWhiteSpace(question.selectorPlaceholder)
|
|
? "Select a role..." : question.selectorPlaceholder));
|
|
break;
|
|
case QuestionType.USER_SELECTOR:
|
|
msgBuilder.AddComponents(new DiscordUserSelectComponent("supportchild_interviewuserselector", string.IsNullOrWhiteSpace(question.selectorPlaceholder)
|
|
? "Select a user..." : question.selectorPlaceholder));
|
|
break;
|
|
case QuestionType.CHANNEL_SELECTOR:
|
|
msgBuilder.AddComponents(new DiscordChannelSelectComponent("supportchild_interviewchannelselector", string.IsNullOrWhiteSpace(question.selectorPlaceholder)
|
|
? "Select a channel..." : question.selectorPlaceholder));
|
|
break;
|
|
case QuestionType.MENTIONABLE_SELECTOR:
|
|
msgBuilder.AddComponents(new DiscordMentionableSelectComponent("supportchild_interviewmentionableselector", string.IsNullOrWhiteSpace(question.selectorPlaceholder)
|
|
? "Select a user or role..." : question.selectorPlaceholder));
|
|
break;
|
|
case QuestionType.TEXT_INPUT:
|
|
embed.WithFooter("Reply to this message with your answer. You cannot include images or files.");
|
|
break;
|
|
case QuestionType.END_WITH_SUMMARY:
|
|
case QuestionType.END_WITHOUT_SUMMARY:
|
|
case QuestionType.ERROR:
|
|
default:
|
|
break;
|
|
}
|
|
|
|
msgBuilder.AddEmbed(embed);
|
|
DiscordMessage message = await channel.SendMessageAsync(msgBuilder);
|
|
question.messageID = message.Id;
|
|
}
|
|
} |