From d8e186318294152fc90cfdb2e65d003b7fe7bc62 Mon Sep 17 00:00:00 2001
From: Toastie <toastie@toastiet0ast.com>
Date: Tue, 4 Feb 2025 19:27:20 +1300
Subject: [PATCH] Made it possible to add a summary to any message instead of
 only interview ends

---
 Config.cs                                 |   6 +-
 Interviews/Interview.cs                   |  23 ++---
 Interviews/Interviewer.cs                 | 104 ++++++++++------------
 Interviews/interview_template.schema.json |  23 +++--
 default_config.yml                        |   7 +-
 5 files changed, 75 insertions(+), 88 deletions(-)

diff --git a/Config.cs b/Config.cs
index 80a85cd..8ecd93a 100644
--- a/Config.cs
+++ b/Config.cs
@@ -28,8 +28,7 @@ internal static class Config
     internal static bool closingNotifications = false;
 
     internal static bool interviewsEnabled = false;
-    internal static bool deleteMessagesAfterSummary = false;
-    internal static bool deleteMessagesAfterNoSummary = false;
+    internal static bool deleteMessagesAfterInterviewEnd = false;
 
     internal static string hostName = "127.0.0.1";
     internal static int port = 3306;
@@ -100,8 +99,7 @@ internal static class Config
         closingNotifications = json.SelectToken("notifications.closing")?.Value<bool>() ?? false;
 
         interviewsEnabled = json.SelectToken("interviews.enabled")?.Value<bool>() ?? false;
-        deleteMessagesAfterSummary = json.SelectToken("interviews.delete-messages-after-summary")?.Value<bool>() ?? false;
-        deleteMessagesAfterNoSummary = json.SelectToken("interviews.delete-messages-after-no-summary")?.Value<bool>() ?? false;
+        deleteMessagesAfterInterviewEnd = json.SelectToken("interviews.delete-messages-after-interview-end")?.Value<bool>() ?? false;
 
         // Reads database info
         hostName = json.SelectToken("database.address")?.Value<string>() ?? "";
diff --git a/Interviews/Interview.cs b/Interviews/Interview.cs
index e102ceb..97a8aaa 100644
--- a/Interviews/Interview.cs
+++ b/Interviews/Interview.cs
@@ -15,8 +15,9 @@ public enum MessageType
 {
     // TODO: Support multiselector as separate type
     ERROR,
-    END_WITH_SUMMARY,
-    END_WITHOUT_SUMMARY,
+    INTERVIEW_END = 1,
+    [Obsolete("Use INTERVIEW_END instead")] END_WITH_SUMMARY = 1,
+    [Obsolete("Use INTERVIEW_END instead")] END_WITHOUT_SUMMARY = 1,
     BUTTONS,
     TEXT_SELECTOR,
     USER_SELECTOR,
@@ -125,6 +126,10 @@ public class InterviewStep
     [JsonProperty("min-length")]
     public int? minLength;
 
+    // Adds a summary to the message.
+    [JsonProperty("add-summary")]
+    public bool? addSummary;
+
     // References to steps defined elsewhere in the template
     [JsonProperty("step-references")]
     public Dictionary<string, ReferencedInterviewStep> references = new();
@@ -185,7 +190,7 @@ public class InterviewStep
         }
 
         // This object is the deepest object with a message ID set, meaning it is the latest asked question.
-        previousSteps = new List<InterviewStep> { this };
+        previousSteps = [this];
         return true;
     }
 
@@ -196,7 +201,7 @@ public class InterviewStep
             return;
         }
 
-        if (!string.IsNullOrWhiteSpace(summaryField))
+        if (!string.IsNullOrWhiteSpace(summaryField) && !string.IsNullOrWhiteSpace(answer))
         {
             // TODO: Add option to merge answers
             summary[summaryField] = answer;
@@ -292,8 +297,7 @@ public class InterviewStep
                 case MessageType.TEXT_INPUT:
                     summaryMaxLength += Math.Min(maxLength ?? 1024, 1024);
                     break;
-                case MessageType.END_WITH_SUMMARY:
-                case MessageType.END_WITHOUT_SUMMARY:
+                case MessageType.INTERVIEW_END:
                 case MessageType.ERROR:
                 case MessageType.REFERENCE_END:
                 default:
@@ -317,7 +321,7 @@ public class InterviewStep
             }
         }
 
-        if (messageType is MessageType.ERROR or MessageType.END_WITH_SUMMARY or MessageType.END_WITHOUT_SUMMARY or MessageType.REFERENCE_END)
+        if (messageType is MessageType.ERROR or MessageType.INTERVIEW_END or MessageType.REFERENCE_END)
         {
             if (steps.Count > 0 || references.Count > 0)
             {
@@ -334,7 +338,6 @@ public class InterviewStep
             errors.Add("Steps of the type '" + messageType + "' must have at least one child step.\n\n> " + stepID + ".message-type");
         }
 
-        // TODO: Test this
         foreach (KeyValuePair<string, ReferencedInterviewStep> reference in references)
         {
             if (!reference.Value.TryGetReferencedStep(definitions, out InterviewStep referencedStep, true))
@@ -343,7 +346,7 @@ public class InterviewStep
             }
             else if (reference.Value.afterReferenceStep == null)
             {
-                List<InterviewStep> allChildSteps = new List<InterviewStep>();
+                List<InterviewStep> allChildSteps = [];
                 referencedStep.GetAllSteps(ref allChildSteps);
                 if (allChildSteps.Any(s => s.messageType == MessageType.REFERENCE_END))
                 {
@@ -352,7 +355,7 @@ public class InterviewStep
             }
         }
 
-        if (messageType is MessageType.END_WITH_SUMMARY)
+        if (addSummary ?? false)
         {
             summaryMaxLength += message?.Length ?? 0;
             summaryMaxLength += heading?.Length ?? 0;
diff --git a/Interviews/Interviewer.cs b/Interviews/Interviewer.cs
index 6d091d0..4c25e58 100644
--- a/Interviews/Interviewer.cs
+++ b/Interviews/Interviewer.cs
@@ -19,7 +19,7 @@ public static class Interviewer
             return false;
         }
 
-        await SendNextMessage(channel, interview.interviewRoot);
+        await SendNextMessage(interview, channel, interview.interviewRoot);
         return Database.SaveInterview(interview);
     }
 
@@ -27,7 +27,7 @@ public static class Interviewer
     {
         if (Database.TryGetInterview(channel.Id, out Interview interview))
         {
-            if (Config.deleteMessagesAfterNoSummary)
+            if (Config.deleteMessagesAfterInterviewEnd)
             {
                 await DeletePreviousMessages(interview, channel);
             }
@@ -45,7 +45,7 @@ public static class Interviewer
     {
         if (Database.TryGetInterview(channel.Id, out Interview interview))
         {
-            if (Config.deleteMessagesAfterNoSummary)
+            if (Config.deleteMessagesAfterInterviewEnd)
             {
                 await DeletePreviousMessages(interview, channel);
             }
@@ -362,28 +362,25 @@ public static class Interviewer
                 }
                 nextStep.references.Clear();
 
-                await SendNextMessage(channel, nextStep);
+                await SendNextMessage(interview, channel, nextStep);
                 Database.SaveInterview(interview);
                 break;
-            case MessageType.END_WITH_SUMMARY:
-                OrderedDictionary summaryFields = new OrderedDictionary();
-                interview.interviewRoot.GetSummary(ref summaryFields);
-
-                DiscordEmbedBuilder embed = new DiscordEmbedBuilder
+            case MessageType.INTERVIEW_END:
+                DiscordEmbedBuilder endEmbed = new()
                 {
                     Color = Utilities.StringToColor(nextStep.color),
                     Title = nextStep.heading,
                     Description = nextStep.message,
                 };
 
-                foreach (DictionaryEntry entry in summaryFields)
+                if (nextStep.addSummary ?? false)
                 {
-                    embed.AddField((string)entry.Key, (string)entry.Value ?? string.Empty);
+                    AddSummary(interview, ref endEmbed);
                 }
 
-                await channel.SendMessageAsync(embed);
+                await channel.SendMessageAsync(endEmbed);
 
-                if (Config.deleteMessagesAfterSummary)
+                if (Config.deleteMessagesAfterInterviewEnd)
                 {
                     await DeletePreviousMessages(interview, channel);
                 }
@@ -393,26 +390,7 @@ public static class Interviewer
                     Logger.Error("Could not delete interview from database. Channel ID: " + channel.Id);
                 }
                 return;
-            case MessageType.END_WITHOUT_SUMMARY:
-                await channel.SendMessageAsync(new DiscordEmbedBuilder
-                {
-                    Color = Utilities.StringToColor(nextStep.color),
-                    Title = nextStep.heading,
-                    Description = nextStep.message
-                });
-
-                if (Config.deleteMessagesAfterNoSummary)
-                {
-                    await DeletePreviousMessages(interview, channel);
-                }
-
-                if (!Database.TryDeleteInterview(channel.Id))
-                {
-                    Logger.Error("Could not delete interview from database. Channel ID: " + channel.Id);
-                }
-                break;
             case MessageType.REFERENCE_END:
-                // TODO: What is happening with the summaries?
                 if (interview.interviewRoot.TryGetTakenSteps(out List<InterviewStep> previousSteps))
                 {
                     foreach (InterviewStep step in previousSteps)
@@ -431,19 +409,14 @@ public static class Interviewer
 
                                 previousStep.steps.Clear();
                                 previousStep.steps.Add(answer, nextStep);
-                                await HandleAnswer(answer,
-                                    nextStep,
-                                    interview,
-                                    previousStep,
-                                    channel,
-                                    answerMessage);
+                                await HandleAnswer(answer, nextStep, interview, previousStep, channel, answerMessage);
                                 return;
                             }
                         }
                     }
                 }
 
