Added optional properties to interview questions
This commit is contained in:
parent
a8d7440a7a
commit
1ced350789
1 changed files with 94 additions and 22 deletions
116
Interviewer.cs
116
Interviewer.cs
|
@ -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);
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue