2024-12-26 07:12:50 +00:00
using System ;
using System.Collections ;
using System.Collections.Generic ;
using System.Collections.Specialized ;
2024-12-26 04:55:00 +00:00
using System.Linq ;
2024-12-26 07:12:50 +00:00
using System.Text.RegularExpressions ;
using System.Threading.Tasks ;
2024-12-26 07:47:07 +00:00
using DSharpPlus.Commands.Processors.SlashCommands ;
2024-12-26 04:55:00 +00:00
using DSharpPlus.Entities ;
using Newtonsoft.Json ;
2024-12-26 06:50:47 +00:00
using Newtonsoft.Json.Converters ;
2024-12-26 04:55:00 +00:00
namespace SupportChild ;
public static class Interviewer
{
public enum QuestionType
{
2024-12-26 07:51:41 +00:00
// Support multiselector as separate type, with only one subpath supported
2024-12-26 07:12:50 +00:00
ERROR ,
2024-12-26 07:28:39 +00:00
END_WITH_SUMMARY ,
END_WITHOUT_SUMMARY ,
2024-12-26 04:55:00 +00:00
BUTTONS ,
2024-12-26 07:16:18 +00:00
TEXT_SELECTOR ,
2024-12-26 07:51:41 +00:00
USER_SELECTOR ,
ROLE_SELECTOR ,
MENTIONABLE_SELECTOR , // User or role
CHANNEL_SELECTOR ,
2024-12-26 04:55:00 +00:00
TEXT_INPUT
}
2024-12-26 07:32:34 +00:00
public enum ButtonType
{
// Secondary first to make it the default
SECONDARY ,
PRIMARY ,
SUCCESS ,
DANGER
}
2024-12-26 04:55:00 +00:00
// 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.
2024-12-26 05:07:36 +00:00
public class InterviewQuestion
2024-12-26 04:55:00 +00:00
{
2024-12-26 07:32:34 +00:00
// Title of the message embed.
[JsonProperty("title")]
public string title ;
2024-12-26 07:12:50 +00:00
2024-12-26 04:55:00 +00:00
// Message contents sent to the user.
2024-12-26 06:50:47 +00:00
[JsonProperty("message")]
2024-12-26 04:55:00 +00:00
public string message ;
// The type of question.
2024-12-26 06:50:47 +00:00
[JsonConverter(typeof(StringEnumConverter))]
[JsonProperty("type")]
2024-12-26 04:55:00 +00:00
public QuestionType type ;
// Colour of the message embed.
2024-12-26 06:50:47 +00:00
[JsonProperty("color")]
2024-12-26 04:55:00 +00:00
public string color ;
// Used as label for this question in the post-interview summary.
2024-12-26 06:50:47 +00:00
[JsonProperty("summary-field")]
2024-12-26 04:55:00 +00:00
public string summaryField ;
2024-12-26 07:32:34 +00:00
// If this question is on a button, give it this style.
[JsonConverter(typeof(StringEnumConverter))]
[JsonProperty("button-style")]
public ButtonType buttonStyle ;
2024-12-26 09:24:42 +00:00
// If this question is on a selector, give it this placeholder.
[JsonProperty("selector-placeholder")]
public string selectorPlaceholder ;
2024-12-26 10:25:11 +00:00
// If this question is on a selector, give it this description.
[JsonProperty("selector-description")]
public string selectorDescription ;
2024-12-26 09:24:42 +00:00
// The maximum length of a text input.
2024-12-26 07:32:34 +00:00
[JsonProperty("max-length")]
public int maxLength ;
2024-12-26 09:24:42 +00:00
// The minimum length of a text input.
2024-12-26 07:32:34 +00:00
[JsonProperty("min-length", Required = Required.Default)]
public int minLength ;
// Possible questions to ask next, an error message, or the end of the interview.
2024-12-26 07:12:50 +00:00
[JsonProperty("paths")]
public Dictionary < string , InterviewQuestion > paths ;
2024-12-26 07:28:39 +00:00
// ////////////////////////////////////////////////////////////////////////////
// The following parameters are populated by the bot, not the json template. //
// ////////////////////////////////////////////////////////////////////////////
2024-12-26 07:12:50 +00:00
// The ID of this message where the bot asked this question.
[JsonProperty("message-id")]
public ulong messageID ;
// The contents of the user's answer.
2024-12-26 06:50:47 +00:00
[JsonProperty("answer")]
2024-12-26 04:55:00 +00:00
public string answer ;
2024-12-26 07:12:50 +00:00
// The ID of the user's answer message if this is a TEXT_INPUT type.
2024-12-26 06:50:47 +00:00
[JsonProperty("answer-id")]
2024-12-26 04:55:00 +00:00
public ulong answerID ;
2024-12-26 07:28:39 +00:00
// Any extra messages generated by the bot that should be removed when the interview ends.
[JsonProperty("related-message-ids")]
public List < ulong > relatedMessageIDs ;
2024-12-26 07:12:50 +00:00
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 )
{
2024-12-26 07:51:41 +00:00
if ( messageID = = 0 )
{
return ;
}
2024-12-26 07:12:50 +00:00
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 ) ;
}
2024-12-26 07:28:39 +00:00
if ( relatedMessageIDs ! = null )
{
messageIDs . AddRange ( relatedMessageIDs ) ;
}
2024-12-26 07:12:50 +00:00
// This will always contain exactly one or zero children.
foreach ( KeyValuePair < string , InterviewQuestion > path in paths )
{
path . Value . GetMessageIDs ( ref messageIDs ) ;
}
}
2024-12-26 07:28:39 +00:00
public void AddRelatedMessageIDs ( params ulong [ ] messageIDs )
{
if ( relatedMessageIDs = = null )
{
relatedMessageIDs = messageIDs . ToList ( ) ;
}
else
{
relatedMessageIDs . AddRange ( messageIDs ) ;
}
}
2024-12-26 07:32:34 +00:00
public DiscordButtonStyle GetButtonStyle ( )
{
return buttonStyle switch
{
ButtonType . PRIMARY = > DiscordButtonStyle . Primary ,
ButtonType . SECONDARY = > DiscordButtonStyle . Secondary ,
ButtonType . SUCCESS = > DiscordButtonStyle . Success ,
ButtonType . DANGER = > DiscordButtonStyle . Danger ,
_ = > DiscordButtonStyle . Primary
} ;
}
2024-12-26 04:55:00 +00:00
}
2024-12-26 06:50:47 +00:00
// 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
{
2024-12-26 07:32:34 +00:00
[JsonProperty("title", Required = Required.Default)]
public string title ;
2024-12-26 06:50:47 +00:00
[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 ;
2024-12-26 07:32:34 +00:00
[JsonProperty("summary-field", Required = Required.Default)]
2024-12-26 06:50:47 +00:00
public string summaryField ;
2024-12-26 07:32:34 +00:00
[JsonConverter(typeof(StringEnumConverter))]
[JsonProperty("button-style", Required = Required.Default)]
public ButtonType buttonStyle ;
2024-12-26 09:24:42 +00:00
[JsonProperty("selector-placeholder", Required = Required.Default)]
public string selectorPlaceholder ;
2024-12-26 10:25:11 +00:00
[JsonProperty("selector-description", Required = Required.Default)]
public string selectorDescription ;
2024-12-26 07:32:34 +00:00
[JsonProperty("max-length", Required = Required.Default)]
public int maxLength ;
[JsonProperty("min-length", Required = Required.Default)]
public int minLength ;
2024-12-26 06:50:47 +00:00
[JsonProperty("paths", Required = Required.Always)]
public Dictionary < string , ValidatedInterviewQuestion > paths ;
2024-12-26 10:23:25 +00:00
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 ;
}
}
}
2024-12-26 06:50:47 +00:00
}
2024-12-26 07:12:50 +00:00
public static async void StartInterview ( DiscordChannel channel )
2024-12-26 04:55:00 +00:00
{
2024-12-26 07:12:50 +00:00
if ( channel . Parent = = null )
2024-12-26 06:50:47 +00:00
{
2024-12-26 07:12:50 +00:00
return ;
}
2024-12-26 07:38:17 +00:00
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 ) )
2024-12-26 07:12:50 +00:00
{
await CreateQuestion ( channel , interview ) ;
Database . SaveInterview ( channel . Id , interview ) ;
}
2024-12-26 04:55:00 +00:00
}
2024-12-26 07:47:07 +00:00
public static async Task RestartInterview ( SlashCommandContext command )
{
2024-12-26 10:33:32 +00:00
if ( Database . TryGetInterview ( command . Channel . Id , out InterviewQuestion interviewRoot ) )
2024-12-26 07:47:07 +00:00
{
2024-12-26 10:47:55 +00:00
if ( Config . deleteMessagesAfterNoSummary )
{
await DeletePreviousMessages ( interviewRoot , command . Channel ) ;
}
2024-12-26 07:47:07 +00:00
if ( ! Database . TryDeleteInterview ( command . Channel . Id ) )
{
Logger . Error ( "Could not delete interview from database. Channel ID: " + command . Channel . Id ) ;
}
}
StartInterview ( command . Channel ) ;
}
2024-12-26 07:16:18 +00:00
public static async Task ProcessButtonOrSelectorResponse ( DiscordInteraction interaction )
2024-12-26 04:55:00 +00:00
{
2024-12-26 07:12:50 +00:00
if ( interaction ? . Channel = = null | | interaction ? . Message = = null )
2024-12-26 04:55:00 +00:00
{
return ;
}
2024-12-26 07:30:56 +00:00
// Ignore if option was deselected.
2024-12-26 07:51:41 +00:00
if ( interaction . Data . ComponentType is not DiscordComponentType . Button & & interaction . Data . Values . Length = = 0 )
2024-12-26 07:16:18 +00:00
{
2024-12-26 07:30:56 +00:00
await interaction . CreateResponseAsync ( DiscordInteractionResponseType . UpdateMessage ) ;
2024-12-26 07:16:18 +00:00
return ;
}
2024-12-26 07:28:39 +00:00
// Return if there is no active interview in this channel
2024-12-26 10:33:32 +00:00
if ( ! Database . TryGetInterview ( interaction . Channel . Id , out InterviewQuestion interviewRoot ) )
2024-12-26 04:55:00 +00:00
{
2024-12-26 07:30:56 +00:00
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 ( ) ) ;
2024-12-26 07:12:50 +00:00
return ;
2024-12-26 04:55:00 +00:00
}
2024-12-26 07:12:50 +00:00
2024-12-26 07:28:39 +00:00
// Return if the current question cannot be found in the interview.
2024-12-26 07:12:50 +00:00
if ( ! interviewRoot . TryGetCurrentQuestion ( out InterviewQuestion currentQuestion ) )
{
2024-12-26 07:30:56 +00:00
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" ) ;
2024-12-26 07:12:50 +00:00
return ;
}
2024-12-26 07:28:39 +00:00
// Check if this button/selector is for an older question.
2024-12-26 07:12:50 +00:00
if ( interaction . Message . Id ! = currentQuestion . messageID )
{
2024-12-26 07:30:56 +00:00
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 ( ) ) ;
2024-12-26 07:12:50 +00:00
return ;
}
2024-12-26 07:51:41 +00:00
try
{
await interaction . CreateResponseAsync ( DiscordInteractionResponseType . UpdateMessage ) ;
}
catch ( Exception e )
{
Logger . Error ( "Could not update original message:" , e ) ;
}
2024-12-26 07:32:34 +00:00
2024-12-26 07:28:39 +00:00
// Parse the response index from the button/selector.
2024-12-26 07:16:18 +00:00
string componentID = "" ;
2024-12-26 07:51:41 +00:00
string answer = "" ;
2024-12-26 07:16:18 +00:00
switch ( interaction . Data . ComponentType )
2024-12-26 07:12:50 +00:00
{
2024-12-26 07:51:41 +00:00
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 ;
2024-12-26 07:16:18 +00:00
case DiscordComponentType . StringSelect :
componentID = interaction . Data . Values [ 0 ] ;
break ;
case DiscordComponentType . Button :
componentID = interaction . Data . CustomId . Replace ( "supportchild_interviewbutton " , "" ) ;
break ;
2024-12-26 10:23:25 +00:00
case DiscordComponentType . ActionRow :
case DiscordComponentType . FormInput :
2024-12-26 07:16:18 +00:00
default :
2024-12-26 10:23:25 +00:00
throw new ArgumentOutOfRangeException ( "Tried to process an invalid component type: " + interaction . Data . ComponentType ) ;
2024-12-26 07:12:50 +00:00
}
2024-12-26 07:51:41 +00:00
// The different mentionable selectors provide the actual answer, while the others just return the ID.
if ( componentID = = "" )
2024-12-26 07:12:50 +00:00
{
2024-12-26 10:27:36 +00:00
foreach ( KeyValuePair < string , InterviewQuestion > path in currentQuestion . paths )
2024-12-26 07:51:41 +00:00
{
2024-12-26 10:27:36 +00:00
// Skip to the first matching path.
if ( Regex . IsMatch ( answer , path . Key ) )
{
await HandleAnswer ( answer , path . Value , interviewRoot , currentQuestion , interaction . Channel ) ;
return ;
}
2024-12-26 07:51:41 +00:00
}
2024-12-26 07:12:50 +00:00
2024-12-26 10:27:36 +00:00
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 ( ) ) ;
2024-12-26 07:12:50 +00:00
}
2024-12-26 07:51:41 +00:00
else
{
if ( ! int . TryParse ( componentID , out int pathIndex ) )
{
Logger . Error ( "Invalid interview button/selector index: " + componentID ) ;
return ;
}
2024-12-26 07:12:50 +00:00
2024-12-26 07:51:41 +00:00
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 ) ;
}
2024-12-26 07:12:50 +00:00
2024-12-26 04:55:00 +00:00
}
2024-12-26 07:32:34 +00:00
public static async Task ProcessResponseMessage ( DiscordMessage answerMessage )
2024-12-26 04:55:00 +00:00
{
2024-12-26 07:12:50 +00:00
// Either the message or the referenced message is null.
2024-12-26 07:32:34 +00:00
if ( answerMessage . Channel = = null | | answerMessage . ReferencedMessage ? . Channel = = null )
2024-12-26 07:12:50 +00:00
{
return ;
}
// The channel does not have an active interview.
2024-12-26 10:33:32 +00:00
if ( ! Database . TryGetInterview ( answerMessage . ReferencedMessage . Channel . Id , out InterviewQuestion interviewRoot ) )
2024-12-26 07:12:50 +00:00
{
return ;
}
if ( ! interviewRoot . TryGetCurrentQuestion ( out InterviewQuestion currentQuestion ) )
{
return ;
}
// The user responded to something other than the latest interview question.
2024-12-26 07:32:34 +00:00
if ( answerMessage . ReferencedMessage . Id ! = currentQuestion . messageID )
2024-12-26 07:12:50 +00:00
{
return ;
}
// The user responded to a question which does not take a text response.
if ( currentQuestion . type ! = QuestionType . TEXT_INPUT )
{
return ;
}
2024-12-26 07:32:34 +00:00
// 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 ;
}
2024-12-26 07:12:50 +00:00
foreach ( ( string questionString , InterviewQuestion nextQuestion ) in currentQuestion . paths )
{
2024-12-26 07:30:56 +00:00
// Skip to the first matching path.
2024-12-26 07:32:34 +00:00
if ( ! Regex . IsMatch ( answerMessage . Content , questionString ) ) continue ;
2024-12-26 07:12:50 +00:00
2024-12-26 07:51:41 +00:00
await HandleAnswer ( answerMessage . Content , nextQuestion , interviewRoot , currentQuestion , answerMessage . Channel , answerMessage ) ;
2024-12-26 07:16:18 +00:00
return ;
}
2024-12-26 07:12:50 +00:00
2024-12-26 10:27:36 +00:00
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 ) ;
2024-12-26 07:32:34 +00:00
DiscordMessage errorMessage = await answerMessage . RespondAsync ( new DiscordEmbedBuilder
2024-12-26 07:30:56 +00:00
{
2024-12-26 10:27:36 +00:00
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." ,
2024-12-26 07:30:56 +00:00
Color = DiscordColor . Red
} ) ;
2024-12-26 07:32:34 +00:00
currentQuestion . AddRelatedMessageIDs ( errorMessage . Id ) ;
2024-12-26 07:16:18 +00:00
}
2024-12-26 07:12:50 +00:00
2024-12-26 07:51:41 +00:00
private static async Task HandleAnswer ( string answer ,
2024-12-26 07:16:18 +00:00
InterviewQuestion nextQuestion ,
InterviewQuestion interviewRoot ,
InterviewQuestion previousQuestion ,
DiscordChannel channel ,
2024-12-26 07:28:39 +00:00
DiscordMessage answerMessage = null )
2024-12-26 07:16:18 +00:00
{
2024-12-26 11:38:35 +00:00
// The error message type should not alter anything about the interview.
2024-12-26 07:28:39 +00:00
if ( nextQuestion . type ! = QuestionType . ERROR )
2024-12-26 07:16:18 +00:00
{
2024-12-26 07:51:41 +00:00
previousQuestion . answer = answer ;
2024-12-26 11:38:35 +00:00
// There is no message ID if the question is not a text input.
previousQuestion . answerID = answerMessage = = null ? 0 : answerMessage . Id ;
2024-12-26 07:28:39 +00:00
}
2024-12-26 07:12:50 +00:00
2024-12-26 07:16:18 +00:00
// Create next question, or finish the interview.
switch ( nextQuestion . type )
{
case QuestionType . TEXT_INPUT :
case QuestionType . BUTTONS :
case QuestionType . TEXT_SELECTOR :
2024-12-26 07:51:41 +00:00
case QuestionType . ROLE_SELECTOR :
case QuestionType . USER_SELECTOR :
case QuestionType . CHANNEL_SELECTOR :
case QuestionType . MENTIONABLE_SELECTOR :
2024-12-26 07:16:18 +00:00
await CreateQuestion ( channel , nextQuestion ) ;
Database . SaveInterview ( channel . Id , interviewRoot ) ;
break ;
2024-12-26 07:28:39 +00:00
case QuestionType . END_WITH_SUMMARY :
2024-12-26 07:16:18 +00:00
OrderedDictionary summaryFields = new OrderedDictionary ( ) ;
interviewRoot . GetSummary ( ref summaryFields ) ;
2024-12-26 07:12:50 +00:00
2024-12-26 07:16:18 +00:00
DiscordEmbedBuilder embed = new DiscordEmbedBuilder ( )
{
Color = Utilities . StringToColor ( nextQuestion . color ) ,
2024-12-26 07:32:34 +00:00
Title = nextQuestion . title ,
2024-12-26 07:16:18 +00:00
Description = nextQuestion . message ,
} ;
2024-12-26 07:12:50 +00:00
2024-12-26 07:16:18 +00:00
foreach ( DictionaryEntry entry in summaryFields )
{
embed . AddField ( ( string ) entry . Key , ( string ) entry . Value ) ;
}
2024-12-26 07:12:50 +00:00
2024-12-26 07:16:18 +00:00
await channel . SendMessageAsync ( embed ) ;
2024-12-26 07:12:50 +00:00
2024-12-26 10:47:55 +00:00
if ( Config . deleteMessagesAfterSummary )
{
await DeletePreviousMessages ( interviewRoot , channel ) ;
}
2024-12-26 07:28:39 +00:00
if ( ! Database . TryDeleteInterview ( channel . Id ) )
2024-12-26 07:16:18 +00:00
{
2024-12-26 07:28:39 +00:00
Logger . Error ( "Could not delete interview from database. Channel ID: " + channel . Id ) ;
2024-12-26 07:16:18 +00:00
}
2024-12-26 07:28:39 +00:00
return ;
case QuestionType . END_WITHOUT_SUMMARY :
await channel . SendMessageAsync ( new DiscordEmbedBuilder ( )
{
Color = Utilities . StringToColor ( nextQuestion . color ) ,
2024-12-26 07:32:34 +00:00
Title = nextQuestion . title ,
2024-12-26 07:28:39 +00:00
Description = nextQuestion . message
} ) ;
2024-12-26 07:12:50 +00:00
2024-12-26 10:47:55 +00:00
if ( Config . deleteMessagesAfterNoSummary )
{
await DeletePreviousMessages ( interviewRoot , channel ) ;
}
2024-12-26 07:16:18 +00:00
if ( ! Database . TryDeleteInterview ( channel . Id ) )
{
Logger . Error ( "Could not delete interview from database. Channel ID: " + channel . Id ) ;
}
break ;
case QuestionType . ERROR :
default :
2024-12-26 07:28:39 +00:00
if ( answerMessage = = null )
{
DiscordMessage errorMessage = await channel . SendMessageAsync ( new DiscordEmbedBuilder ( )
{
Color = Utilities . StringToColor ( nextQuestion . color ) ,
2024-12-26 07:32:34 +00:00
Title = nextQuestion . title ,
2024-12-26 07:28:39 +00:00
Description = nextQuestion . message
} ) ;
previousQuestion . AddRelatedMessageIDs ( errorMessage . Id ) ;
}
else
{
DiscordMessageBuilder errorMessageBuilder = new DiscordMessageBuilder ( )
. AddEmbed ( new DiscordEmbedBuilder ( )
{
Color = Utilities . StringToColor ( nextQuestion . color ) ,
2024-12-26 07:32:34 +00:00
Title = nextQuestion . title ,
2024-12-26 07:28:39 +00:00
Description = nextQuestion . message
} ) . WithReply ( answerMessage . Id ) ;
DiscordMessage errorMessage = await answerMessage . RespondAsync ( errorMessageBuilder ) ;
previousQuestion . AddRelatedMessageIDs ( errorMessage . Id , answerMessage . Id ) ;
}
2024-12-26 07:16:18 +00:00
break ;
}
2024-12-26 04:55:00 +00:00
}
2024-12-26 07:28:39 +00:00
private static async Task DeletePreviousMessages ( InterviewQuestion interviewRoot , DiscordChannel channel )
{
2024-12-26 11:38:35 +00:00
List < ulong > previousMessages = [ ] ;
2024-12-26 07:28:39 +00:00
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 ) ;
}
}
}
2024-12-26 07:12:50 +00:00
private static async Task CreateQuestion ( DiscordChannel channel , InterviewQuestion question )
2024-12-26 04:55:00 +00:00
{
2024-12-26 09:24:42 +00:00
DiscordMessageBuilder msgBuilder = new ( ) ;
DiscordEmbedBuilder embed = new ( )
2024-12-26 04:55:00 +00:00
{
2024-12-26 05:07:36 +00:00
Color = Utilities . StringToColor ( question . color ) ,
2024-12-26 07:32:34 +00:00
Title = question . title ,
2024-12-26 05:07:36 +00:00
Description = question . message
2024-12-26 04:55:00 +00:00
} ;
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 + + )
{
2024-12-26 07:32:34 +00:00
( string questionString , InterviewQuestion nextQuestion ) = question . paths . ToArray ( ) [ nrOfButtons ] ;
buttonRow . Add ( new DiscordButtonComponent ( nextQuestion . GetButtonStyle ( ) , "supportchild_interviewbutton " + nrOfButtons , questionString ) ) ;
2024-12-26 04:55:00 +00:00
}
msgBuilder . AddComponents ( buttonRow ) ;
}
break ;
2024-12-26 07:16:18 +00:00
case QuestionType . TEXT_SELECTOR :
2024-12-26 04:55:00 +00:00
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 + + )
{
2024-12-26 10:25:11 +00:00
( string questionString , InterviewQuestion nextQuestion ) = question . paths . ToArray ( ) [ selectionOptions ] ;
categoryOptions . Add ( new DiscordSelectComponentOption ( questionString , selectionOptions . ToString ( ) , nextQuestion . selectorDescription ) ) ;
2024-12-26 04:55:00 +00:00
}
2024-12-26 09:24:42 +00:00
selectionComponents . Add ( new DiscordSelectComponent ( "supportchild_interviewselector " + selectionBoxes , string . IsNullOrWhiteSpace ( question . selectorPlaceholder )
? "Select an option..." : question . selectorPlaceholder , categoryOptions ) ) ;
2024-12-26 04:55:00 +00:00
}
msgBuilder . AddComponents ( selectionComponents ) ;
break ;
2024-12-26 07:51:41 +00:00
case QuestionType . ROLE_SELECTOR :
2024-12-26 09:24:42 +00:00
msgBuilder . AddComponents ( new DiscordRoleSelectComponent ( "supportchild_interviewroleselector" , string . IsNullOrWhiteSpace ( question . selectorPlaceholder )
? "Select a role..." : question . selectorPlaceholder ) ) ;
2024-12-26 07:51:41 +00:00
break ;
case QuestionType . USER_SELECTOR :
2024-12-26 09:24:42 +00:00
msgBuilder . AddComponents ( new DiscordUserSelectComponent ( "supportchild_interviewuserselector" , string . IsNullOrWhiteSpace ( question . selectorPlaceholder )
? "Select a user..." : question . selectorPlaceholder ) ) ;
2024-12-26 07:51:41 +00:00
break ;
case QuestionType . CHANNEL_SELECTOR :
2024-12-26 09:24:42 +00:00
msgBuilder . AddComponents ( new DiscordChannelSelectComponent ( "supportchild_interviewchannelselector" , string . IsNullOrWhiteSpace ( question . selectorPlaceholder )
? "Select a channel..." : question . selectorPlaceholder ) ) ;
2024-12-26 07:51:41 +00:00
break ;
case QuestionType . MENTIONABLE_SELECTOR :
2024-12-26 09:24:42 +00:00
msgBuilder . AddComponents ( new DiscordMentionableSelectComponent ( "supportchild_interviewmentionableselector" , string . IsNullOrWhiteSpace ( question . selectorPlaceholder )
? "Select a user or role..." : question . selectorPlaceholder ) ) ;
2024-12-26 07:51:41 +00:00
break ;
2024-12-26 04:55:00 +00:00
case QuestionType . TEXT_INPUT :
2024-12-26 07:12:50 +00:00
embed . WithFooter ( "Reply to this message with your answer. You cannot include images or files." ) ;
2024-12-26 04:55:00 +00:00
break ;
2024-12-26 07:28:39 +00:00
case QuestionType . END_WITH_SUMMARY :
case QuestionType . END_WITHOUT_SUMMARY :
2024-12-26 07:18:07 +00:00
case QuestionType . ERROR :
2024-12-26 04:55:00 +00:00
default :
break ;
}
2024-12-26 05:07:36 +00:00
msgBuilder . AddEmbed ( embed ) ;
2024-12-26 04:55:00 +00:00
DiscordMessage message = await channel . SendMessageAsync ( msgBuilder ) ;
question . messageID = message . Id ;
}
}