Add JSON schema for interview templates, switched so using it internally for validation too
This commit is contained in:
parent
a254bc5697
commit
76462b97fb
6 changed files with 288 additions and 178 deletions
|
@ -11,6 +11,7 @@ using DSharpPlus.Commands.Processors.SlashCommands;
|
||||||
using DSharpPlus.Entities;
|
using DSharpPlus.Entities;
|
||||||
using DSharpPlus.Exceptions;
|
using DSharpPlus.Exceptions;
|
||||||
using Newtonsoft.Json;
|
using Newtonsoft.Json;
|
||||||
|
using Newtonsoft.Json.Schema;
|
||||||
using SupportChild.Interviews;
|
using SupportChild.Interviews;
|
||||||
|
|
||||||
namespace SupportChild.Commands;
|
namespace SupportChild.Commands;
|
||||||
|
@ -19,6 +20,8 @@ namespace SupportChild.Commands;
|
||||||
[Description("Administrative commands.")]
|
[Description("Administrative commands.")]
|
||||||
public class InterviewTemplateCommands
|
public class InterviewTemplateCommands
|
||||||
{
|
{
|
||||||
|
private static string jsonSchema = Utilities.ReadManifestData("Interviews.interview_template.schema.json");
|
||||||
|
|
||||||
[RequireGuild]
|
[RequireGuild]
|
||||||
[Command("get")]
|
[Command("get")]
|
||||||
[Description("Provides a copy of the interview template for a category which you can edit and then reupload.")]
|
[Description("Provides a copy of the interview template for a category which you can edit and then reupload.")]
|
||||||
|
@ -96,35 +99,20 @@ public class InterviewTemplateCommands
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
Stream stream = await new HttpClient().GetStreamAsync(file.Url);
|
|
||||||
string json = await new StreamReader(stream).ReadToEndAsync();
|
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
List<string> errors = [];
|
List<string> errors = [];
|
||||||
|
|
||||||
// Convert it to an interview object to validate the template
|
Stream stream = await new HttpClient().GetStreamAsync(file.Url);
|
||||||
Interviews.ValidatedTemplate template = JsonConvert.DeserializeObject<Interviews.ValidatedTemplate>(json, new JsonSerializerSettings()
|
JSchemaValidatingReader validatingReader = new(new JsonTextReader(new StreamReader(stream)));
|
||||||
{
|
validatingReader.Schema = JSchema.Parse(jsonSchema);
|
||||||
//NullValueHandling = NullValueHandling.Include,
|
|
||||||
MissingMemberHandling = MissingMemberHandling.Error,
|
|
||||||
Error = delegate (object sender, Newtonsoft.Json.Serialization.ErrorEventArgs args)
|
|
||||||
{
|
|
||||||
// I noticed the main exception mainly has information for developers, not administrators,
|
|
||||||
// so I switched to using the inner message if available.
|
|
||||||
if (string.IsNullOrEmpty(args.ErrorContext.Error.InnerException?.Message))
|
|
||||||
{
|
|
||||||
errors.Add(args.ErrorContext.Error.Message);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
errors.Add(args.ErrorContext.Error.InnerException.Message);
|
|
||||||
}
|
|
||||||
|
|
||||||
Logger.Debug("Exception occured when trying to upload interview template:\n" + args.ErrorContext.Error);
|
// The schema seems to throw an additional error with incorrect information if an invalid parameter is included
|
||||||
args.ErrorContext.Handled = false;
|
// in the template. Throw here in order to only show the first correct error to the user, also skips unnecessary validation further down.
|
||||||
}
|
validatingReader.ValidationEventHandler += (o, a) => throw new JsonException(a.Message);
|
||||||
});
|
|
||||||
|
JsonSerializer serializer = new();
|
||||||
|
Template template = serializer.Deserialize<Template>(validatingReader);
|
||||||
|
|
||||||
DiscordChannel category = await SupportChild.client.GetChannelAsync(template.categoryID);
|
DiscordChannel category = await SupportChild.client.GetChannelAsync(template.categoryID);
|
||||||
if (!category.IsCategory)
|
if (!category.IsCategory)
|
||||||
|
@ -170,11 +158,7 @@ public class InterviewTemplateCommands
|
||||||
await command.RespondAsync(new DiscordEmbedBuilder
|
await command.RespondAsync(new DiscordEmbedBuilder
|
||||||
{
|
{
|
||||||
Color = DiscordColor.Red,
|
Color = DiscordColor.Red,
|
||||||
Description = "The uploaded JSON structure could not be converted to an interview template.\n\nErrors:\n```\n" + errorString + "\n```",
|
Description = "The uploaded JSON structure could not be parsed as an interview template.\n\nErrors:\n```\n" + errorString + "\n```"
|
||||||
Footer = new DiscordEmbedBuilder.EmbedFooter()
|
|
||||||
{
|
|
||||||
Text = "More detailed information may be available as debug messages in the bot logs."
|
|
||||||
}
|
|
||||||
}, true);
|
}, true);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -208,11 +192,15 @@ public class InterviewTemplateCommands
|
||||||
}
|
}
|
||||||
catch (Exception e)
|
catch (Exception e)
|
||||||
{
|
{
|
||||||
|
Logger.Debug("Exception occured when trying to upload interview template:\n", e);
|
||||||
await command.RespondAsync(new DiscordEmbedBuilder
|
await command.RespondAsync(new DiscordEmbedBuilder
|
||||||
{
|
{
|
||||||
|
|
||||||
Color = DiscordColor.Red,
|
Color = DiscordColor.Red,
|
||||||
Description = "The uploaded JSON structure could not be converted to an interview template.\n\nError message:\n```\n" + e.Message + "\n```"
|
Description = "The uploaded JSON structure could not be parsed as an interview template.\n\nError message:\n```\n" + e.Message + "\n```",
|
||||||
|
Footer = new DiscordEmbedBuilder.EmbedFooter
|
||||||
|
{
|
||||||
|
Text = "More detailed information may be available as debug messages in the bot logs."
|
||||||
|
}
|
||||||
}, true);
|
}, true);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,6 +6,7 @@ using DSharpPlus;
|
||||||
using MySqlConnector;
|
using MySqlConnector;
|
||||||
using Newtonsoft.Json;
|
using Newtonsoft.Json;
|
||||||
using Newtonsoft.Json.Serialization;
|
using Newtonsoft.Json.Serialization;
|
||||||
|
using SupportChild.Interviews;
|
||||||
|
|
||||||
namespace SupportChild;
|
namespace SupportChild;
|
||||||
|
|
||||||
|
@ -797,7 +798,7 @@ public static class Database
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static bool SetInterviewTemplate(Interviews.ValidatedTemplate template)
|
public static bool SetInterviewTemplate(Interviews.Template template)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
@ -805,7 +806,8 @@ public static class Database
|
||||||
{
|
{
|
||||||
NullValueHandling = NullValueHandling.Ignore,
|
NullValueHandling = NullValueHandling.Ignore,
|
||||||
MissingMemberHandling = MissingMemberHandling.Error,
|
MissingMemberHandling = MissingMemberHandling.Error,
|
||||||
Formatting = Formatting.Indented
|
Formatting = Formatting.Indented,
|
||||||
|
ContractResolver = new InterviewQuestion.StripInternalPropertiesResolver()
|
||||||
});
|
});
|
||||||
|
|
||||||
string query;
|
string query;
|
||||||
|
|
|
@ -1,15 +1,18 @@
|
||||||
using System.Collections.Generic;
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
using System.Collections.Specialized;
|
using System.Collections.Specialized;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
using System.Reflection;
|
||||||
using DSharpPlus.Entities;
|
using DSharpPlus.Entities;
|
||||||
using Newtonsoft.Json;
|
using Newtonsoft.Json;
|
||||||
using Newtonsoft.Json.Converters;
|
using Newtonsoft.Json.Converters;
|
||||||
|
using Newtonsoft.Json.Serialization;
|
||||||
|
|
||||||
namespace SupportChild.Interviews;
|
namespace SupportChild.Interviews;
|
||||||
|
|
||||||
public enum QuestionType
|
public enum QuestionType
|
||||||
{
|
{
|
||||||
// Support multiselector as separate type, with only one subpath supported
|
// TODO: Support multiselector as separate type, with only one subpath supported
|
||||||
ERROR,
|
ERROR,
|
||||||
END_WITH_SUMMARY,
|
END_WITH_SUMMARY,
|
||||||
END_WITHOUT_SUMMARY,
|
END_WITHOUT_SUMMARY,
|
||||||
|
@ -46,7 +49,7 @@ public class InterviewQuestion
|
||||||
|
|
||||||
// The type of question.
|
// The type of question.
|
||||||
[JsonConverter(typeof(StringEnumConverter))]
|
[JsonConverter(typeof(StringEnumConverter))]
|
||||||
[JsonProperty("type")]
|
[JsonProperty("message-type")]
|
||||||
public QuestionType type;
|
public QuestionType type;
|
||||||
|
|
||||||
// Colour of the message embed.
|
// Colour of the message embed.
|
||||||
|
@ -192,6 +195,106 @@ public class InterviewQuestion
|
||||||
_ => DiscordButtonStyle.Secondary
|
_ => DiscordButtonStyle.Secondary
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 = [];
|
||||||
|
Dictionary<string, int> childMaxLengths = new Dictionary<string, int>();
|
||||||
|
foreach (KeyValuePair<string, InterviewQuestion> 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 += Math.Min(maxLength ?? 1024, 1024);
|
||||||
|
break;
|
||||||
|
case QuestionType.END_WITH_SUMMARY:
|
||||||
|
case QuestionType.END_WITHOUT_SUMMARY:
|
||||||
|
case QuestionType.ERROR:
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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, InterviewQuestion interview)
|
public class Template(ulong categoryID, InterviewQuestion interview)
|
||||||
|
|
|
@ -1,137 +0,0 @@
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
|
||||||
using Newtonsoft.Json;
|
|
||||||
using Newtonsoft.Json.Converters;
|
|
||||||
using SupportChild.Interviews;
|
|
||||||
|
|
||||||
namespace SupportChild.Interviews;
|
|
||||||
|
|
||||||
// This class is identical to the normal interview question 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 = [];
|
|
||||||
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 += Math.Min(maxLength ?? 1024, 1024);
|
|
||||||
break;
|
|
||||||
case QuestionType.END_WITH_SUMMARY:
|
|
||||||
case QuestionType.END_WITHOUT_SUMMARY:
|
|
||||||
case QuestionType.ERROR:
|
|
||||||
default:
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public class ValidatedTemplate(ulong categoryID, ValidatedInterviewQuestion interview)
|
|
||||||
{
|
|
||||||
[JsonProperty("category-id", Required = Required.Always)]
|
|
||||||
public ulong categoryID = categoryID;
|
|
||||||
|
|
||||||
[JsonProperty("interview", Required = Required.Always)]
|
|
||||||
public ValidatedInterviewQuestion interview = interview;
|
|
||||||
}
|
|
152
Interviews/interview_template.schema.json
Normal file
152
Interviews/interview_template.schema.json
Normal file
|
@ -0,0 +1,152 @@
|
||||||
|
{
|
||||||
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||||
|
"id": "https://toastielab.dev/Emotions-stuff/SupportChild/src/branch/main/Interviews/interview_template.schema.json",
|
||||||
|
"title": "Interview Template",
|
||||||
|
"description": "An interview dialog tree template for SupportChild",
|
||||||
|
"definitions": {
|
||||||
|
"step": {
|
||||||
|
"type": "object",
|
||||||
|
"title": "Interview",
|
||||||
|
"description": "Contains the interview dialog tree.",
|
||||||
|
"properties": {
|
||||||
|
"heading": {
|
||||||
|
"type": "string",
|
||||||
|
"title": "Heading",
|
||||||
|
"description": "The title of the message.",
|
||||||
|
"minLength": 1
|
||||||
|
},
|
||||||
|
"message": {
|
||||||
|
"type": "string",
|
||||||
|
"title": "Message",
|
||||||
|
"description": "The message text.",
|
||||||
|
"minLength": 1
|
||||||
|
},
|
||||||
|
"message-type": {
|
||||||
|
"type": "string",
|
||||||
|
"title": "Message Type",
|
||||||
|
"description": "The type of message, decides what the bot will do when the user gets to this step.",
|
||||||
|
"enum": [
|
||||||
|
"ERROR",
|
||||||
|
"END_WITH_SUMMARY",
|
||||||
|
"END_WITHOUT_SUMMARY",
|
||||||
|
"BUTTONS",
|
||||||
|
"TEXT_SELECTOR",
|
||||||
|
"USER_SELECTOR",
|
||||||
|
"ROLE_SELECTOR",
|
||||||
|
"MENTIONABLE_SELECTOR",
|
||||||
|
"CHANNEL_SELECTOR",
|
||||||
|
"TEXT_INPUT"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"color": {
|
||||||
|
"type": "string",
|
||||||
|
"title": "Color",
|
||||||
|
"description": "Colour of the embed message.",
|
||||||
|
"examples": [
|
||||||
|
"BLACK",
|
||||||
|
"WHITE",
|
||||||
|
"GRAY",
|
||||||
|
"DARKGRAY",
|
||||||
|
"LIGHTGRAY",
|
||||||
|
"VERYDARKGRAY",
|
||||||
|
"BLURPLE",
|
||||||
|
"GRAYPLE",
|
||||||
|
"DARKBUTNOTBLACK",
|
||||||
|
"NOTQUITEBLACK",
|
||||||
|
"RED",
|
||||||
|
"DARKRED",
|
||||||
|
"GREEN",
|
||||||
|
"DARKGREEN",
|
||||||
|
"BLUE",
|
||||||
|
"DARKBLUE",
|
||||||
|
"YELLOW",
|
||||||
|
"CYAN",
|
||||||
|
"MAGENTA",
|
||||||
|
"TEAL",
|
||||||
|
"AQUAMARINE",
|
||||||
|
"GOLD",
|
||||||
|
"GOLDENROD",
|
||||||
|
"AZURE",
|
||||||
|
"ROSE",
|
||||||
|
"SPRINGGREEN",
|
||||||
|
"CHARTREUSE",
|
||||||
|
"ORANGE",
|
||||||
|
"PURPLE",
|
||||||
|
"VIOLET",
|
||||||
|
"BROWN",
|
||||||
|
"HOTPINK",
|
||||||
|
"LILAC",
|
||||||
|
"CORNFLOWERBLUE",
|
||||||
|
"MIDNIGHTBLUE",
|
||||||
|
"WHEAT",
|
||||||
|
"INDIANRED",
|
||||||
|
"TURQUOISE",
|
||||||
|
"SAPGREEN",
|
||||||
|
"PHTHALOBLUE",
|
||||||
|
"PHTHALOGREEN",
|
||||||
|
"SIENNA"
|
||||||
|
],
|
||||||
|
"minLength": 1
|
||||||
|
},
|
||||||
|
"paths": {
|
||||||
|
"type": "object",
|
||||||
|
"title": "Paths",
|
||||||
|
"description": "The possible next steps, the name of the path is matched against the user input, use \".*\" to match anything.",
|
||||||
|
"patternProperties": {
|
||||||
|
".*": {
|
||||||
|
"$ref": "#/definitions/step"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"summary-field": {
|
||||||
|
"type": "string",
|
||||||
|
"title": "Summary Field",
|
||||||
|
"description": "If this interview ends with a summary this will be the name of the answer the user used in this field.",
|
||||||
|
"minLength": 1
|
||||||
|
},
|
||||||
|
"button-style": {
|
||||||
|
"type": "string",
|
||||||
|
"title": "Button Style",
|
||||||
|
"description": "The style of the button that leads to this step, the step before this one must be a button step for this to work.",
|
||||||
|
"enum": [
|
||||||
|
"PRIMARY",
|
||||||
|
"SECONDARY",
|
||||||
|
"SUCCESS",
|
||||||
|
"DANGER"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"selector-description": {
|
||||||
|
"type": "string",
|
||||||
|
"title": "Selector Description",
|
||||||
|
"description": "The description of the selector.",
|
||||||
|
"minLength": 1
|
||||||
|
},
|
||||||
|
"max-length": {
|
||||||
|
"type": "number",
|
||||||
|
"title": "Max Length",
|
||||||
|
"description": "The maximum length of the text input."
|
||||||
|
},
|
||||||
|
"min-length": {
|
||||||
|
"type": "number",
|
||||||
|
"title": "Min Length",
|
||||||
|
"description": "The minimum length of the text input."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [ "message", "message-type", "color", "paths" ],
|
||||||
|
"unevaluatedProperties": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"category-id": {
|
||||||
|
"type": "string",
|
||||||
|
"title": "Category ID",
|
||||||
|
"description": "The category the template applies to. Change this to apply the template to a different category."
|
||||||
|
},
|
||||||
|
"interview": {
|
||||||
|
"$ref": "#/definitions/step"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [ "category-id", "interview" ],
|
||||||
|
"unevaluatedProperties": false
|
||||||
|
}
|
|
@ -42,6 +42,7 @@
|
||||||
<PackageReference Include="JsonExtensions" Version="1.2.0" />
|
<PackageReference Include="JsonExtensions" Version="1.2.0" />
|
||||||
<PackageReference Include="MySqlConnector" Version="2.3.7" />
|
<PackageReference Include="MySqlConnector" Version="2.3.7" />
|
||||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
|
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
|
||||||
|
<PackageReference Include="Newtonsoft.Json.Schema" Version="4.0.1" />
|
||||||
<PackageReference Include="Polly" Version="8.4.2" />
|
<PackageReference Include="Polly" Version="8.4.2" />
|
||||||
<PackageReference Include="RazorBlade" Version="0.6.0" />
|
<PackageReference Include="RazorBlade" Version="0.6.0" />
|
||||||
<PackageReference Include="Superpower" Version="3.0.0" />
|
<PackageReference Include="Superpower" Version="3.0.0" />
|
||||||
|
@ -49,15 +50,16 @@
|
||||||
<PackageReference Include="YamlDotNet" Version="16.1.3" />
|
<PackageReference Include="YamlDotNet" Version="16.1.3" />
|
||||||
<PackageReference Include="YoutubeExplode" Version="6.4.3" />
|
<PackageReference Include="YoutubeExplode" Version="6.4.3" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
|
||||||
<Folder Include="lib\" />
|
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<Reference Include="DiscordChatExporter.Core">
|
<Reference Include="DiscordChatExporter.Core">
|
||||||
<HintPath>lib\DiscordChatExporter.Core.dll</HintPath>
|
<HintPath>lib/DiscordChatExporter.Core.dll</HintPath>
|
||||||
</Reference>
|
</Reference>
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<EmbeddedResource Include="default_config.yml" />
|
||||||
|
<EmbeddedResource Include="Interviews/interview_template.schema.json" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
Loading…
Reference in a new issue