Add commands to update interview templates
This commit is contained in:
parent
e87208d8bd
commit
d212f13b12
8 changed files with 250 additions and 61 deletions
|
@ -1,7 +1,11 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Net.Http;
|
||||
using System.Runtime.Serialization;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using DSharpPlus.Commands;
|
||||
using DSharpPlus.Commands.ContextChecks;
|
||||
|
@ -10,6 +14,10 @@ using DSharpPlus.Entities;
|
|||
using DSharpPlus.Exceptions;
|
||||
using DSharpPlus.Interactivity;
|
||||
using DSharpPlus.Interactivity.Extensions;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using Newtonsoft.Json.Schema;
|
||||
using ErrorEventArgs = Newtonsoft.Json.Serialization.ErrorEventArgs;
|
||||
|
||||
namespace SupportChild.Commands;
|
||||
|
||||
|
@ -215,4 +223,98 @@ public class AdminCommands
|
|||
Logger.Log("Reloading bot...");
|
||||
SupportChild.Reload();
|
||||
}
|
||||
|
||||
[Command("getinterviewtemplates")]
|
||||
[Description("Provides a copy of the interview templates which you can edit and then reupload.")]
|
||||
public async Task GetInterviewTemplates(SlashCommandContext command)
|
||||
{
|
||||
MemoryStream stream = new MemoryStream(Encoding.UTF8.GetBytes(Database.GetInterviewTemplates()));
|
||||
await command.RespondAsync(new DiscordInteractionResponseBuilder().AddFile("interview-templates.json", stream));
|
||||
}
|
||||
|
||||
[Command("setinterviewtemplates")]
|
||||
[Description("Uploads an interview template file.")]
|
||||
public async Task SetInterviewTemplates(SlashCommandContext command, [Parameter("file")] DiscordAttachment file)
|
||||
{
|
||||
if (!file.MediaType?.Contains("application/json") ?? false)
|
||||
{
|
||||
await command.RespondAsync(new DiscordEmbedBuilder
|
||||
{
|
||||
|
||||
Color = DiscordColor.Red,
|
||||
Description = "The uploaded file is not a JSON file according to Discord."
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
Stream stream = await new HttpClient().GetStreamAsync(file.Url);
|
||||
string json = await new StreamReader(stream).ReadToEndAsync();
|
||||
|
||||
try
|
||||
{
|
||||
List<string> errors = [];
|
||||
|
||||
// Convert it to an interview object to validate the template
|
||||
Dictionary<ulong, Interviewer.ValidatedInterviewQuestion> interview = JsonConvert.DeserializeObject<Dictionary<ulong, Interviewer.ValidatedInterviewQuestion>>(json, new JsonSerializerSettings()
|
||||
{
|
||||
//NullValueHandling = NullValueHandling.Include,
|
||||
MissingMemberHandling = MissingMemberHandling.Error,
|
||||
Error = delegate (object sender, ErrorEventArgs args)
|
||||
{
|
||||
// I noticed the main exception mainly has information for developers, not administrators,
|
||||
// so I switched to using the inner message if available.
|
||||
if (string.IsNullOrEmpty(args.ErrorContext.Error.InnerException?.Message))
|
||||
{
|
||||
errors.Add(args.ErrorContext.Error.Message);
|
||||
}
|
||||
else
|
||||
{
|
||||
errors.Add(args.ErrorContext.Error.InnerException.Message);
|
||||
}
|
||||
|
||||
Logger.Debug("Exception occured when trying to upload interview template:\n" + args.ErrorContext.Error);
|
||||
args.ErrorContext.Handled = true;
|
||||
}
|
||||
});
|
||||
|
||||
if (errors.Count != 0)
|
||||
{
|
||||
string errorString = string.Join("\n\n", errors);
|
||||
if (errorString.Length > 1500)
|
||||
{
|
||||
errorString = errorString.Substring(0, 1500);
|
||||
}
|
||||
|
||||
await command.RespondAsync(new DiscordEmbedBuilder
|
||||
{
|
||||
Color = DiscordColor.Red,
|
||||
Description = "The uploaded JSON structure could not be converted to an interview template.\n\nErrors:\n```\n" + errorString + "\n```",
|
||||
Footer = new DiscordEmbedBuilder.EmbedFooter()
|
||||
{
|
||||
Text = "More detailed information may be available as debug messages in the bot logs."
|
||||
}
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
Database.SetInterviewTemplates(JsonConvert.SerializeObject(interview, Formatting.Indented));
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
await command.RespondAsync(new DiscordEmbedBuilder
|
||||
{
|
||||
|
||||
Color = DiscordColor.Red,
|
||||
Description = "The uploaded JSON structure could not be converted to an interview template.\n\nError message:\n```\n" + e.Message + "\n```"
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
await command.RespondAsync(new DiscordEmbedBuilder
|
||||
{
|
||||
|
||||
Color = DiscordColor.Green,
|
||||
Description = "Uploaded interview template."
|
||||
});
|
||||
}
|
||||
}
|
|
@ -32,8 +32,6 @@ 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()
|
||||
|
@ -102,8 +100,5 @@ internal static class Config
|
|||
database = json.SelectToken("database.name")?.Value<string>() ?? "supportchild";
|
||||
username = json.SelectToken("database.user")?.Value<string>() ?? "supportchild";
|
||||
password = json.SelectToken("database.password")?.Value<string>() ?? "";
|
||||
|
||||
// Set up interviewer
|
||||
interviews = json.SelectToken("interviews") ?? new JObject();
|
||||
}
|
||||
}
|
85
Database.cs
85
Database.cs
|
@ -731,6 +731,52 @@ public static class Database
|
|||
}
|
||||
}
|
||||
|
||||
public static string GetInterviewTemplates()
|
||||
{
|
||||
using MySqlConnection c = GetConnection();
|
||||
c.Open();
|
||||
using MySqlCommand selection = new MySqlCommand("SELECT * FROM interviews WHERE channel_id=0", c);
|
||||
selection.Prepare();
|
||||
MySqlDataReader results = selection.ExecuteReader();
|
||||
|
||||
// Check if messages exist in the database
|
||||
if (!results.Read())
|
||||
{
|
||||
return "{}";
|
||||
}
|
||||
|
||||
string templates = results.GetString("interview");
|
||||
results.Close();
|
||||
return templates;
|
||||
}
|
||||
|
||||
public static bool SetInterviewTemplates(string templates)
|
||||
{
|
||||
try
|
||||
{
|
||||
string query;
|
||||
if (TryGetInterview(0, out _))
|
||||
{
|
||||
query = "UPDATE interviews SET interview = @interview WHERE channel_id = 0";
|
||||
}
|
||||
else
|
||||
{
|
||||
query = "INSERT INTO interviews (channel_id,interview) VALUES (0, @interview)";
|
||||
}
|
||||
|
||||
using MySqlConnection c = GetConnection();
|
||||
c.Open();
|
||||
using MySqlCommand cmd = new MySqlCommand(query, c);
|
||||
cmd.Parameters.AddWithValue("@interview", templates);
|
||||
cmd.Prepare();
|
||||
return cmd.ExecuteNonQuery() > 0;
|
||||
}
|
||||
catch (MySqlException)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public static Dictionary<ulong, Interviewer.InterviewQuestion> GetAllInterviews()
|
||||
{
|
||||
using MySqlConnection c = GetConnection();
|
||||
|
@ -745,14 +791,16 @@ public static class Database
|
|||
return new Dictionary<ulong, Interviewer.InterviewQuestion>();
|
||||
}
|
||||
|
||||
Dictionary<ulong, Interviewer.InterviewQuestion> questions = new Dictionary<ulong, Interviewer.InterviewQuestion>
|
||||
Dictionary<ulong, Interviewer.InterviewQuestion> questions = new();
|
||||
do
|
||||
{
|
||||
{ results.GetUInt64("channel_id"), JsonConvert.DeserializeObject<Interviewer.InterviewQuestion>(results.GetString("interview")) }
|
||||
};
|
||||
while (results.Read())
|
||||
{
|
||||
questions.Add(results.GetUInt64("channel_id"), JsonConvert.DeserializeObject<Interviewer.InterviewQuestion>(results.GetString("interview")));
|
||||
// Channel ID 0 is the interview template
|
||||
if (results.GetUInt64("channel_id") != 0)
|
||||
{
|
||||
questions.Add(results.GetUInt64("channel_id"), JsonConvert.DeserializeObject<Interviewer.InterviewQuestion>(results.GetString("interview")));
|
||||
}
|
||||
}
|
||||
while (results.Read());
|
||||
results.Close();
|
||||
|
||||
return questions;
|
||||
|
@ -762,26 +810,23 @@ public static class Database
|
|||
{
|
||||
try
|
||||
{
|
||||
string query;
|
||||
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;
|
||||
query = "UPDATE interviews SET interview = @interview WHERE channel_id = @channel_id";
|
||||
}
|
||||
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;
|
||||
query = "INSERT INTO interviews (channel_id,interview) VALUES (@channel_id, @interview)";
|
||||
}
|
||||
|
||||
using MySqlConnection c = GetConnection();
|
||||
c.Open();
|
||||
using MySqlCommand cmd = new MySqlCommand(query, c);
|
||||
cmd.Parameters.AddWithValue("@channel_id", channelID);
|
||||
cmd.Parameters.AddWithValue("@interview", JsonConvert.SerializeObject(interview));
|
||||
cmd.Prepare();
|
||||
return cmd.ExecuteNonQuery() > 0;
|
||||
}
|
||||
catch (MySqlException)
|
||||
{
|
||||
|
|
|
@ -1,9 +1,12 @@
|
|||
using System.Collections.Generic;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Linq;
|
||||
using DSharpPlus;
|
||||
using DSharpPlus.Entities;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Converters;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using Newtonsoft.Json.Serialization;
|
||||
|
||||
namespace SupportChild;
|
||||
|
||||
|
@ -11,8 +14,8 @@ public static class Interviewer
|
|||
{
|
||||
public enum QuestionType
|
||||
{
|
||||
DONE,
|
||||
FAIL,
|
||||
DONE,
|
||||
BUTTONS,
|
||||
SELECTOR,
|
||||
TEXT_INPUT
|
||||
|
@ -25,37 +28,95 @@ public static class Interviewer
|
|||
public class InterviewQuestion
|
||||
{
|
||||
// Message contents sent to the user.
|
||||
[JsonProperty("message")]
|
||||
public string message;
|
||||
|
||||
// The type of question.
|
||||
[JsonConverter(typeof(StringEnumConverter))]
|
||||
[JsonProperty("type")]
|
||||
public QuestionType type;
|
||||
|
||||
// Colour of the message embed.
|
||||
[JsonProperty("color")]
|
||||
public string color;
|
||||
|
||||
// The ID of this message, populated after it has been sent.
|
||||
// The ID of this message where the bot asked this question,
|
||||
// populated after it has been sent.
|
||||
[JsonProperty("message-id")]
|
||||
public ulong messageID;
|
||||
|
||||
// Used as label for this question in the post-interview summary.
|
||||
[JsonProperty("summary-field")]
|
||||
public string summaryField;
|
||||
|
||||
// The user's response to the question.
|
||||
[JsonProperty("answer")]
|
||||
public string answer;
|
||||
|
||||
// The ID of the user's answer message, populated after it has been received.
|
||||
[JsonProperty("answer-id")]
|
||||
public ulong answerID;
|
||||
|
||||
// Possible questions to ask next, or DONE/FAIL type in order to finish interview.
|
||||
[JsonProperty("paths")]
|
||||
public Dictionary<string, InterviewQuestion> paths;
|
||||
}
|
||||
|
||||
// This class is identical to the one above and just exists as a hack to get JSON validation when
|
||||
// new entries are entered but not when read from database in order to be more lenient with old interviews.
|
||||
// I might do this in a more proper way at some point.
|
||||
public class ValidatedInterviewQuestion
|
||||
{
|
||||
// Message contents sent to the user.
|
||||
[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;
|
||||
|
||||
// The ID of this message where the bot asked this question,
|
||||
// populated after it has been sent.
|
||||
[JsonProperty("message-id", Required = Required.Default)]
|
||||
public ulong messageID;
|
||||
|
||||
// Used as label for this question in the post-interview summary.
|
||||
[JsonProperty("summary-field", Required = Required.Default)]
|
||||
public string summaryField;
|
||||
|
||||
// The user's response to the question.
|
||||
[JsonProperty("answer", Required = Required.Default)]
|
||||
public string answer;
|
||||
|
||||
// The ID of the user's answer message, populated after it has been received.
|
||||
[JsonProperty("answer-id", Required = Required.Default)]
|
||||
public ulong answerID;
|
||||
|
||||
// Possible questions to ask next, or DONE/FAIL type in order to finish interview.
|
||||
[JsonProperty("paths", Required = Required.Always)]
|
||||
public Dictionary<string, ValidatedInterviewQuestion> paths;
|
||||
}
|
||||
|
||||
private static Dictionary<ulong, InterviewQuestion> categoryInterviews = [];
|
||||
|
||||
private static Dictionary<ulong, InterviewQuestion> activeInterviews = [];
|
||||
|
||||
public static void ParseConfig(JToken interviewConfig)
|
||||
public static void ParseTemplates(JToken interviewConfig)
|
||||
{
|
||||
categoryInterviews = JsonConvert.DeserializeObject<Dictionary<ulong, InterviewQuestion>>(interviewConfig.ToString());
|
||||
categoryInterviews = JsonConvert.DeserializeObject<Dictionary<ulong, InterviewQuestion>>(interviewConfig.ToString(), new JsonSerializerSettings
|
||||
{
|
||||
Error = delegate (object sender, ErrorEventArgs args)
|
||||
{
|
||||
Logger.Error("Exception occured when trying to read interview from database:\n" + args.ErrorContext.Error.Message);
|
||||
Logger.Debug("Detailed exception:", args.ErrorContext.Error);
|
||||
args.ErrorContext.Handled = true;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public static void LoadActiveInterviews()
|
||||
|
@ -107,7 +168,7 @@ public static class Interviewer
|
|||
List<DiscordButtonComponent> buttonRow = [];
|
||||
for (; nrOfButtons < 5 * (nrOfButtonRows + 1) && nrOfButtons < question.paths.Count; nrOfButtons++)
|
||||
{
|
||||
buttonRow.Add(new DiscordButtonComponent(ButtonStyle.Primary, "supportchild_interviewbutton " + nrOfButtons, question.paths.ToArray()[nrOfButtons].Key));
|
||||
buttonRow.Add(new DiscordButtonComponent(DiscordButtonStyle.Primary, "supportchild_interviewbutton " + nrOfButtons, question.paths.ToArray()[nrOfButtons].Key));
|
||||
}
|
||||
msgBuilder.AddComponents(buttonRow);
|
||||
}
|
||||
|
|
|
@ -93,7 +93,11 @@ internal static class SupportChild
|
|||
Logger.Log("Starting " + Assembly.GetEntryAssembly()?.GetName().Name + " version " + GetVersion() + "...");
|
||||
try
|
||||
{
|
||||
Reload();
|
||||
if (!await Reload())
|
||||
{
|
||||
Logger.Fatal("Aborting startup due to a fatal error...");
|
||||
return;
|
||||
}
|
||||
|
||||
// Block this task until the program is closed.
|
||||
await Task.Delay(-1);
|
||||
|
@ -115,7 +119,7 @@ internal static class SupportChild
|
|||
+ " (" + ThisAssembly.Git.Commit + ")";
|
||||
}
|
||||
|
||||
public static async void Reload()
|
||||
public static async Task<bool> Reload()
|
||||
{
|
||||
if (client != null)
|
||||
{
|
||||
|
@ -141,20 +145,20 @@ internal static class SupportChild
|
|||
}
|
||||
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.Fatal("Could not set up database tables, please confirm connection settings, status of the server and permissions of MySQL user. Error: ", e);
|
||||
return false;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
Logger.Log("Connecting to database... (" + Config.hostName + ":" + Config.port + ")");
|
||||
Interviewer.ParseConfig(Config.interviews);
|
||||
Logger.Log("Loading interviews from database...");
|
||||
Interviewer.ParseTemplates(Database.GetInterviewTemplates());
|
||||
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.Fatal("Could not load interviews from database. Error: ", e);
|
||||
return false;
|
||||
}
|
||||
|
||||
Logger.Log("Setting up Discord client...");
|
||||
|
@ -233,6 +237,7 @@ internal static class SupportChild
|
|||
|
||||
Logger.Log("Connecting to Discord...");
|
||||
await client.ConnectAsync();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -42,11 +42,12 @@
|
|||
<PackageReference Include="JsonExtensions" Version="1.2.0" />
|
||||
<PackageReference Include="MySqlConnector" Version="2.3.7" />
|
||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
|
||||
<PackageReference Include="Newtonsoft.Json.Schema" Version="4.0.1" />
|
||||
<PackageReference Include="Polly" Version="8.4.0" />
|
||||
<PackageReference Include="RazorBlade" Version="0.6.0" />
|
||||
<PackageReference Include="Superpower" Version="3.0.0" />
|
||||
<PackageReference Include="WebMarkupMin.Core" Version="2.17.0" />
|
||||
<PackageReference Include="YamlDotNet" Version="16.1.0" />
|
||||
<PackageReference Include="YamlDotNet" Version="16.1.3" />
|
||||
<PackageReference Include="YoutubeExplode" Version="6.4.0" />
|
||||
</ItemGroup>
|
||||
|
||||
|
|
|
@ -66,7 +66,6 @@ public static class Utilities
|
|||
return verifiedCategories;
|
||||
}
|
||||
|
||||
|
||||
public static string ReadManifestData(string embeddedFileName)
|
||||
{
|
||||
Assembly assembly = Assembly.GetExecutingAssembly();
|
||||
|
|
|
@ -19,7 +19,7 @@
|
|||
# Whether staff members should be randomly assigned tickets when they are made. Individual staff members can opt out using the toggleactive command.
|
||||
random-assignment: true
|
||||
|
||||
# If set to true the rasssign command will include staff members set as inactive if a specific role is specified in the command.
|
||||
# If set to true the rassign command will include staff members set as inactive if a specific role is specified in the command.
|
||||
# This can be useful if you have admins set as inactive to not automatically receive tickets and then have moderators elevate tickets when needed.
|
||||
random-assign-role-override: true
|
||||
|
||||
|
@ -63,23 +63,4 @@ database:
|
|||
|
||||
# Username and password for authentication.
|
||||
user: ""
|
||||
password: ""
|
||||
|
||||
# TODO: May want to move interview entries to subkey to make room for additional interview settings
|
||||
interviews:
|
||||
000000000000000000:
|
||||
message: "Are you appealing your own ban or on behalf of another user?"
|
||||
type: "BUTTONS"
|
||||
color: "CYAN"
|
||||
paths:
|
||||
- "My own ban": # TODO: Can I add button color support somehow?
|
||||
text: "What is your user name?"
|
||||
type: "TEXT_INPUT"
|
||||
summary-field: "Username"
|
||||
paths:
|
||||
- ".*":
|
||||
text: "Please write your appeal below, motivate why you think you should be unbanned."
|
||||
- "Another user's ban":
|
||||
text: "You can only appeal your own ban. Please close this ticket."
|
||||
type: "FAIL"
|
||||
color: "CYAN"
|
||||
password: ""
|
Loading…
Reference in a new issue