336 lines
No EOL
11 KiB
C#
336 lines
No EOL
11 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Collections.Specialized;
|
|
using System.Linq;
|
|
using System.Reflection;
|
|
using DSharpPlus.Entities;
|
|
using Newtonsoft.Json;
|
|
using Newtonsoft.Json.Converters;
|
|
using Newtonsoft.Json.Serialization;
|
|
|
|
namespace SupportChild.Interviews;
|
|
|
|
public enum MessageType
|
|
{
|
|
// TODO: Support multiselector as separate type
|
|
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
|
|
{
|
|
PRIMARY,
|
|
SECONDARY,
|
|
SUCCESS,
|
|
DANGER
|
|
}
|
|
|
|
// 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.
|
|
// The entire interview tree is serialized and stored in the database to record responses as they are made.
|
|
public class InterviewStep
|
|
{
|
|
// Title of the message embed.
|
|
[JsonProperty("heading")]
|
|
public string heading;
|
|
|
|
// Message contents sent to the user.
|
|
[JsonProperty("message")]
|
|
public string message;
|
|
|
|
// The type of message.
|
|
[JsonConverter(typeof(StringEnumConverter))]
|
|
[JsonProperty("message-type")]
|
|
public MessageType messageType;
|
|
|
|
// Colour of the message embed.
|
|
[JsonProperty("color")]
|
|
public string color;
|
|
|
|
// Used as label for this answer in the post-interview summary.
|
|
[JsonProperty("summary-field")]
|
|
public string summaryField;
|
|
|
|
// If this step is on a button, give it this style.
|
|
[JsonConverter(typeof(StringEnumConverter))]
|
|
[JsonProperty("button-style")]
|
|
public ButtonType? buttonStyle;
|
|
|
|
// If this step is on a selector, give it this placeholder.
|
|
[JsonProperty("selector-placeholder")]
|
|
public string selectorPlaceholder;
|
|
|
|
// If this step 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")]
|
|
public int? minLength;
|
|
|
|
// Possible questions to ask next, an error message, or the end of the interview.
|
|
[JsonProperty("steps")]
|
|
public Dictionary<string, InterviewStep> steps = new();
|
|
|
|
// ////////////////////////////////////////////////////////////////////////////
|
|
// 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 TryGetCurrentStep(out InterviewStep step)
|
|
{
|
|
// This object has not been initialized, we have checked too deep.
|
|
if (messageID == 0)
|
|
{
|
|
step = null;
|
|
return false;
|
|
}
|
|
|
|
// Check children.
|
|
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))
|
|
{
|
|
return true;
|
|
}
|
|
}
|
|
|
|
// This object is the deepest object with a message ID set, meaning it is the latest asked question.
|
|
step = 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, InterviewStep> step in steps)
|
|
{
|
|
step.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, InterviewStep> step in steps)
|
|
{
|
|
step.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.Secondary
|
|
};
|
|
}
|
|
|
|
public void Validate(ref List<string> errors,
|
|
ref List<string> warnings,
|
|
string stepID,
|
|
int summaryFieldCount = 0,
|
|
int summaryMaxLength = 0,
|
|
InterviewStep parent = null)
|
|
{
|
|
if (!string.IsNullOrWhiteSpace(summaryField))
|
|
{
|
|
++summaryFieldCount;
|
|
summaryMaxLength += summaryField.Length;
|
|
switch (messageType)
|
|
{
|
|
case MessageType.BUTTONS:
|
|
case MessageType.TEXT_SELECTOR:
|
|
// Get the longest button/selector text
|
|
if (steps.Count > 0)
|
|
{
|
|
summaryMaxLength += steps.Max(kv => kv.Key.Length);
|
|
}
|
|
break;
|
|
case MessageType.USER_SELECTOR:
|
|
case MessageType.ROLE_SELECTOR:
|
|
case MessageType.MENTIONABLE_SELECTOR:
|
|
case MessageType.CHANNEL_SELECTOR:
|
|
// Approximate length of a mention
|
|
summaryMaxLength += 23;
|
|
break;
|
|
case MessageType.TEXT_INPUT:
|
|
summaryMaxLength += Math.Min(maxLength ?? 1024, 1024);
|
|
break;
|
|
case MessageType.END_WITH_SUMMARY:
|
|
case MessageType.END_WITHOUT_SUMMARY:
|
|
case MessageType.ERROR:
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (messageType is MessageType.ERROR or MessageType.END_WITH_SUMMARY or MessageType.END_WITHOUT_SUMMARY)
|
|
{
|
|
if (steps.Count > 0)
|
|
{
|
|
warnings.Add("Steps of the type '" + messageType + "' cannot have child steps.\n\n" + stepID + ".message-type");
|
|
}
|
|
|
|
if (!string.IsNullOrWhiteSpace(summaryField))
|
|
{
|
|
warnings.Add("Steps of the type '" + messageType + "' cannot have summary field names.\n\n" + stepID + ".summary-field");
|
|
}
|
|
}
|
|
else if (steps.Count == 0)
|
|
{
|
|
errors.Add("Steps of the type '" + messageType + "' must have at least one child step.\n\n" + stepID + ".message-type");
|
|
}
|
|
|
|
if (messageType is MessageType.END_WITH_SUMMARY)
|
|
{
|
|
summaryMaxLength += message?.Length ?? 0;
|
|
summaryMaxLength += heading?.Length ?? 0;
|
|
if (summaryFieldCount > 25)
|
|
{
|
|
errors.Add("A summary cannot contain more than 25 fields, but you have " + summaryFieldCount + " fields in this branch.\n\n" + stepID);
|
|
}
|
|
else if (summaryMaxLength >= 6000)
|
|
{
|
|
warnings.Add("A summary cannot contain more than 6000 characters, but this branch may reach " + summaryMaxLength + " characters.\n" +
|
|
"Use the \"max-length\" parameter to limit text input field lengths, or shorten other parts of the summary message.\n\n" + stepID);
|
|
}
|
|
}
|
|
|
|
if (parent?.messageType is not MessageType.BUTTONS && buttonStyle != null)
|
|
{
|
|
warnings.Add("Button styles have no effect on child steps of a '" + parent?.messageType + "' step.\n\n" + stepID + ".button-style");
|
|
}
|
|
|
|
if (parent?.messageType is not MessageType.TEXT_SELECTOR && selectorDescription != null)
|
|
{
|
|
warnings.Add("Selector descriptions have no effect on child steps of a '" + parent?.messageType + "' step.\n\n" + stepID + ".selector-description");
|
|
}
|
|
|
|
if (messageType is not MessageType.TEXT_SELECTOR && selectorPlaceholder != null)
|
|
{
|
|
warnings.Add("Selector placeholders have no effect on steps of the type '" + messageType + "'.\n\n" + stepID + ".selector-placeholder");
|
|
}
|
|
|
|
if (messageType is not MessageType.TEXT_INPUT && maxLength != null)
|
|
{
|
|
warnings.Add("Max length has no effect on steps of the type '" + messageType + "'.\n\n" + stepID + ".max-length");
|
|
}
|
|
|
|
if (messageType is not MessageType.TEXT_INPUT && minLength != null)
|
|
{
|
|
warnings.Add("Min length has no effect on steps of the type '" + messageType + "'.\n\n" + stepID + ".min-length");
|
|
}
|
|
|
|
foreach (KeyValuePair<string, InterviewStep> step in steps)
|
|
{
|
|
// The JSON schema error messages use this format for the JSON path, so we use it here too.
|
|
string nextStepID = stepID;
|
|
nextStepID += step.Key.ContainsAny('.', ' ', '[', ']', '(', ')', '/', '\\')
|
|
? ".steps['" + step.Key + "']"
|
|
: ".steps." + step.Key;
|
|
|
|
step.Value.Validate(ref errors, ref warnings, nextStepID, summaryFieldCount, summaryMaxLength, this);
|
|
}
|
|
}
|
|
|
|
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;
|
|
}
|
|
}
|
|
}
|
|
|
|
public class Template(ulong categoryID, InterviewStep interview)
|
|
{
|
|
[JsonProperty("category-id", Required = Required.Always)]
|
|
public ulong categoryID = categoryID;
|
|
|
|
[JsonProperty("interview", Required = Required.Always)]
|
|
public InterviewStep interview = interview;
|
|
} |