diff --git a/Database.cs b/Database.cs index 2a0aaf3..9808c63 100644 --- a/Database.cs +++ b/Database.cs @@ -862,7 +862,7 @@ public static class Database c.Open(); using MySqlCommand cmd = new MySqlCommand(query, c); cmd.Parameters.AddWithValue("@channel_id", channelID); - cmd.Parameters.AddWithValue("@interview", JsonConvert.SerializeObject(interview)); + cmd.Parameters.AddWithValue("@interview", JsonConvert.SerializeObject(interview, Formatting.Indented)); cmd.Prepare(); return cmd.ExecuteNonQuery() > 0; } diff --git a/Interviewer.cs b/Interviewer.cs index f304fda..acd7b56 100644 --- a/Interviewer.cs +++ b/Interviewer.cs @@ -2,6 +2,7 @@ using System.Collections; using System.Collections.Generic; using System.Collections.Specialized; +using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Text.RegularExpressions; using System.Threading.Tasks; @@ -17,8 +18,8 @@ public static class Interviewer public enum QuestionType { ERROR, - CANCEL, - DONE, + END_WITH_SUMMARY, + END_WITHOUT_SUMMARY, BUTTONS, TEXT_SELECTOR, TEXT_INPUT @@ -53,7 +54,9 @@ public static class Interviewer [JsonProperty("paths")] public Dictionary 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. [JsonProperty("message-id")] @@ -67,6 +70,10 @@ public static class Interviewer [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 relatedMessageIDs; + public bool TryGetCurrentQuestion(out InterviewQuestion question) { // This object has not been initialized, we have checked too deep. @@ -117,12 +124,29 @@ public static class Interviewer messageIDs.Add(answerID); } + if (relatedMessageIDs != null) + { + messageIDs.AddRange(relatedMessageIDs); + } + // This will always contain exactly one or zero children. foreach (KeyValuePair path in paths) { 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 @@ -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) { // TODO: Add error responses. @@ -192,7 +211,7 @@ public static class Interviewer return; } - // The user selected nothing. + // Return if the user didn't select anything if (interaction.Data.ComponentType == DiscordComponentType.StringSelect && interaction.Data.Values.Length == 0) { return; @@ -200,25 +219,25 @@ public static class Interviewer 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)) { 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)) { return; } - // This button is for an older question. + // Check if this button/selector is for an older question. if (interaction.Message.Id != currentQuestion.messageID) { return; } - // Parse the response index from the button. + // Parse the response index from the button/selector. string componentID = ""; switch (interaction.Data.ComponentType) @@ -249,10 +268,6 @@ public static class Interviewer (string questionString, InterviewQuestion nextQuestion) = currentQuestion.paths.ElementAt(pathIndex); 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) @@ -304,25 +319,28 @@ public static class Interviewer InterviewQuestion interviewRoot, InterviewQuestion previousQuestion, DiscordChannel channel, - DiscordMessage message = null) + DiscordMessage answerMessage = null) { - // The answer was provided using a button or selector - if (message == null) + if (nextQuestion.type != QuestionType.ERROR) { - previousQuestion.answer = questionString; - previousQuestion.answerID = 0; - } - else - { - previousQuestion.answer = message.Content; - previousQuestion.answerID = message.Id; - } + // The answer was provided using a button or selector + if (answerMessage == null) + { + previousQuestion.answer = questionString; + previousQuestion.answerID = 0; + } + else + { + previousQuestion.answer = answerMessage.Content; + previousQuestion.answerID = answerMessage.Id; + } - // Remove any other paths from the previous question. - previousQuestion.paths = new Dictionary - { - { questionString, nextQuestion } - }; + // Remove any other paths from the previous question. + previousQuestion.paths = new Dictionary + { + { questionString, nextQuestion } + }; + } // Create next question, or finish the interview. switch (nextQuestion.type) @@ -333,14 +351,14 @@ public static class Interviewer await CreateQuestion(channel, nextQuestion); Database.SaveInterview(channel.Id, interviewRoot); break; - case QuestionType.DONE: + case QuestionType.END_WITH_SUMMARY: OrderedDictionary summaryFields = new OrderedDictionary(); interviewRoot.GetSummary(ref summaryFields); DiscordEmbedBuilder embed = new DiscordEmbedBuilder() { Color = Utilities.StringToColor(nextQuestion.color), - Title = "Summary:", + Title = "Summary:", // TODO: Set title Description = nextQuestion.message, }; @@ -351,41 +369,73 @@ public static class Interviewer await channel.SendMessageAsync(embed); - List previousMessages = new List { }; - 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); - } - - } - + await DeletePreviousMessages(interviewRoot, channel); if (!Database.TryDeleteInterview(channel.Id)) { Logger.Error("Could not delete interview from database. Channel ID: " + channel.Id); } Reload(); return; - case QuestionType.CANCEL: - // TODO: Post fail message. - // TODO: Remove active interview. + case QuestionType.END_WITHOUT_SUMMARY: + // TODO: Add command to restart 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; case QuestionType.ERROR: 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; } } + private static async Task DeletePreviousMessages(InterviewQuestion interviewRoot, DiscordChannel channel) + { + List previousMessages = new List { }; + 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) { DiscordMessageBuilder msgBuilder = new DiscordMessageBuilder(); @@ -428,8 +478,8 @@ public static class Interviewer case QuestionType.TEXT_INPUT: embed.WithFooter("Reply to this message with your answer. You cannot include images or files."); break; - case QuestionType.DONE: - case QuestionType.CANCEL: + case QuestionType.END_WITH_SUMMARY: + case QuestionType.END_WITHOUT_SUMMARY: case QuestionType.ERROR: default: break;