Added support for END_WITHOUT_SUMMARY and ERROR question types

This commit is contained in:
Toastie 2024-12-26 20:28:39 +13:00
parent ebd279da45
commit 91991c10f2
Signed by: toastie_t0ast
GPG key ID: 27F3B6855AFD40A4
2 changed files with 110 additions and 60 deletions

View file

@ -862,7 +862,7 @@ public static class Database
c.Open(); c.Open();
using MySqlCommand cmd = new MySqlCommand(query, c); using MySqlCommand cmd = new MySqlCommand(query, c);
cmd.Parameters.AddWithValue("@channel_id", channelID); cmd.Parameters.AddWithValue("@channel_id", channelID);
cmd.Parameters.AddWithValue("@interview", JsonConvert.SerializeObject(interview)); cmd.Parameters.AddWithValue("@interview", JsonConvert.SerializeObject(interview, Formatting.Indented));
cmd.Prepare(); cmd.Prepare();
return cmd.ExecuteNonQuery() > 0; return cmd.ExecuteNonQuery() > 0;
} }

View file

@ -2,6 +2,7 @@
using System.Collections; using System.Collections;
using System.Collections.Generic; using System.Collections.Generic;
using System.Collections.Specialized; using System.Collections.Specialized;
using System.Diagnostics.CodeAnalysis;
using System.Linq; using System.Linq;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
using System.Threading.Tasks; using System.Threading.Tasks;
@ -17,8 +18,8 @@ public static class Interviewer
public enum QuestionType public enum QuestionType
{ {
ERROR, ERROR,
CANCEL, END_WITH_SUMMARY,
DONE, END_WITHOUT_SUMMARY,
BUTTONS, BUTTONS,
TEXT_SELECTOR, TEXT_SELECTOR,
TEXT_INPUT TEXT_INPUT
@ -53,7 +54,9 @@ public static class Interviewer
[JsonProperty("paths")] [JsonProperty("paths")]
public Dictionary<string, InterviewQuestion> paths; public Dictionary<string, InterviewQuestion> paths;
// The following parameters are populated by the bot, not the json template. // ////////////////////////////////////////////////////////////////////////////
// The following parameters are populated by the bot, not the json template. //
// ////////////////////////////////////////////////////////////////////////////
// The ID of this message where the bot asked this question. // The ID of this message where the bot asked this question.
[JsonProperty("message-id")] [JsonProperty("message-id")]
@ -67,6 +70,10 @@ public static class Interviewer
[JsonProperty("answer-id")] [JsonProperty("answer-id")]
public ulong answerID; 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 TryGetCurrentQuestion(out InterviewQuestion question) public bool TryGetCurrentQuestion(out InterviewQuestion question)
{ {
// This object has not been initialized, we have checked too deep. // This object has not been initialized, we have checked too deep.
@ -117,12 +124,29 @@ public static class Interviewer
messageIDs.Add(answerID); messageIDs.Add(answerID);
} }
if (relatedMessageIDs != null)
{
messageIDs.AddRange(relatedMessageIDs);
}
// This will always contain exactly one or zero children. // This will always contain exactly one or zero children.
foreach (KeyValuePair<string, InterviewQuestion> path in paths) foreach (KeyValuePair<string, InterviewQuestion> path in paths)
{ {
path.Value.GetMessageIDs(ref messageIDs); path.Value.GetMessageIDs(ref messageIDs);
} }
} }
public void AddRelatedMessageIDs(params ulong[] messageIDs)
{
if (relatedMessageIDs == null)
{
relatedMessageIDs = messageIDs.ToList();
}
else
{
relatedMessageIDs.AddRange(messageIDs);
}
}
} }
// This class is identical to the one above and just exists as a hack to get JSON validation when // This class is identical to the one above and just exists as a hack to get JSON validation when
@ -178,11 +202,6 @@ public static class Interviewer
} }
} }
public static bool IsInterviewActive(ulong channelID)
{
return activeInterviews.ContainsKey(channelID);
}
public static async Task ProcessButtonOrSelectorResponse(DiscordInteraction interaction) public static async Task ProcessButtonOrSelectorResponse(DiscordInteraction interaction)
{ {
// TODO: Add error responses. // TODO: Add error responses.
@ -192,7 +211,7 @@ public static class Interviewer
return; return;
} }
// The user selected nothing. // Return if the user didn't select anything
if (interaction.Data.ComponentType == DiscordComponentType.StringSelect && interaction.Data.Values.Length == 0) if (interaction.Data.ComponentType == DiscordComponentType.StringSelect && interaction.Data.Values.Length == 0)
{ {
return; return;
@ -200,25 +219,25 @@ public static class Interviewer
await interaction.CreateResponseAsync(DiscordInteractionResponseType.DeferredMessageUpdate); await interaction.CreateResponseAsync(DiscordInteractionResponseType.DeferredMessageUpdate);
// Could not find active interview. // Return if there is no active interview in this channel
if (!activeInterviews.TryGetValue(interaction.Channel.Id, out InterviewQuestion interviewRoot)) if (!activeInterviews.TryGetValue(interaction.Channel.Id, out InterviewQuestion interviewRoot))
{ {
return; return;
} }
// Could not find message id in interview. // Return if the current question cannot be found in the interview.
if (!interviewRoot.TryGetCurrentQuestion(out InterviewQuestion currentQuestion)) if (!interviewRoot.TryGetCurrentQuestion(out InterviewQuestion currentQuestion))
{ {
return; return;
} }
// This button is for an older question. // Check if this button/selector is for an older question.
if (interaction.Message.Id != currentQuestion.messageID) if (interaction.Message.Id != currentQuestion.messageID)
{ {
return; return;
} }
// Parse the response index from the button. // Parse the response index from the button/selector.
string componentID = ""; string componentID = "";
switch (interaction.Data.ComponentType) switch (interaction.Data.ComponentType)
@ -249,10 +268,6 @@ public static class Interviewer
(string questionString, InterviewQuestion nextQuestion) = currentQuestion.paths.ElementAt(pathIndex); (string questionString, InterviewQuestion nextQuestion) = currentQuestion.paths.ElementAt(pathIndex);
await HandleAnswer(questionString, nextQuestion, interviewRoot, currentQuestion, interaction.Channel); await HandleAnswer(questionString, nextQuestion, interviewRoot, currentQuestion, interaction.Channel);
// Edit message to remove buttons/selectors.
// TODO: Add footer with answer.
await interaction.EditOriginalResponseAsync(new DiscordWebhookBuilder().AddEmbed(interaction.Message.Embeds[0]));
} }
public static async Task ProcessResponseMessage(DiscordMessage message) public static async Task ProcessResponseMessage(DiscordMessage message)
@ -304,25 +319,28 @@ public static class Interviewer
InterviewQuestion interviewRoot, InterviewQuestion interviewRoot,
InterviewQuestion previousQuestion, InterviewQuestion previousQuestion,
DiscordChannel channel, DiscordChannel channel,
DiscordMessage message = null) DiscordMessage answerMessage = null)
{ {
// The answer was provided using a button or selector if (nextQuestion.type != QuestionType.ERROR)
if (message == null)
{ {
previousQuestion.answer = questionString; // The answer was provided using a button or selector
previousQuestion.answerID = 0; if (answerMessage == null)
} {
else previousQuestion.answer = questionString;
{ previousQuestion.answerID = 0;
previousQuestion.answer = message.Content; }
previousQuestion.answerID = message.Id; else
} {
previousQuestion.answer = answerMessage.Content;
previousQuestion.answerID = answerMessage.Id;
}
// Remove any other paths from the previous question. // Remove any other paths from the previous question.
previousQuestion.paths = new Dictionary<string, InterviewQuestion> previousQuestion.paths = new Dictionary<string, InterviewQuestion>
{ {
{ questionString, nextQuestion } { questionString, nextQuestion }
}; };
}
// Create next question, or finish the interview. // Create next question, or finish the interview.
switch (nextQuestion.type) switch (nextQuestion.type)
@ -333,14 +351,14 @@ public static class Interviewer
await CreateQuestion(channel, nextQuestion); await CreateQuestion(channel, nextQuestion);
Database.SaveInterview(channel.Id, interviewRoot); Database.SaveInterview(channel.Id, interviewRoot);
break; break;
case QuestionType.DONE: case QuestionType.END_WITH_SUMMARY:
OrderedDictionary summaryFields = new OrderedDictionary(); OrderedDictionary summaryFields = new OrderedDictionary();
interviewRoot.GetSummary(ref summaryFields); interviewRoot.GetSummary(ref summaryFields);
DiscordEmbedBuilder embed = new DiscordEmbedBuilder() DiscordEmbedBuilder embed = new DiscordEmbedBuilder()
{ {
Color = Utilities.StringToColor(nextQuestion.color), Color = Utilities.StringToColor(nextQuestion.color),
Title = "Summary:", Title = "Summary:", // TODO: Set title
Description = nextQuestion.message, Description = nextQuestion.message,
}; };
@ -351,41 +369,73 @@ public static class Interviewer
await channel.SendMessageAsync(embed); await channel.SendMessageAsync(embed);
List<ulong> previousMessages = new List<ulong> { }; await DeletePreviousMessages(interviewRoot, channel);
interviewRoot.GetMessageIDs(ref previousMessages);
foreach (ulong previousMessageID in previousMessages)
{
try
{
Logger.Debug("Deleting message: " + previousMessageID);
DiscordMessage previousMessage = await channel.GetMessageAsync(previousMessageID);
await channel.DeleteMessageAsync(previousMessage, "Deleting old interview message.");
}
catch (Exception e)
{
Logger.Error("Failed to delete old interview message: " + e.Message);
}
}
if (!Database.TryDeleteInterview(channel.Id)) if (!Database.TryDeleteInterview(channel.Id))
{ {
Logger.Error("Could not delete interview from database. Channel ID: " + channel.Id); Logger.Error("Could not delete interview from database. Channel ID: " + channel.Id);
} }
Reload(); Reload();
return; return;
case QuestionType.CANCEL: case QuestionType.END_WITHOUT_SUMMARY:
// TODO: Post fail message. // TODO: Add command to restart interview.
// TODO: Remove active interview. await channel.SendMessageAsync(new DiscordEmbedBuilder()
{
Color = Utilities.StringToColor(nextQuestion.color),
Description = nextQuestion.message
});
await DeletePreviousMessages(interviewRoot, channel);
if (!Database.TryDeleteInterview(channel.Id))
{
Logger.Error("Could not delete interview from database. Channel ID: " + channel.Id);
}
Reload();
break; break;
case QuestionType.ERROR: case QuestionType.ERROR:
default: default:
// TODO: Post error message. if (answerMessage == null)
{
DiscordMessage errorMessage = await channel.SendMessageAsync(new DiscordEmbedBuilder()
{
Color = Utilities.StringToColor(nextQuestion.color),
Description = nextQuestion.message
});
previousQuestion.AddRelatedMessageIDs(errorMessage.Id);
}
else
{
DiscordMessageBuilder errorMessageBuilder = new DiscordMessageBuilder()
.AddEmbed(new DiscordEmbedBuilder()
{
Color = Utilities.StringToColor(nextQuestion.color),
Description = nextQuestion.message
}).WithReply(answerMessage.Id);
DiscordMessage errorMessage = await answerMessage.RespondAsync(errorMessageBuilder);
previousQuestion.AddRelatedMessageIDs(errorMessage.Id, answerMessage.Id);
}
break; break;
} }
} }
private static async Task DeletePreviousMessages(InterviewQuestion interviewRoot, DiscordChannel channel)
{
List<ulong> previousMessages = new List<ulong> { };
interviewRoot.GetMessageIDs(ref previousMessages);
foreach (ulong previousMessageID in previousMessages)
{
try
{
DiscordMessage previousMessage = await channel.GetMessageAsync(previousMessageID);
await channel.DeleteMessageAsync(previousMessage, "Deleting old interview message.");
}
catch (Exception e)
{
Logger.Warn("Failed to delete old interview message: ", e);
}
}
}
private static async Task CreateQuestion(DiscordChannel channel, InterviewQuestion question) private static async Task CreateQuestion(DiscordChannel channel, InterviewQuestion question)
{ {
DiscordMessageBuilder msgBuilder = new DiscordMessageBuilder(); DiscordMessageBuilder msgBuilder = new DiscordMessageBuilder();
@ -428,8 +478,8 @@ public static class Interviewer
case QuestionType.TEXT_INPUT: case QuestionType.TEXT_INPUT:
embed.WithFooter("Reply to this message with your answer. You cannot include images or files."); embed.WithFooter("Reply to this message with your answer. You cannot include images or files.");
break; break;
case QuestionType.DONE: case QuestionType.END_WITH_SUMMARY:
case QuestionType.CANCEL: case QuestionType.END_WITHOUT_SUMMARY:
case QuestionType.ERROR: case QuestionType.ERROR:
default: default:
break; break;