From ce836a465f80043a16c7a50595032644832d851a Mon Sep 17 00:00:00 2001 From: Toastie Date: Thu, 26 Dec 2024 18:07:36 +1300 Subject: [PATCH] Added database functions for interviews --- Config.cs | 4 ++- Database.cs | 85 +++++++++++++++++++++++++++++++++++++++++++++++++ Interviewer.cs | 38 ++++++++++++++-------- SupportChild.cs | 12 +++++++ 4 files changed, 125 insertions(+), 14 deletions(-) diff --git a/Config.cs b/Config.cs index 9d993d0..4528d1f 100644 --- a/Config.cs +++ b/Config.cs @@ -32,6 +32,8 @@ internal static class Config internal static string username = "supportchild"; internal static string password = ""; + internal static JToken interviews; + private static string configPath = "./config.yml"; public static void LoadConfig() @@ -100,6 +102,6 @@ internal static class Config password = json.SelectToken("database.password")?.Value() ?? ""; // Set up interviewer - Interviewer.ParseConfig(json.SelectToken("interviews")); + interviews = json.SelectToken("interviews"); } } \ No newline at end of file diff --git a/Database.cs b/Database.cs index 517da9e..d1d1bda 100644 --- a/Database.cs +++ b/Database.cs @@ -4,6 +4,7 @@ using System.Collections.Generic; using System.Security.Cryptography; using DSharpPlus; using MySqlConnector; +using Newtonsoft.Json; namespace SupportChild; @@ -107,6 +108,11 @@ public static class Database "name VARCHAR(256) NOT NULL UNIQUE," + "category_id BIGINT UNSIGNED NOT NULL PRIMARY KEY)", c); + using MySqlCommand createInterviews = new MySqlCommand( + "CREATE TABLE IF NOT EXISTS interviews(" + + "channel_id BIGINT UNSIGNED NOT NULL PRIMARY KEY," + + "interview JSON NOT NULL)", + c); c.Open(); createTickets.ExecuteNonQuery(); createBlacklisted.ExecuteNonQuery(); @@ -114,6 +120,7 @@ public static class Database createStaffList.ExecuteNonQuery(); createMessages.ExecuteNonQuery(); createCategories.ExecuteNonQuery(); + createInterviews.ExecuteNonQuery(); } public static bool IsOpenTicket(ulong channelID) @@ -724,6 +731,84 @@ public static class Database } } + public static Dictionary GetAllInterviews() + { + using MySqlConnection c = GetConnection(); + c.Open(); + using MySqlCommand selection = new MySqlCommand("SELECT * FROM interviews", c); + selection.Prepare(); + MySqlDataReader results = selection.ExecuteReader(); + + // Check if messages exist in the database + if (!results.Read()) + { + return new Dictionary(); + } + + Dictionary questions = new Dictionary + { + { results.GetUInt64("channel_id"), JsonConvert.DeserializeObject(results.GetString("interview")) } + }; + while (results.Read()) + { + questions.Add(results.GetUInt64("channel_id"), JsonConvert.DeserializeObject(results.GetString("interview"))); + } + results.Close(); + + return questions; + } + + public static bool SaveInterview(ulong channelID, Interviewer.InterviewQuestion interview) + { + try + { + if (TryGetInterview(channelID, out _)) + { + using MySqlConnection c = GetConnection(); + c.Open(); + using MySqlCommand cmd = new MySqlCommand(@"UPDATE interviews SET interview = @interview WHERE channel_id = @channel_id;", c); + cmd.Parameters.AddWithValue("@channel_id", channelID); + cmd.Parameters.AddWithValue("@interview", JsonConvert.SerializeObject(interview)); + cmd.Prepare(); + return cmd.ExecuteNonQuery() > 0; + } + else + { + using MySqlConnection c = GetConnection(); + c.Open(); + using MySqlCommand cmd = new MySqlCommand(@"INSERT INTO interviews (channel_id,interview) VALUES (@channel_id, @interview);", c); + cmd.Parameters.AddWithValue("@channel_id", channelID); + cmd.Parameters.AddWithValue("@interview", JsonConvert.SerializeObject(interview)); + cmd.Prepare(); + return cmd.ExecuteNonQuery() > 0; + } + } + catch (MySqlException) + { + return false; + } + } + + public static bool TryGetInterview(ulong channelID, out Interviewer.InterviewQuestion interview) + { + using MySqlConnection c = GetConnection(); + c.Open(); + using MySqlCommand selection = new MySqlCommand(@"SELECT * FROM interviews WHERE channel_id=@channel_id", c); + selection.Parameters.AddWithValue("@channel_id", channelID); + selection.Prepare(); + MySqlDataReader results = selection.ExecuteReader(); + + // Check if ticket exists in the database + if (!results.Read()) + { + interview = null; + return false; + } + interview = JsonConvert.DeserializeObject(results.GetString("interview")); + results.Close(); + return true; + } + public class Ticket { public uint id; diff --git a/Interviewer.cs b/Interviewer.cs index 737fa95..506b28b 100644 --- a/Interviewer.cs +++ b/Interviewer.cs @@ -22,7 +22,7 @@ public static class Interviewer // 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 struct InterviewQuestion + public class InterviewQuestion { // Message contents sent to the user. public string message; @@ -51,11 +51,18 @@ public static class Interviewer private static Dictionary categoryInterviews = []; + private static Dictionary activeInterviews = []; + public static void ParseConfig(JToken interviewConfig) { categoryInterviews = JsonConvert.DeserializeObject>(interviewConfig.ToString()); } + public static void LoadActiveInterviews() + { + activeInterviews = Database.GetAllInterviews(); + } + public static void StartInterview(DiscordChannel channel) { if (channel.Parent == null) @@ -66,23 +73,29 @@ public static class Interviewer if (categoryInterviews.TryGetValue(channel.Parent.Id, out InterviewQuestion interview)) { CreateQuestion(channel, interview); + Database.SaveInterview(channel.Id, interview); } } - public static void ProcessResponse(DiscordChannel channel, string response) + public static void ProcessResponse(DiscordMessage message) { + // TODO: Find if channel has open interview. + // TODO: Find if message is replying to interview message. + // TODO: Handle other interactions like button presses. + // TODO: Find out where in the interview tree we are. + // TODO: Handle FAIL event, cancelling the interview. + // TODO: Handle DONE event, creating a summary. + //Database.SaveInterview(channel.Id, interview); } private static async void CreateQuestion(DiscordChannel channel, InterviewQuestion question) { - DiscordMessageBuilder msgBuilder = new DiscordMessageBuilder + DiscordMessageBuilder msgBuilder = new DiscordMessageBuilder(); + DiscordEmbedBuilder embed = new DiscordEmbedBuilder() { - Embed = new DiscordEmbedBuilder - { - Color = Utilities.StringToColor(question.color), - Description = question.message - } + Color = Utilities.StringToColor(question.color), + Description = question.message }; switch (question.type) @@ -94,7 +107,7 @@ public static class Interviewer List buttonRow = []; for (; nrOfButtons < 5 * (nrOfButtonRows + 1) && nrOfButtons < question.paths.Count; nrOfButtons++) { - buttonRow.Add(new DiscordButtonComponent(ButtonStyle.Primary, "supportboi_interviewbutton " + nrOfButtons, question.paths.ToArray()[nrOfButtons].Key)); + buttonRow.Add(new DiscordButtonComponent(ButtonStyle.Primary, "supportchild_interviewbutton " + nrOfButtons, question.paths.ToArray()[nrOfButtons].Key)); } msgBuilder.AddComponents(buttonRow); } @@ -110,13 +123,13 @@ public static class Interviewer { categoryOptions.Add(new DiscordSelectComponentOption(question.paths.ToArray()[selectionOptions].Key, selectionOptions.ToString())); } - selectionComponents.Add(new DiscordSelectComponent("supportboi_interviewselector " + selectionBoxes, "Select an option...", categoryOptions, false, 0, 1)); + selectionComponents.Add(new DiscordSelectComponent("supportchild_interviewselector " + selectionBoxes, "Select an option...", categoryOptions, false, 0, 1)); } msgBuilder.AddComponents(selectionComponents); break; case QuestionType.TEXT_INPUT: - // TODO: Add embed footer with response info + embed.WithFooter("Reply to this message with your answer."); break; case QuestionType.DONE: case QuestionType.FAIL: @@ -124,10 +137,9 @@ public static class Interviewer break; } + msgBuilder.AddEmbed(embed); DiscordMessage message = await channel.SendMessageAsync(msgBuilder); question.messageID = message.Id; - - // TODO: Save interview progress } public static void CreateSummary() diff --git a/SupportChild.cs b/SupportChild.cs index 0f8147f..730e413 100644 --- a/SupportChild.cs +++ b/SupportChild.cs @@ -152,6 +152,18 @@ internal static class SupportChild throw; } + try + { + Logger.Log("Connecting to database... (" + Config.hostName + ":" + Config.port + ")"); + Interviewer.ParseConfig(Config.interviews); + Interviewer.LoadActiveInterviews(); + } + catch (Exception e) + { + Logger.Fatal("Could not set up database tables, please confirm connection settings, status of the server and permissions of MySQL user. Error: " + e); + throw; + } + Logger.Log("Setting up Discord client..."); // Setting up client configuration