-                DiscordEmbedBuilder error = new DiscordEmbedBuilder
+                DiscordEmbedBuilder error = new()
                 {
                     Color = DiscordColor.Red,
                     Description = "An error occured while trying to find the next interview step."
@@ -466,26 +439,41 @@ public static class Interviewer
                 return;
             case MessageType.ERROR:
             default:
-                DiscordEmbedBuilder err = new DiscordEmbedBuilder
+                DiscordEmbedBuilder errorEmbed = new()
                 {
                     Color = Utilities.StringToColor(nextStep.color),
                     Title = nextStep.heading,
                     Description = nextStep.message
                 };
 
+                if (nextStep.addSummary ?? false)
+                {
+                    AddSummary(interview, ref errorEmbed);
+                }
+
                 if (answerMessage == null)
                 {
-                    DiscordMessage errorMessage = await channel.SendMessageAsync(err);
+                    DiscordMessage errorMessage = await channel.SendMessageAsync(errorEmbed);
                     previousStep.AddRelatedMessageIDs(errorMessage.Id);
                 }
                 else
                 {
-                    DiscordMessage errorMessage = await answerMessage.RespondAsync(err);
+                    DiscordMessage errorMessage = await answerMessage.RespondAsync(errorEmbed);
                     previousStep.AddRelatedMessageIDs(errorMessage.Id, answerMessage.Id);
                 }
 
                 Database.SaveInterview(interview);
-                break;
+                return;
+        }
+    }
+
+    private static void AddSummary(Interview interview, ref DiscordEmbedBuilder embed)
+    {
+        OrderedDictionary summaryFields = new();
+        interview.interviewRoot.GetSummary(ref summaryFields);
+        foreach (DictionaryEntry entry in summaryFields)
+        {
+            embed.AddField((string)entry.Key, (string)entry.Value ?? "-");
         }
     }
 
@@ -508,7 +496,7 @@ public static class Interviewer
         }
     }
 
-    private static async Task SendNextMessage(DiscordChannel channel, InterviewStep step)
+    private static async Task SendNextMessage(Interview interview, DiscordChannel channel, InterviewStep step)
     {
         DiscordMessageBuilder msgBuilder = new();
         DiscordEmbedBuilder embed = new()
@@ -518,6 +506,11 @@ public static class Interviewer
             Description = step.message
         };
 
+        if (step.addSummary ?? false)
+        {
+            AddSummary(interview, ref embed);
+        }
+
         switch (step.messageType)
         {
             case MessageType.BUTTONS:
@@ -546,33 +539,32 @@ public static class Interviewer
                         categoryOptions.Add(new DiscordSelectComponentOption(stepPattern, selectionOptions.ToString(), nextStep.selectorDescription));
                     }
 
-                    selectionComponents.Add(new DiscordSelectComponent("supportboi_interviewselector " + selectionBoxes, string.IsNullOrWhiteSpace(step.selectorPlaceholder)
-                                                                                                                           ? "Select an option..." : step.selectorPlaceholder, categoryOptions));
+                    selectionComponents.Add(new DiscordSelectComponent("supportboi_interviewselector " + selectionBoxes,
+                                              string.IsNullOrWhiteSpace(step.selectorPlaceholder) ? "Select an option..." : step.selectorPlaceholder, categoryOptions));
                 }
 
                 msgBuilder.AddComponents(selectionComponents);
                 break;
             case MessageType.ROLE_SELECTOR:
-                msgBuilder.AddComponents(new DiscordRoleSelectComponent("supportboi_interviewroleselector", string.IsNullOrWhiteSpace(step.selectorPlaceholder)
-                                                                                                              ? "Select a role..." : step.selectorPlaceholder));
+                msgBuilder.AddComponents(new DiscordRoleSelectComponent("supportboi_interviewroleselector",
+                                           string.IsNullOrWhiteSpace(step.selectorPlaceholder) ? "Select a role..." : step.selectorPlaceholder));
                 break;
             case MessageType.USER_SELECTOR:
