Added optional properties to interview questions

This commit is contained in:
Toastie 2024-12-26 20:32:34 +13:00
parent a8d7440a7a
commit 1ced350789
Signed by: toastie_t0ast
GPG key ID: 27F3B6855AFD40A4

View file

@ -25,13 +25,26 @@ public static class Interviewer
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
{
// TODO: Optional properties: embed title, button style
// TODO: Other selector types.
// Title of the message embed.
[JsonProperty("title")]
public string title;
// Message contents sent to the user.
[JsonProperty("message")]
@ -50,7 +63,18 @@ public static class Interviewer
[JsonProperty("summary-field")]
public string summaryField;
// Possible questions to ask next, or DONE/FAIL type in order to finish interview.
// If this question is on a button, give it this style.
[JsonConverter(typeof(StringEnumConverter))]
[JsonProperty("button-style")]
public ButtonType buttonStyle;
[JsonProperty("max-length")]
public int maxLength;
[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;
@ -147,6 +171,18 @@ public static class Interviewer
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
@ -154,24 +190,32 @@ public static class Interviewer
// I might do this in a more proper way at some point.
public class ValidatedInterviewQuestion
{
// Message contents sent to the user.
[JsonProperty("title", Required = Required.Default)]
public string title;
[JsonProperty("message", Required = Required.Always)]
public string message;
// The type of question.
[JsonConverter(typeof(StringEnumConverter))]
[JsonProperty("type", Required = Required.Always)]
public QuestionType type;
// Colour of the message embed.
[JsonProperty("color", Required = Required.Always)]
public string color;
// Used as label for this question in the post-interview summary.
[JsonProperty("summary-field", Required = Required.Always)]
[JsonProperty("summary-field", Required = Required.Default)]
public string summaryField;
// Possible questions to ask next, or DONE/FAIL type in order to finish interview.
[JsonConverter(typeof(StringEnumConverter))]
[JsonProperty("button-style", Required = Required.Default)]
public ButtonType buttonStyle;
[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;
}
@ -250,6 +294,8 @@ public static class Interviewer
return;
}
await interaction.CreateResponseAsync(DiscordInteractionResponseType.UpdateMessage);
// Parse the response index from the button/selector.
string componentID = "";
@ -265,38 +311,33 @@ public static class Interviewer
throw new ArgumentOutOfRangeException();
}
if (!int.TryParse(componentID, out int pathIndex))
{
Logger.Error("Invalid interview button/selector index: " + componentID);
await interaction.CreateResponseAsync(DiscordInteractionResponseType.UpdateMessage);
return;
}
if (pathIndex >= currentQuestion.paths.Count || pathIndex < 0)
{
Logger.Error("Invalid interview button/selector index: " + pathIndex);
await interaction.CreateResponseAsync(DiscordInteractionResponseType.UpdateMessage);
return;
}
await interaction.CreateResponseAsync(DiscordInteractionResponseType.UpdateMessage);
(string questionString, InterviewQuestion nextQuestion) = currentQuestion.paths.ElementAt(pathIndex);
await HandleAnswer(questionString, nextQuestion, interviewRoot, currentQuestion, interaction.Channel);
}
public static async Task ProcessResponseMessage(DiscordMessage message)
public static async Task ProcessResponseMessage(DiscordMessage answerMessage)
{
// Either the message or the referenced message is null.
if (message.Channel == null || message.ReferencedMessage?.Channel == null)
if (answerMessage.Channel == null || answerMessage.ReferencedMessage?.Channel == null)
{
return;
}
// The channel does not have an active interview.
if (!activeInterviews.TryGetValue(message.ReferencedMessage.Channel.Id, out InterviewQuestion interviewRoot))
if (!activeInterviews.TryGetValue(answerMessage.ReferencedMessage.Channel.Id, out InterviewQuestion interviewRoot))
{
return;
}
@ -307,7 +348,7 @@ public static class Interviewer
}
// The user responded to something other than the latest interview question.
if (message.ReferencedMessage.Id != currentQuestion.messageID)
if (answerMessage.ReferencedMessage.Id != currentQuestion.messageID)
{
return;
}
@ -318,21 +359,47 @@ public static class Interviewer
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(message.Content, questionString)) continue;
if (!Regex.IsMatch(answerMessage.Content, questionString)) continue;
await HandleAnswer(questionString, nextQuestion, interviewRoot, currentQuestion, message.Channel, message);
await HandleAnswer(questionString, nextQuestion, interviewRoot, currentQuestion, answerMessage.Channel, answerMessage);
return;
}
// TODO: Make message configurable.
await message.RespondAsync(new DiscordEmbedBuilder
DiscordMessage errorMessage = await answerMessage.RespondAsync(new DiscordEmbedBuilder
{
Description = "Error: Could not determine the next question based on your answer.",
Color = DiscordColor.Red
});
currentQuestion.AddRelatedMessageIDs(errorMessage.Id);
}
private static async Task HandleAnswer(string questionString,
@ -379,7 +446,7 @@ public static class Interviewer
DiscordEmbedBuilder embed = new DiscordEmbedBuilder()
{
Color = Utilities.StringToColor(nextQuestion.color),
Title = "Summary:", // TODO: Set title
Title = nextQuestion.title,
Description = nextQuestion.message,
};
@ -402,6 +469,7 @@ public static class Interviewer
await channel.SendMessageAsync(new DiscordEmbedBuilder()
{
Color = Utilities.StringToColor(nextQuestion.color),
Title = nextQuestion.title,
Description = nextQuestion.message
});
@ -419,6 +487,7 @@ public static class Interviewer
DiscordMessage errorMessage = await channel.SendMessageAsync(new DiscordEmbedBuilder()
{
Color = Utilities.StringToColor(nextQuestion.color),
Title = nextQuestion.title,
Description = nextQuestion.message
});
previousQuestion.AddRelatedMessageIDs(errorMessage.Id);
@ -429,6 +498,7 @@ public static class Interviewer
.AddEmbed(new DiscordEmbedBuilder()
{
Color = Utilities.StringToColor(nextQuestion.color),
Title = nextQuestion.title,
Description = nextQuestion.message
}).WithReply(answerMessage.Id);
DiscordMessage errorMessage = await answerMessage.RespondAsync(errorMessageBuilder);
@ -463,6 +533,7 @@ public static class Interviewer
DiscordEmbedBuilder embed = new DiscordEmbedBuilder()
{
Color = Utilities.StringToColor(question.color),
Title = question.title,
Description = question.message
};
@ -475,7 +546,8 @@ public static class Interviewer
List<DiscordButtonComponent> buttonRow = [];
for (; nrOfButtons < 5 * (nrOfButtonRows + 1) && nrOfButtons < question.paths.Count; nrOfButtons++)
{
buttonRow.Add(new DiscordButtonComponent(DiscordButtonStyle.Primary, "supportchild_interviewbutton " + nrOfButtons, question.paths.ToArray()[nrOfButtons].Key));
(string questionString, InterviewQuestion nextQuestion) = question.paths.ToArray()[nrOfButtons];
buttonRow.Add(new DiscordButtonComponent(nextQuestion.GetButtonStyle(), "supportchild_interviewbutton " + nrOfButtons, questionString));
}
msgBuilder.AddComponents(buttonRow);
}