2024-12-27 02:47:04 +13:00
using System ;
using System.Collections.Generic ;
2024-12-27 01:54:28 +13:00
using System.Collections.Specialized ;
using System.Linq ;
2024-12-27 02:47:04 +13:00
using System.Reflection ;
2025-02-04 16:32:21 +13:00
using System.Text.RegularExpressions ;
2024-12-27 01:54:28 +13:00
using DSharpPlus.Entities ;
using Newtonsoft.Json ;
using Newtonsoft.Json.Converters ;
2024-12-27 02:47:04 +13:00
using Newtonsoft.Json.Serialization ;
2024-12-27 01:54:28 +13:00
namespace SupportChild.Interviews ;
2024-12-27 17:23:03 +13:00
public enum MessageType
2024-12-27 01:54:28 +13:00
{
2024-12-27 17:23:03 +13:00
// TODO: Support multiselector as separate type
2024-12-27 01:54:28 +13:00
ERROR ,
2025-02-04 19:27:20 +13:00
INTERVIEW_END = 1 ,
[Obsolete("Use INTERVIEW_END instead")] END_WITH_SUMMARY = 1 ,
[Obsolete("Use INTERVIEW_END instead")] END_WITHOUT_SUMMARY = 1 ,
2024-12-27 01:54:28 +13:00
BUTTONS ,
TEXT_SELECTOR ,
USER_SELECTOR ,
ROLE_SELECTOR ,
MENTIONABLE_SELECTOR , // User or role
CHANNEL_SELECTOR ,
2025-02-04 16:32:21 +13:00
TEXT_INPUT ,
REFERENCE_END
2024-12-27 01:54:28 +13:00
}
public enum ButtonType
{
PRIMARY ,
SECONDARY ,
SUCCESS ,
DANGER
}
2025-02-04 16:32:21 +13:00
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 ) ;
}
2025-02-04 19:21:48 +13:00
public bool TryGetReferencedStep ( Dictionary < string , InterviewStep > definitions , out InterviewStep step , bool ignoreReferenceParameters = false )
2025-02-04 16:32:21 +13:00
{
2025-02-04 19:21:48 +13:00
if ( ! definitions . TryGetValue ( id , out step ) )
2025-02-04 16:32:21 +13:00
{
2025-02-04 19:21:48 +13:00
Logger . Error ( "Could not find referenced step '" + id + "' in interview." ) ;
2025-02-04 16:32:21 +13:00
return false ;
}
2025-02-04 19:21:48 +13:00
if ( ! ignoreReferenceParameters )
{
step . buttonStyle = buttonStyle ;
step . selectorDescription = selectorDescription ;
step . afterReferenceStep = afterReferenceStep ;
}
2025-02-04 16:32:21 +13:00
return true ;
}
}
2024-12-27 17:23:03 +13:00
// A tree of steps representing an interview.
2024-12-27 01:54:28 +13:00
// 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.
2024-12-27 16:34:28 +13:00
// The entire interview tree is serialized and stored in the database to record responses as they are made.
2024-12-27 17:23:03 +13:00
public class InterviewStep
2024-12-27 01:54:28 +13:00
{
// Title of the message embed.
2024-12-27 17:23:03 +13:00
[JsonProperty("heading")]
public string heading ;
2024-12-27 01:54:28 +13:00
// Message contents sent to the user.
[JsonProperty("message")]
public string message ;
2024-12-27 17:23:03 +13:00
// The type of message.
2024-12-27 01:54:28 +13:00
[JsonConverter(typeof(StringEnumConverter))]
2024-12-27 02:47:04 +13:00
[JsonProperty("message-type")]
2024-12-27 17:23:03 +13:00
public MessageType messageType ;
2024-12-27 01:54:28 +13:00
// Colour of the message embed.
[JsonProperty("color")]
2025-02-04 16:32:21 +13:00
public string color = "CYAN" ;
2024-12-27 01:54:28 +13:00
2024-12-27 17:23:03 +13:00
// Used as label for this answer in the post-interview summary.
2024-12-27 01:54:28 +13:00
[JsonProperty("summary-field")]
public string summaryField ;
2024-12-27 17:23:03 +13:00
// If this step is on a button, give it this style.
2024-12-27 01:54:28 +13:00
[JsonConverter(typeof(StringEnumConverter))]
[JsonProperty("button-style")]
public ButtonType ? buttonStyle ;
2025-02-04 16:32:21 +13:00
// If this step is a selector, give it this placeholder.
2024-12-27 01:54:28 +13:00
[JsonProperty("selector-placeholder")]
public string selectorPlaceholder ;
2025-02-04 16:32:21 +13:00
// If this step is in a selector, give it this description.
2024-12-27 01:54:28 +13:00
[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.
2024-12-27 17:23:03 +13:00
[JsonProperty("min-length")]
2024-12-27 01:54:28 +13:00
public int? minLength ;
2025-02-04 19:27:20 +13:00
// Adds a summary to the message.
[JsonProperty("add-summary")]
public bool? addSummary ;
2025-02-04 16:32:21 +13:00
// References to steps defined elsewhere in the template
[JsonProperty("step-references")]
public Dictionary < string , ReferencedInterviewStep > references = new ( ) ;
2025-02-04 19:30:12 +13:00
// If set will merge answers with the delimiter, otherwise will overwrite
[JsonProperty("answer-delimiter")]
public string answerDelimiter ;
2024-12-27 01:54:28 +13:00
// Possible questions to ask next, an error message, or the end of the interview.
2024-12-27 17:23:03 +13:00
[JsonProperty("steps")]
public Dictionary < string , InterviewStep > steps = new ( ) ;
2024-12-27 01:54:28 +13:00
// ////////////////////////////////////////////////////////////////////////////
// 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 ;
2025-02-04 16:32:21 +13:00
// 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 )
2024-12-27 01:54:28 +13:00
{
// This object has not been initialized, we have checked too deep.
if ( messageID = = 0 )
{
2025-02-04 16:32:21 +13:00
previousSteps = null ;
2024-12-27 01:54:28 +13:00
return false ;
}
// Check children.
2024-12-27 17:23:03 +13:00
foreach ( KeyValuePair < string , InterviewStep > childStep in steps )
2024-12-27 01:54:28 +13:00
{
// This child either is the one we are looking for or contains the one we are looking for.
2025-02-04 16:32:21 +13:00
if ( childStep . Value . TryGetTakenSteps ( out previousSteps ) )
2024-12-27 01:54:28 +13:00
{
2025-02-04 16:32:21 +13:00
previousSteps . Add ( this ) ;
2024-12-27 01:54:28 +13:00
return true ;
}
}
// This object is the deepest object with a message ID set, meaning it is the latest asked question.
2025-02-04 19:27:20 +13:00
previousSteps = [ this ] ;
2024-12-27 01:54:28 +13:00
return true ;
}
public void GetSummary ( ref OrderedDictionary summary )
{
if ( messageID = = 0 )
{
return ;
}
2025-02-04 19:27:20 +13:00
if ( ! string . IsNullOrWhiteSpace ( summaryField ) & & ! string . IsNullOrWhiteSpace ( answer ) )
2024-12-27 01:54:28 +13:00
{
2025-02-04 19:30:12 +13:00
if ( answerDelimiter ! = null & & summary . Contains ( summaryField ) )
{
summary [ summaryField ] + = answerDelimiter + answer ;
}
else
{
summary [ summaryField ] = answer ;
}
2024-12-27 01:54:28 +13:00
}
// This will always contain exactly one or zero children.
2024-12-27 17:23:03 +13:00
foreach ( KeyValuePair < string , InterviewStep > step in steps )
2024-12-27 01:54:28 +13:00
{
2024-12-27 17:23:03 +13:00
step . Value . GetSummary ( ref summary ) ;
2024-12-27 01:54:28 +13:00
}
}
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.
2024-12-27 17:23:03 +13:00
foreach ( KeyValuePair < string , InterviewStep > step in steps )
2024-12-27 01:54:28 +13:00
{
2024-12-27 17:23:03 +13:00
step . Value . GetMessageIDs ( ref messageIDs ) ;
2024-12-27 01:54:28 +13:00
}
}
public void AddRelatedMessageIDs ( params ulong [ ] messageIDs )
{
if ( relatedMessageIDs = = null )
{
relatedMessageIDs = messageIDs . ToList ( ) ;
}
else
{
relatedMessageIDs . AddRange ( messageIDs ) ;
}
}
2025-02-04 19:21:48 +13:00
// Gets all steps in the interview tree, including after-reference-steps but not referenced steps
private void GetAllSteps ( ref List < InterviewStep > allSteps )
{
allSteps . Add ( this ) ;
foreach ( KeyValuePair < string , InterviewStep > step in steps )
{
step . Value . GetAllSteps ( ref allSteps ) ;
}
foreach ( KeyValuePair < string , ReferencedInterviewStep > reference in references )
{
reference . Value . afterReferenceStep ? . GetAllSteps ( ref allSteps ) ;
}
}
2024-12-27 17:23:03 +13:00
public void Validate ( ref List < string > errors ,
ref List < string > warnings ,
string stepID ,
2025-02-04 19:21:48 +13:00
Dictionary < string , InterviewStep > definitions ,
2024-12-27 17:23:03 +13:00
int summaryFieldCount = 0 ,
int summaryMaxLength = 0 ,
InterviewStep parent = null )
2024-12-27 02:47:04 +13:00
{
2024-12-27 03:39:00 +13:00
if ( ! string . IsNullOrWhiteSpace ( summaryField ) )
2024-12-27 02:47:04 +13:00
{
2024-12-27 03:39:00 +13:00
+ + summaryFieldCount ;
2024-12-27 02:47:04 +13:00
summaryMaxLength + = summaryField . Length ;
2024-12-27 17:23:03 +13:00
switch ( messageType )
2024-12-27 02:47:04 +13:00
{
2024-12-27 17:23:03 +13:00
case MessageType . BUTTONS :
case MessageType . TEXT_SELECTOR :
2024-12-27 03:39:00 +13:00
// Get the longest button/selector text
2024-12-27 17:23:03 +13:00
if ( steps . Count > 0 )
2024-12-27 03:39:00 +13:00
{
2024-12-27 17:23:03 +13:00
summaryMaxLength + = steps . Max ( kv = > kv . Key . Length ) ;
2024-12-27 03:39:00 +13:00
}
2024-12-27 02:47:04 +13:00
break ;
2024-12-27 17:23:03 +13:00
case MessageType . USER_SELECTOR :
case MessageType . ROLE_SELECTOR :
case MessageType . MENTIONABLE_SELECTOR :
case MessageType . CHANNEL_SELECTOR :
2024-12-27 02:47:04 +13:00
// Approximate length of a mention
summaryMaxLength + = 23 ;
break ;
2024-12-27 17:23:03 +13:00
case MessageType . TEXT_INPUT :
2024-12-27 02:47:04 +13:00
summaryMaxLength + = Math . Min ( maxLength ? ? 1024 , 1024 ) ;
break ;
2025-02-04 19:27:20 +13:00
case MessageType . INTERVIEW_END :
2024-12-27 17:23:03 +13:00
case MessageType . ERROR :
2025-02-04 19:21:48 +13:00
case MessageType . REFERENCE_END :
2024-12-27 02:47:04 +13:00
default :
break ;
}
}
2024-12-27 03:39:00 +13:00
2025-02-04 19:30:12 +13:00
// TODO: Warning for answer-delimiter if there is no summary-field
2025-02-04 19:21:48 +13:00
// TODO: Add url button here when implemented
if ( messageType is MessageType . REFERENCE_END )
{
if ( ! string . IsNullOrWhiteSpace ( message ) )
{
warnings . Add ( "The message parameter on '" + messageType + "' steps have no effect.\n\n> " + stepID + ".message" ) ;
}
}
else
{
if ( string . IsNullOrWhiteSpace ( message ) )
{
errors . Add ( "'" + messageType + "' steps must have a message parameter.\n\n> " + stepID + ".message" ) ;
}
}
2025-02-04 19:27:20 +13:00
if ( messageType is MessageType . ERROR or MessageType . INTERVIEW_END or MessageType . REFERENCE_END )
2024-12-27 03:39:00 +13:00
{
2025-02-04 16:32:21 +13:00
if ( steps . Count > 0 | | references . Count > 0 )
2024-12-27 03:39:00 +13:00
{
2025-02-04 19:21:48 +13:00
warnings . Add ( "Steps of the type '" + messageType + "' cannot have child steps.\n\n> " + stepID + ".message-type" ) ;
2024-12-27 03:39:00 +13:00
}
if ( ! string . IsNullOrWhiteSpace ( summaryField ) )
{
2025-02-04 19:21:48 +13:00
warnings . Add ( "Steps of the type '" + messageType + "' cannot have summary field names.\n\n> " + stepID + ".summary-field" ) ;
2024-12-27 03:39:00 +13:00
}
}
2025-02-04 16:32:21 +13:00
else if ( steps . Count = = 0 & & references . Count = = 0 )
2024-12-27 03:39:00 +13:00
{
2025-02-04 19:21:48 +13:00
errors . Add ( "Steps of the type '" + messageType + "' must have at least one child step.\n\n> " + stepID + ".message-type" ) ;
}
foreach ( KeyValuePair < string , ReferencedInterviewStep > reference in references )
{
if ( ! reference . Value . TryGetReferencedStep ( definitions , out InterviewStep referencedStep , true ) )
{
errors . Add ( "'" + reference . Value . id + "' does not exist in the step definitions.\n\n> " + FormatJSONKey ( stepID + ".step-references" , reference . Key ) ) ;
}
else if ( reference . Value . afterReferenceStep = = null )
{
2025-02-04 19:27:20 +13:00
List < InterviewStep > allChildSteps = [ ] ;
2025-02-04 19:21:48 +13:00
referencedStep . GetAllSteps ( ref allChildSteps ) ;
if ( allChildSteps . Any ( s = > s . messageType = = MessageType . REFERENCE_END ) )
{
errors . Add ( "The '" + FormatJSONKey ( stepID + ".step-references" , reference . Key ) + "' reference needs an after-reference-step as the '" + reference . Value . id + "' definition contains a REFERENCE_END step." ) ;
}
}
2024-12-27 03:39:00 +13:00
}
2025-02-04 19:27:20 +13:00
if ( addSummary ? ? false )
2024-12-27 03:39:00 +13:00
{
summaryMaxLength + = message ? . Length ? ? 0 ;
2024-12-27 17:23:03 +13:00
summaryMaxLength + = heading ? . Length ? ? 0 ;
2024-12-27 03:39:00 +13:00
if ( summaryFieldCount > 25 )
{
2025-02-04 19:21:48 +13:00
errors . Add ( "A summary cannot contain more than 25 fields, but you have " + summaryFieldCount + " fields in this branch.\n\n> " + stepID ) ;
2024-12-27 03:39:00 +13:00
}
else if ( summaryMaxLength > = 6000 )
{
warnings . Add ( "A summary cannot contain more than 6000 characters, but this branch may reach " + summaryMaxLength + " characters.\n" +
2025-02-04 19:21:48 +13:00
"Use the \"max-length\" parameter to limit text input field lengths, or shorten other parts of the summary message.\n\n> " + stepID ) ;
2024-12-27 03:39:00 +13:00
}
}
2024-12-27 17:23:03 +13:00
if ( parent ? . messageType is not MessageType . BUTTONS & & buttonStyle ! = null )
{
2025-02-04 19:21:48 +13:00
warnings . Add ( "Button styles have no effect on child steps of a '" + parent ? . messageType + "' step.\n\n> " + stepID + ".button-style" ) ;
2024-12-27 17:23:03 +13:00
}
if ( parent ? . messageType is not MessageType . TEXT_SELECTOR & & selectorDescription ! = null )
{
2025-02-04 19:21:48 +13:00
warnings . Add ( "Selector descriptions have no effect on child steps of a '" + parent ? . messageType + "' step.\n\n> " + stepID + ".selector-description" ) ;
2024-12-27 17:23:03 +13:00
}
if ( messageType is not MessageType . TEXT_SELECTOR & & selectorPlaceholder ! = null )
{
2025-02-04 19:21:48 +13:00
warnings . Add ( "Selector placeholders have no effect on steps of the type '" + messageType + "'.\n\n> " + stepID + ".selector-placeholder" ) ;
2024-12-27 17:23:03 +13:00
}
if ( messageType is not MessageType . TEXT_INPUT & & maxLength ! = null )
{
2025-02-04 19:21:48 +13:00
warnings . Add ( "Max length has no effect on steps of the type '" + messageType + "'.\n\n> " + stepID + ".max-length" ) ;
2024-12-27 17:23:03 +13:00
}
if ( messageType is not MessageType . TEXT_INPUT & & minLength ! = null )
{
2025-02-04 19:21:48 +13:00
warnings . Add ( "Min length has no effect on steps of the type '" + messageType + "'.\n\n> " + stepID + ".min-length" ) ;
2024-12-27 17:23:03 +13:00
}
foreach ( KeyValuePair < string , InterviewStep > step in steps )
2024-12-27 03:39:00 +13:00
{
2025-02-04 19:21:48 +13:00
step . Value . Validate ( ref errors , ref warnings , FormatJSONKey ( stepID + ".steps" , step . Key ) , definitions , summaryFieldCount , summaryMaxLength , this ) ;
2024-12-27 03:39:00 +13:00
}
2024-12-27 02:47:04 +13:00
}
2025-02-04 19:21:48 +13:00
private string FormatJSONKey ( string parentPath , string key )
{
// The JSON schema error messages use this format for the JSON path, so we use it in the validation too.
return parentPath + ( key . ContainsAny ( '.' , ' ' , '[' , ']' , '(' , ')' , '/' , '\\' )
? "['" + key + "']"
: "." + key ) ;
}
2025-02-04 16:32:21 +13:00
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
} ;
}
2024-12-27 02:47:04 +13:00
public class StripInternalPropertiesResolver : DefaultContractResolver
{
private static readonly HashSet < string > ignoreProps =
[
"message-id" ,
"answer" ,
"answer-id" ,
"related-message-ids"
] ;
protected override JsonProperty CreateProperty ( MemberInfo member , MemberSerialization memberSerialization )
{
JsonProperty property = base . CreateProperty ( member , memberSerialization ) ;
if ( ignoreProps . Contains ( property . PropertyName ) )
{
property . ShouldSerialize = _ = > false ;
}
return property ;
}
}
2024-12-27 01:54:28 +13:00
}
2025-02-04 16:32:21 +13:00
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 )
2024-12-27 01:54:28 +13:00
{
[JsonProperty("category-id", Required = Required.Always)]
public ulong categoryID = categoryID ;
[JsonProperty("interview", Required = Required.Always)]
2024-12-27 17:23:03 +13:00
public InterviewStep interview = interview ;
2025-02-04 16:32:21 +13:00
[JsonProperty("definitions", Required = Required.Default)]
public Dictionary < string , InterviewStep > definitions = definitions ;
2024-12-27 01:54:28 +13:00
}