-                msgBuilder.AddComponents(new DiscordUserSelectComponent("supportboi_interviewuserselector", string.IsNullOrWhiteSpace(step.selectorPlaceholder)
-                                                                                                              ? "Select a user..." : step.selectorPlaceholder));
+                msgBuilder.AddComponents(new DiscordUserSelectComponent("supportboi_interviewuserselector",
+                                           string.IsNullOrWhiteSpace(step.selectorPlaceholder) ? "Select a user..." : step.selectorPlaceholder));
                 break;
             case MessageType.CHANNEL_SELECTOR:
-                msgBuilder.AddComponents(new DiscordChannelSelectComponent("supportboi_interviewchannelselector", string.IsNullOrWhiteSpace(step.selectorPlaceholder)
-                                                                                                                    ? "Select a channel..." : step.selectorPlaceholder));
+                msgBuilder.AddComponents(new DiscordChannelSelectComponent("supportboi_interviewchannelselector",
+                                           string.IsNullOrWhiteSpace(step.selectorPlaceholder) ? "Select a channel..." : step.selectorPlaceholder));
                 break;
             case MessageType.MENTIONABLE_SELECTOR:
-                msgBuilder.AddComponents(new DiscordMentionableSelectComponent("supportboi_interviewmentionableselector", string.IsNullOrWhiteSpace(step.selectorPlaceholder)
-                                                                                                                            ? "Select a user or role..." : step.selectorPlaceholder));
+                msgBuilder.AddComponents(new DiscordMentionableSelectComponent("supportboi_interviewmentionableselector",
+                                           string.IsNullOrWhiteSpace(step.selectorPlaceholder) ? "Select a user or role..." : step.selectorPlaceholder));
                 break;
             case MessageType.TEXT_INPUT:
                 embed.WithFooter("Reply to this message with your answer. You cannot include images or files.");
                 break;
-            case MessageType.END_WITH_SUMMARY:
-            case MessageType.END_WITHOUT_SUMMARY:
+            case MessageType.INTERVIEW_END:
             case MessageType.ERROR:
             default:
                 break;
diff --git a/Interviews/interview_template.schema.json b/Interviews/interview_template.schema.json
index 995e041..0440bbf 100644
--- a/Interviews/interview_template.schema.json
+++ b/Interviews/interview_template.schema.json
@@ -39,9 +39,7 @@
           "$ref": "#/definitions/step"
         }
       },
-      "required": [
-        "id"
-      ],
+      "required": [ "id" ],
       "unevaluatedProperties": false
     },
     "step": {
@@ -58,7 +56,7 @@
         "message": {
           "type": "string",
           "title": "Message",
-          "description": "The text in the embed message that will be sent to the user when they reach this step.",
+          "description": "The text in the embed message that will be sent to the user when they reach this step. Required for all message types except 'REFERENCE_END'.",
           "minLength": 1
         },
         "message-type": {
@@ -67,8 +65,7 @@
           "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",
+            "INTERVIEW_END",
             "BUTTONS",
             "TEXT_SELECTOR",
             "USER_SELECTOR",
@@ -189,11 +186,14 @@
           "title": "Min Length",
           "description": "The minimum length of the user's response message. Requires that this step is a 'TEXT_INPUT'.",
           "minimum": 0
+        },
+        "add-summary": {
+          "type": "boolean",
+          "title": "Add Summary",
+          "description": "This adds a summary field to the end of the message."
         }
       },
-      "required": [
-        "message-type"
-      ],
+      "required": [ "message-type" ],
       "unevaluatedProperties": false
     }
   },
@@ -218,9 +218,6 @@
       }
     }
   },
-  "required": [
-    "category-id",
-    "interview"
-  ],
+  "required": [ "category-id", "interview" ],
   "unevaluatedProperties": false
 }
\ No newline at end of file
diff --git a/default_config.yml b/default_config.yml
index b05cced..fde5bce 100644
--- a/default_config.yml
+++ b/default_config.yml
@@ -61,11 +61,8 @@ interviews:
     # Any existing interviews can still be completed while interviews are disabled, but new ones will not be created.
     enabled: true
 
-    # Whether to delete the interview question and answer messages after an interview summary is posted.
-    delete-messages-after-summary: true
-
-    # Whether to delete the interview question and answer messages after an interview ends without a summary.
-    delete-messages-after-no-summary: true
+    # Whether to delete the interview question and answer messages after an interview ends.
+    delete-messages-after-interview-end: true
 
 database:
     # Address and port of the mysql server.