Compare commits

...

12 commits
4.0.0 ... main

44 changed files with 1468 additions and 1270 deletions

View file

@ -37,7 +37,7 @@ public class AddCategoryCommand
return;
}
if (Database.TryGetCategory(category.Id, out Database.Category _))
if (Database.Category.TryGetCategory(category.Id, out Database.Category _))
{
await command.RespondAsync(new DiscordEmbedBuilder
{
@ -47,7 +47,7 @@ public class AddCategoryCommand
return;
}
if (Database.TryGetCategory(title, out Database.Category _))
if (Database.Category.TryGetCategory(title, out Database.Category _))
{
await command.RespondAsync(new DiscordEmbedBuilder
{
@ -57,7 +57,7 @@ public class AddCategoryCommand
return;
}
if (Database.AddCategory(title, category.Id))
if (Database.Category.AddCategory(title, category.Id))
{
await command.RespondAsync(new DiscordEmbedBuilder
{

View file

@ -18,7 +18,7 @@ public class AddCommand
[Parameter("user")][Description("User to add to ticket.")] DiscordUser user)
{
// Check if ticket exists in the database
if (!Database.TryGetOpenTicket(command.Channel.Id, out Database.Ticket ticket))
if (!Database.Ticket.TryGetOpenTicket(command.Channel.Id, out Database.Ticket ticket))
{
await command.RespondAsync(new DiscordEmbedBuilder
{

View file

@ -1,60 +0,0 @@
using System.ComponentModel;
using System.Globalization;
using DSharpPlus.Entities;
using System.Threading.Tasks;
using DSharpPlus.Commands;
using DSharpPlus.Commands.ContextChecks;
using DSharpPlus.Commands.Processors.SlashCommands;
using DSharpPlus.Exceptions;
namespace SupportChild.Commands;
public class AddMessageCommand
{
[RequireGuild]
[Command("addmessage")]
[Description("Adds a new message for the 'say' command.")]
public async Task OnExecute(SlashCommandContext command,
[Parameter("identifier")][Description("The identifier word used in the /say command.")] string identifier,
[Parameter("message")][Description("The message the /say command will return.")] string message)
{
if (string.IsNullOrEmpty(message))
{
await command.RespondAsync(new DiscordEmbedBuilder
{
Color = DiscordColor.Red,
Description = "No message specified."
}, true);
return;
}
if (Database.TryGetMessage(identifier.ToLower(CultureInfo.InvariantCulture), out Database.Message _))
{
await command.RespondAsync(new DiscordEmbedBuilder
{
Color = DiscordColor.Red,
Description = "There is already a message with that identifier."
}, true);
return;
}
if (Database.AddMessage(identifier, command.Member.Id, message))
{
await command.RespondAsync(new DiscordEmbedBuilder
{
Color = DiscordColor.Green,
Description = "Message added."
}, true);
}
else
{
await command.RespondAsync(new DiscordEmbedBuilder
{
Color = DiscordColor.Red,
Description = "Error: Failed adding the message to the database."
}, true);
}
await LogChannel.Success(command.User.Mention + " added or updated `" + identifier + "` in the /say command.\n\nContent:\n\n" + message);
}
}

View file

@ -18,7 +18,7 @@ public class AddStaffCommand
public async Task OnExecute(SlashCommandContext command,
[Parameter("user")][Description("User to add to staff.")] DiscordUser user)
{
DiscordMember staffMember = null;
DiscordMember staffMember;
try
{
staffMember = user == null ? command.Member : await command.Guild.GetMemberAsync(user.Id);
@ -43,16 +43,15 @@ public class AddStaffCommand
return;
}
bool alreadyStaff = Database.IsStaff(staffMember.Id);
await using MySqlConnection c = Database.GetConnection();
MySqlCommand cmd = alreadyStaff ? new MySqlCommand(@"UPDATE staff SET name = @name WHERE user_id = @user_id", c) : new MySqlCommand(@"INSERT INTO staff (user_id, name) VALUES (@user_id, @name);", c);
c.Open();
cmd.Parameters.AddWithValue("@user_id", staffMember.Id);
cmd.Parameters.AddWithValue("@name", staffMember.DisplayName);
cmd.ExecuteNonQuery();
cmd.Dispose();
bool alreadyStaff = Database.StaffMember.IsStaff(staffMember.Id);
if (!Database.StaffMember.AddStaff(staffMember.DisplayName, staffMember.Id))
{
await command.RespondAsync(new DiscordEmbedBuilder
{
Color = DiscordColor.Red,
Description = "Error: Failed to add staff member to database."
}, true);
}
if (alreadyStaff)
{

View file

@ -21,7 +21,7 @@ public class AdminCommands
[Parameter("user")][Description("(Optional) The owner of the ticket.")] DiscordUser user = null)
{
// Check if ticket exists in the database
if (Database.IsOpenTicket(command.Channel.Id))
if (Database.Ticket.IsOpenTicket(command.Channel.Id))
{
await command.RespondAsync(new DiscordEmbedBuilder
{
@ -33,7 +33,7 @@ public class AdminCommands
DiscordUser ticketUser = user == null ? command.User : user;
long id = Database.NewTicket(ticketUser.Id, 0, command.Channel.Id);
long id = Database.Ticket.NewTicket(ticketUser.Id, 0, command.Channel.Id);
await command.RespondAsync(new DiscordEmbedBuilder
{
Color = DiscordColor.Green,
@ -57,7 +57,7 @@ public class AdminCommands
if (ticketID == 0)
{
// Check if ticket exists in the database
if (!Database.TryGetOpenTicket(command.Channel.Id, out ticket))
if (!Database.Ticket.TryGetOpenTicket(command.Channel.Id, out ticket))
{
await command.RespondAsync(new DiscordEmbedBuilder
{
@ -71,7 +71,7 @@ public class AdminCommands
else
{
// Check if ticket exists in the database
if (!Database.TryGetOpenTicketByID((uint)ticketID, out ticket))
if (!Database.Ticket.TryGetOpenTicketByID((uint)ticketID, out ticket))
{
await command.RespondAsync(new DiscordEmbedBuilder
{
@ -96,7 +96,7 @@ public class AdminCommands
}
// Delete the ticket from the database and respond to command
if (Database.DeleteOpenTicket(ticket.id))
if (Database.Ticket.DeleteOpenTicket(ticket.id))
{
await command.RespondAsync(new DiscordEmbedBuilder
{

View file

@ -43,7 +43,7 @@ public class AssignCommand
}
// Check if ticket exists in the database
if (!Database.TryGetOpenTicket(command.Channel.Id, out Database.Ticket ticket))
if (!Database.Ticket.TryGetOpenTicket(command.Channel.Id, out Database.Ticket ticket))
{
await command.RespondAsync(new DiscordEmbedBuilder
{
@ -53,7 +53,7 @@ public class AssignCommand
return;
}
if (!Database.IsStaff(member.Id))
if (!Database.StaffMember.IsStaff(member.Id))
{
await command.RespondAsync(new DiscordEmbedBuilder
{
@ -63,7 +63,7 @@ public class AssignCommand
return;
}
if (!Database.AssignStaff(ticket, member.Id))
if (!Database.StaffMember.AssignStaff(ticket, member.Id))
{
await command.RespondAsync(new DiscordEmbedBuilder
{

View file

@ -19,7 +19,7 @@ public class BlacklistCommand
{
try
{
if (!Database.Blacklist(user.Id, command.User.Id))
if (!Database.Blacklist.Ban(user.Id, command.User.Id))
{
await command.RespondAsync(new DiscordEmbedBuilder
{

View file

@ -26,7 +26,7 @@ public class CloseCommand
[Parameter("reason")][Description("(Optional) The reason for closing this ticket.")] string reason = "")
{
// Check if ticket exists in the database
if (!Database.TryGetOpenTicket(command.Channel.Id, out Database.Ticket _))
if (!Database.Ticket.TryGetOpenTicket(command.Channel.Id, out Database.Ticket _))
{
await command.RespondAsync(new DiscordEmbedBuilder
{
@ -42,17 +42,20 @@ public class CloseCommand
Color = DiscordColor.Cyan,
Description = "Are you sure you wish to close this ticket? You cannot re-open it again later."
})
.AddComponents(new DiscordButtonComponent(DiscordButtonStyle.Danger, "supportchild_closeconfirm", "Confirm"));
.AddComponents(new DiscordButtonComponent(DiscordButtonStyle.Danger, "supportboi_closeconfirm", "Confirm"));
await command.RespondAsync(confirmation);
if (closeReasons.TryGetValue(command.Channel.Id, out _))
if (!string.IsNullOrWhiteSpace(reason))
{
closeReasons[command.Channel.Id] = reason;
}
else
{
closeReasons.Add(command.Channel.Id, reason);
if (closeReasons.TryGetValue(command.Channel.Id, out _))
{
closeReasons[command.Channel.Id] = reason;
}
else
{
closeReasons.Add(command.Channel.Id, reason);
}
}
}
@ -75,7 +78,7 @@ public class CloseCommand
await interaction.CreateResponseAsync(DiscordInteractionResponseType.DeferredMessageUpdate);
// Check if ticket exists in the database
if (!Database.TryGetOpenTicket(interaction.Channel.Id, out Database.Ticket ticket))
if (!Database.Ticket.TryGetOpenTicket(interaction.Channel.Id, out Database.Ticket ticket))
{
currentlyClosingTickets.Remove(interaction.Channel.Id);
await interaction.EditOriginalResponseAsync(new DiscordWebhookBuilder().AddEmbed(new DiscordEmbedBuilder
@ -165,7 +168,7 @@ public class CloseCommand
{
try
{
DiscordUser staffMember = await SupportChild.client.GetUserAsync(ticket.creatorID);
DiscordUser ticketCreator = await SupportChild.client.GetUserAsync(ticket.creatorID);
await using FileStream file = new(filePath, FileMode.Open, FileAccess.Read);
DiscordMessageBuilder message = new();
@ -198,14 +201,15 @@ public class CloseCommand
message.AddFiles(new Dictionary<string, Stream> { { fileName, file } });
await staffMember.SendMessageAsync(message);
await ticketCreator.SendMessageAsync(message);
}
catch (NotFoundException) { /* ignore */ }
catch (UnauthorizedException) { /* ignore */ }
catch (NullReferenceException) { /* ignore */ }
}
Database.ArchiveTicket(ticket);
Database.TryDeleteInterview(interaction.Channel.Id);
Database.Ticket.ArchiveTicket(ticket);
Database.Interviews.TryDeleteInterview(interaction.Channel.Id);
await interaction.EditOriginalResponseAsync(new DiscordWebhookBuilder().AddEmbed(new DiscordEmbedBuilder
{
@ -218,7 +222,7 @@ public class CloseCommand
// Delete the channel and database entry
await interaction.Channel.DeleteAsync("Ticket closed.");
Database.DeleteOpenTicket(ticket.id);
Database.Ticket.DeleteOpenTicket(ticket.id);
closeReasons.Remove(interaction.Channel.Id);
currentlyClosingTickets.Remove(interaction.Channel.Id);

View file

@ -16,7 +16,7 @@ public class InterviewCommands
[Description("Restarts the interview in this ticket, using an updated template if available.")]
public async Task Restart(SlashCommandContext command)
{
if (!Database.TryGetOpenTicket(command.Channel.Id, out Database.Ticket ticket))
if (!Database.Ticket.TryGetOpenTicket(command.Channel.Id, out Database.Ticket ticket))
{
await command.RespondAsync(new DiscordEmbedBuilder
{
@ -52,7 +52,7 @@ public class InterviewCommands
[Description("Stops the interview in this ticket.")]
public async Task Stop(SlashCommandContext command)
{
if (!Database.TryGetOpenTicket(command.Channel.Id, out Database.Ticket ticket))
if (!Database.Ticket.TryGetOpenTicket(command.Channel.Id, out Database.Ticket ticket))
{
await command.RespondAsync(new DiscordEmbedBuilder
{
@ -62,7 +62,7 @@ public class InterviewCommands
return;
}
if (!Database.TryGetInterview(command.Channel.Id, out Interview _))
if (!Database.Interviews.TryGetInterview(command.Channel.Id, out Interview _))
{
await command.RespondAsync(new DiscordEmbedBuilder
{

View file

@ -39,7 +39,7 @@ public class InterviewTemplateCommands
return;
}
if (Database.TryGetInterviewTemplateJSON(category.Id, out string templateJSON))
if (Database.Interviews.TryGetInterviewTemplateJSON(category.Id, out string templateJSON))
{
MemoryStream stream = new(Encoding.UTF8.GetBytes(templateJSON));
await command.RespondAsync(new DiscordInteractionResponseBuilder()
@ -172,7 +172,7 @@ public class InterviewTemplateCommands
return;
}
if (!Database.SetInterviewTemplate(template))
if (!Database.Interviews.SetInterviewTemplate(template))
{
await command.RespondAsync(new DiscordEmbedBuilder
{
@ -207,7 +207,7 @@ public class InterviewTemplateCommands
try
{
if (Database.TryGetInterviewTemplateJSON(template.categoryID, out string templateJSON))
if (Database.Interviews.TryGetInterviewTemplateJSON(template.categoryID, out string templateJSON))
{
MemoryStream memStream = new(Encoding.UTF8.GetBytes(templateJSON));
await LogChannel.Success(command.User.Mention + " uploaded a new interview template for the `" + category.Name + "` category.", 0,
@ -256,7 +256,7 @@ public class InterviewTemplateCommands
return;
}
if (!Database.TryGetInterviewFromTemplate(category.Id, 0, out Interview _))
if (!Database.Interviews.TryGetInterviewFromTemplate(category.Id, 0, out Interview _))
{
await command.RespondAsync(new DiscordEmbedBuilder
{
@ -266,7 +266,7 @@ public class InterviewTemplateCommands
return;
}
if (!Database.TryGetInterviewTemplateJSON(category.Id, out string templateJSON))
if (!Database.Interviews.TryGetInterviewTemplateJSON(category.Id, out string templateJSON))
{
await command.RespondAsync(new DiscordEmbedBuilder
{
@ -277,7 +277,7 @@ public class InterviewTemplateCommands
}
MemoryStream memStream = new(Encoding.UTF8.GetBytes(templateJSON));
if (!Database.TryDeleteInterviewTemplate(category.Id))
if (!Database.Interviews.TryDeleteInterviewTemplate(category.Id))
{
await command.RespondAsync(new DiscordEmbedBuilder
{

View file

@ -20,7 +20,7 @@ public class ListAssignedCommand
{
DiscordUser listUser = user == null ? command.User : user;
if (!Database.TryGetAssignedTickets(listUser.Id, out List<Database.Ticket> assignedTickets))
if (!Database.Ticket.TryGetAssignedTickets(listUser.Id, out List<Database.Ticket> assignedTickets))
{
await command.RespondAsync(new DiscordEmbedBuilder
{

View file

@ -21,7 +21,7 @@ public class ListCommand
DiscordUser listUser = user == null ? command.User : user;
List<DiscordEmbedBuilder> openEmbeds = new List<DiscordEmbedBuilder>();
if (Database.TryGetOpenTickets(listUser.Id, out List<Database.Ticket> openTickets))
if (Database.Ticket.TryGetOpenTickets(listUser.Id, out List<Database.Ticket> openTickets))
{
List<string> listItems = new List<string>();
foreach (Database.Ticket ticket in openTickets)
@ -46,7 +46,7 @@ public class ListCommand
}
List<DiscordEmbedBuilder> closedEmbeds = new List<DiscordEmbedBuilder>();
if (Database.TryGetClosedTickets(listUser.Id, out List<Database.Ticket> closedTickets))
if (Database.Ticket.TryGetClosedTickets(listUser.Id, out List<Database.Ticket> closedTickets))
{
List<string> listItems = new List<string>();
foreach (Database.Ticket ticket in closedTickets)

View file

@ -20,7 +20,7 @@ public class ListInvalidCommand
[Description("List tickets which channels have been deleted. Use /admin unsetticket <id> to remove them.")]
public async Task ListInvalid(SlashCommandContext command)
{
if (!Database.TryGetOpenTickets(out List<Database.Ticket> openTickets))
if (!Database.Ticket.TryGetOpenTickets(out List<Database.Ticket> openTickets))
{
await command.RespondAsync(new DiscordEmbedBuilder
{

View file

@ -20,7 +20,7 @@ public class ListOpen
[Description("Lists all open tickets, oldest first.")]
public async Task OnExecute(SlashCommandContext command)
{
if (!Database.TryGetOpenTickets(out List<Database.Ticket> openTickets))
if (!Database.Ticket.TryGetOpenTickets(out List<Database.Ticket> openTickets))
{
await command.RespondAsync(new DiscordEmbedBuilder
{

View file

@ -20,7 +20,7 @@ public class ListUnassignedCommand
[Description("Lists unassigned tickets.")]
public async Task OnExecute(SlashCommandContext command)
{
if (!Database.TryGetAssignedTickets(0, out List<Database.Ticket> unassignedTickets))
if (!Database.Ticket.TryGetAssignedTickets(0, out List<Database.Ticket> unassignedTickets))
{
await command.RespondAsync(new DiscordEmbedBuilder
{

View file

@ -20,7 +20,7 @@ public class MoveCommand
[Parameter("category")][Description("The category to move the ticket to. Only has to be the beginning of the name.")] string category)
{
// Check if ticket exists in the database
if (!Database.TryGetOpenTicket(command.Channel.Id, out Database.Ticket ticket))
if (!Database.Ticket.TryGetOpenTicket(command.Channel.Id, out Database.Ticket ticket))
{
await command.RespondAsync(new DiscordEmbedBuilder
{

View file

@ -11,6 +11,7 @@ using DSharpPlus.Exceptions;
using SupportChild.Interviews;
namespace SupportChild.Commands;
public class NewCommand
{
[RequireGuild]
@ -73,7 +74,7 @@ public class NewCommand
for (; nrOfButtons < 5 * (nrOfButtonRows + 1) && nrOfButtons < verifiedCategories.Count; nrOfButtons++)
{
buttonRow.Add(new DiscordButtonComponent(DiscordButtonStyle.Primary, "supportchild_newcommandbutton " + verifiedCategories[nrOfButtons].id, verifiedCategories[nrOfButtons].name));
buttonRow.Add(new DiscordButtonComponent(DiscordButtonStyle.Primary, "supportboi_newcommandbutton " + verifiedCategories[nrOfButtons].id, verifiedCategories[nrOfButtons].name));
}
builder.AddComponents(buttonRow);
}
@ -95,7 +96,7 @@ public class NewCommand
{
categoryOptions.Add(new DiscordSelectComponentOption(verifiedCategories[selectionOptions].name, verifiedCategories[selectionOptions].id.ToString()));
}
selectionComponents.Add(new DiscordSelectComponent("supportchild_newcommandselector" + selectionBoxes, "Open new ticket...", categoryOptions, false, 0, 1));
selectionComponents.Add(new DiscordSelectComponent("supportboi_newcommandselector" + selectionBoxes, "Open new ticket...", categoryOptions, false, 0, 1));
}
await command.RespondAsync(new DiscordInteractionResponseBuilder().AddComponents(selectionComponents).AsEphemeral());
@ -104,18 +105,18 @@ public class NewCommand
public static async Task<(bool, string)> OpenNewTicket(ulong userID, ulong commandChannelID, ulong categoryID)
{
// Check if user is blacklisted
if (Database.IsBlacklisted(userID))
if (Database.Blacklist.IsBanned(userID))
{
return (false, "You are banned from opening tickets.");
}
if (Database.IsOpenTicket(commandChannelID))
if (Database.Ticket.IsOpenTicket(commandChannelID))
{
return (false, "You cannot use this command in a ticket channel.");
}
if (!Database.IsStaff(userID)
&& Database.TryGetOpenTickets(userID, out List<Database.Ticket> ownTickets)
if (!Database.StaffMember.IsStaff(userID)
&& Database.Ticket.TryGetOpenTickets(userID, out List<Database.Ticket> ownTickets)
&& (ownTickets.Count >= Config.ticketLimit && Config.ticketLimit != 0))
{
return (false, "You have reached the limit for maximum open tickets.");
@ -173,7 +174,7 @@ public class NewCommand
assignedStaff = await RandomAssignCommand.GetRandomVerifiedStaffMember(ticketChannel, userID, 0, null);
}
long id = Database.NewTicket(member.Id, assignedStaff?.Id ?? 0, ticketChannel.Id);
long id = Database.Ticket.NewTicket(member.Id, assignedStaff?.Id ?? 0, ticketChannel.Id);
try
{
await ticketChannel.ModifyAsync(modifiedAttributes => modifiedAttributes.Name = "ticket-" + id.ToString("00000"));

View file

@ -20,7 +20,7 @@ public class RandomAssignCommand
public async Task OnExecute(SlashCommandContext command, [Parameter("role")][Description("(Optional) Limit the random assignment to a specific role.")] DiscordRole role = null)
{
// Check if ticket exists in the database
if (!Database.TryGetOpenTicket(command.Channel.Id, out Database.Ticket ticket))
if (!Database.Ticket.TryGetOpenTicket(command.Channel.Id, out Database.Ticket ticket))
{
await command.RespondAsync(new DiscordEmbedBuilder
{
@ -43,7 +43,7 @@ public class RandomAssignCommand
}
// Attempt to assign the staff member to the ticket
if (!Database.AssignStaff(ticket, staffMember.Id))
if (!Database.StaffMember.AssignStaff(ticket, staffMember.Id))
{
await command.RespondAsync(new DiscordEmbedBuilder
{
@ -85,14 +85,14 @@ public class RandomAssignCommand
if (targetRole == null)
{
// No role was specified, any active staff will be picked
staffMembers = Database.GetActiveStaff(ignoredUserIDs);
staffMembers = Database.StaffMember.GetActiveStaff(ignoredUserIDs);
}
else
{
// Check if role rassign should override staff's active status
staffMembers = Config.randomAssignRoleOverride
? Database.GetAllStaff(ignoredUserIDs)
: Database.GetActiveStaff(ignoredUserIDs);
? Database.StaffMember.GetAllStaff(ignoredUserIDs)
: Database.StaffMember.GetActiveStaff(ignoredUserIDs);
}
// Randomize the list before checking for roles in order to reduce number of API calls

View file

@ -16,7 +16,7 @@ public class RemoveCategoryCommand
public async Task OnExecute(SlashCommandContext command,
[Parameter("category")][Description("The category to remove.")] DiscordChannel category)
{
if (!Database.TryGetCategory(category.Id, out Database.Category _))
if (!Database.Category.TryGetCategory(category.Id, out Database.Category _))
{
await command.RespondAsync(new DiscordEmbedBuilder
{
@ -26,7 +26,7 @@ public class RemoveCategoryCommand
return;
}
if (Database.RemoveCategory(category.Id))
if (Database.Category.RemoveCategory(category.Id))
{
await command.RespondAsync(new DiscordEmbedBuilder
{

View file

@ -1,49 +0,0 @@
using System.ComponentModel;
using System.Globalization;
using DSharpPlus.Entities;
using System.Threading.Tasks;
using DSharpPlus.Commands;
using DSharpPlus.Commands.ContextChecks;
using DSharpPlus.Commands.Processors.SlashCommands;
using DSharpPlus.Exceptions;
namespace SupportChild.Commands;
public class RemoveMessageCommand
{
[RequireGuild]
[Command("removemessage")]
[Description("Removes a message from the 'say' command.")]
public async Task OnExecute(SlashCommandContext command,
[Parameter("identifier")][Description("The identifier word used in the /say command.")] string identifier)
{
if (!Database.TryGetMessage(identifier.ToLower(CultureInfo.InvariantCulture), out Database.Message _))
{
await command.RespondAsync(new DiscordEmbedBuilder
{
Color = DiscordColor.Red,
Description = "There is no message with that identifier."
}, true);
return;
}
if (Database.RemoveMessage(identifier))
{
await command.RespondAsync(new DiscordEmbedBuilder
{
Color = DiscordColor.Green,
Description = "Message removed."
}, true);
}
else
{
await command.RespondAsync(new DiscordEmbedBuilder
{
Color = DiscordColor.Red,
Description = "Error: Failed removing the message from the database."
}, true);
}
await LogChannel.Success("`" + identifier + "` was removed from the /say command by " + command.User.Mention + ".");
}
}

View file

@ -18,7 +18,7 @@ public class RemoveStaffCommand
public async Task OnExecute(SlashCommandContext command,
[Parameter("user")][Description("User to remove from staff.")] DiscordUser user)
{
if (!Database.IsStaff(user.Id))
if (!Database.StaffMember.IsStaff(user.Id))
{
await command.RespondAsync(new DiscordEmbedBuilder
{
@ -30,11 +30,11 @@ public class RemoveStaffCommand
await command.DeferResponseAsync(true);
if (Database.TryGetAssignedTickets(user.Id, out List<Database.Ticket> assignedTickets))
if (Database.Ticket.TryGetAssignedTickets(user.Id, out List<Database.Ticket> assignedTickets))
{
foreach (Database.Ticket assignedTicket in assignedTickets)
{
Database.UnassignStaff(assignedTicket);
Database.StaffMember.UnassignStaff(assignedTicket);
try
{
DiscordChannel ticketChannel = await SupportChild.client.GetChannelAsync(assignedTicket.channelID);
@ -53,19 +53,23 @@ public class RemoveStaffCommand
}
}
await using MySqlConnection c = Database.GetConnection();
c.Open();
MySqlCommand deletion = new MySqlCommand(@"DELETE FROM staff WHERE user_id=@user_id", c);
deletion.Parameters.AddWithValue("@user_id", user.Id);
await deletion.PrepareAsync();
deletion.ExecuteNonQuery();
await command.RespondAsync(new DiscordEmbedBuilder
if (Database.StaffMember.RemoveStaff(user.Id))
{
Color = DiscordColor.Green,
Description = "User was removed from staff."
}, true);
await command.RespondAsync(new DiscordEmbedBuilder
{
Color = DiscordColor.Green,
Description = "User was removed from staff."
}, true);
await LogChannel.Success(user.Mention + " was removed from staff by " + command.User.Mention + ".");
await LogChannel.Success(user.Mention + " was removed from staff by " + command.User.Mention + ".");
}
else
{
await command.RespondAsync(new DiscordEmbedBuilder
{
Color = DiscordColor.Red,
Description = "Error: Failed to remove user from staff."
}, true);
}
}
}

View file

@ -27,7 +27,7 @@ public class SayCommand
return;
}
if (!Database.TryGetMessage(identifier.ToLower(CultureInfo.InvariantCulture),
if (!Database.Message.TryGetMessage(identifier.ToLower(CultureInfo.InvariantCulture),
out Database.Message message))
{
await command.RespondAsync(new DiscordEmbedBuilder
@ -44,12 +44,12 @@ public class SayCommand
Description = message.message.Replace("\\n", "\n")
});
await LogChannel.Success(command.User.Mention + " posted the " + message.identifier + " message in " + command.Channel.Mention + ".");
await LogChannel.Success(command.User.Mention + " posted the `" + message.identifier + "` message in " + command.Channel.Mention + ".");
}
private static async Task SendMessageList(SlashCommandContext command)
{
List<Database.Message> messages = Database.GetAllMessages();
List<Database.Message> messages = Database.Message.GetAllMessages();
if (messages.Count == 0)
{
await command.RespondAsync(new DiscordEmbedBuilder

View file

@ -0,0 +1,199 @@
using System.Collections.Generic;
using System.ComponentModel;
using System.Globalization;
using DSharpPlus.Entities;
using System.Threading.Tasks;
using DSharpPlus.Commands;
using DSharpPlus.Commands.ContextChecks;
using DSharpPlus.Commands.Processors.SlashCommands;
namespace SupportChild.Commands;
public class SetMessageCommand
{
private readonly struct CommandInfo(string identifier, string message)
{
public string identifier { get; } = identifier;
public string message { get; } = message;
}
private static Dictionary<ulong, CommandInfo> waitingCommands = new();
[RequireGuild]
[Command("setmessage")]
[Description("Adds or updates message for the 'say' command.")]
public async Task OnExecute(SlashCommandContext command,
[Parameter("identifier")][Description("The identifier word used in the /say command.")] string identifier,
[Parameter("message")][Description("The message the /say command will return. Empty to delete message.")] string message = "")
{
if (Database.Message.TryGetMessage(identifier.ToLower(CultureInfo.InvariantCulture), out Database.Message oldMessage))
{
if (string.IsNullOrEmpty(message))
{
await command.RespondAsync(new DiscordInteractionResponseBuilder()
.AddEmbed(new DiscordEmbedBuilder
{
Color = DiscordColor.Orange,
Description = "Are you sure you want to delete the `" + identifier + "` message?"
})
.AddComponents(new DiscordButtonComponent(DiscordButtonStyle.Danger, "supportboi_confirmmessagedelete " + command.Interaction.Id, "Confirm"),
new DiscordButtonComponent(DiscordButtonStyle.Secondary, "supportboi_cancelmessagedelete " + command.Interaction.Id, "Cancel")));
}
else
{
await command.RespondAsync(new DiscordInteractionResponseBuilder()
.AddEmbed(new DiscordEmbedBuilder
{
Color = DiscordColor.Cyan,
Title = "Replace the `" + identifier + "` message?"
}
.AddField("Old message:", oldMessage.message.Truncate(1024)).AddField("New message:", message.Truncate(1024)))
.AddComponents(new DiscordButtonComponent(DiscordButtonStyle.Success, "supportboi_confirmmessageupdate " + command.Interaction.Id, "Confirm"),
new DiscordButtonComponent(DiscordButtonStyle.Secondary, "supportboi_cancelmessageupdate " + command.Interaction.Id, "Cancel")));
}
waitingCommands.Add(command.Interaction.Id, new CommandInfo(identifier, message));
}
else
{
if (string.IsNullOrEmpty(message))
{
await command.RespondAsync(new DiscordEmbedBuilder
{
Color = DiscordColor.Red,
Description = "Cannot delete that message, it does not exist."
}, true);
}
else
{
await AddNewMessage(command, identifier, message);
}
}
}
private static async Task AddNewMessage(SlashCommandContext command, string identifier, string message)
{
if (Database.Message.AddMessage(identifier, command.Member.Id, message))
{
await command.RespondAsync(new DiscordEmbedBuilder
{
Color = DiscordColor.Green,
Description = "Message added."
}, true);
}
else
{
await command.RespondAsync(new DiscordEmbedBuilder
{
Color = DiscordColor.Red,
Description = "Error: Failed adding the message to the database."
}, true);
}
await LogChannel.Success(command.User.Mention + " added the `" + identifier + "` message for the /say command.\n\n**Content:**\n\n" + message);
}
public static async Task ConfirmMessageDeletion(DiscordInteraction interaction, ulong previousInteractionID)
{
if (!waitingCommands.Remove(previousInteractionID, out CommandInfo command))
{
await interaction.CreateResponseAsync(DiscordInteractionResponseType.UpdateMessage, new DiscordInteractionResponseBuilder().AddEmbed(new DiscordEmbedBuilder
{
Color = DiscordColor.Red,
Description = "I don't remember which message you wanted to delete, has the bot been restarted since the command was used?"
}));
return;
}
if (!Database.Message.TryGetMessage(command.identifier.ToLower(CultureInfo.InvariantCulture), out Database.Message _))
{
await interaction.CreateResponseAsync(DiscordInteractionResponseType.UpdateMessage, new DiscordInteractionResponseBuilder().AddEmbed(new DiscordEmbedBuilder
{
Color = DiscordColor.Red,
Description = "There is no message with that identifier."
}));
return;
}
if (Database.Message.RemoveMessage(command.identifier))
{
await interaction.CreateResponseAsync(DiscordInteractionResponseType.UpdateMessage, new DiscordInteractionResponseBuilder().AddEmbed(new DiscordEmbedBuilder
{
Color = DiscordColor.Green,
Description = "Message removed."
}));
}
else
{
await interaction.CreateResponseAsync(DiscordInteractionResponseType.UpdateMessage, new DiscordInteractionResponseBuilder().AddEmbed(new DiscordEmbedBuilder
{
Color = DiscordColor.Red,
Description = "Error: Failed removing the message from the database."
}));
}
await LogChannel.Success("`" + command.identifier + "` was removed from the /say command by " + interaction.User.Mention + ".");
}
public static async Task CancelMessageDeletion(DiscordInteraction interaction, ulong previousInteractionID)
{
waitingCommands.Remove(previousInteractionID);
if (interaction.Message != null)
{
await interaction.Message.DeleteAsync();
}
}
public static async Task ConfirmMessageUpdate(DiscordInteraction interaction, ulong previousInteractionID)
{
if (!waitingCommands.Remove(previousInteractionID, out CommandInfo command))
{
await interaction.CreateResponseAsync(DiscordInteractionResponseType.UpdateMessage, new DiscordInteractionResponseBuilder().AddEmbed(new DiscordEmbedBuilder
{
Color = DiscordColor.Red,
Description = "I don't remember which message you wanted to update, has the bot been restarted since the command was used?"
}));
return;
}
if (!Database.Message.TryGetMessage(command.identifier.ToLower(CultureInfo.InvariantCulture), out Database.Message _))
{
await interaction.CreateResponseAsync(DiscordInteractionResponseType.UpdateMessage, new DiscordInteractionResponseBuilder().AddEmbed(new DiscordEmbedBuilder
{
Color = DiscordColor.Red,
Description = "There is no message with that identifier."
}));
return;
}
if (Database.Message.UpdateMessage(command.identifier, interaction.User.Id, command.message))
{
await interaction.CreateResponseAsync(DiscordInteractionResponseType.UpdateMessage, new DiscordInteractionResponseBuilder().AddEmbed(new DiscordEmbedBuilder
{
Color = DiscordColor.Green,
Description = "Message updated."
}));
}
else
{
await interaction.CreateResponseAsync(DiscordInteractionResponseType.UpdateMessage, new DiscordInteractionResponseBuilder().AddEmbed(new DiscordEmbedBuilder
{
Color = DiscordColor.Red,
Description = "Error: Failed updating the message in the database."
}));
}
await LogChannel.Success("`" + command.identifier + "` was updated for the /say command by " + interaction.User.Mention + ".");
}
public static async Task CancelMessageUpdate(DiscordInteraction interaction, ulong previousInteractionID)
{
waitingCommands.Remove(previousInteractionID);
if (interaction.Message != null)
{
await interaction.Message.DeleteAsync();
}
}
}

View file

@ -16,9 +16,8 @@ public class SetSummaryCommand
[Description("Sets a ticket's summary for the summary command.")]
public async Task OnExecute(SlashCommandContext command, [Parameter("Summary")][Description("The ticket summary text.")] string summary)
{
ulong channelID = command.Channel.Id;
// Check if ticket exists in the database
if (!Database.TryGetOpenTicket(command.Channel.Id, out Database.Ticket ticket))
if (!Database.Ticket.TryGetOpenTicket(command.Channel.Id, out Database.Ticket ticket))
{
await command.RespondAsync(new DiscordEmbedBuilder
{
@ -28,21 +27,23 @@ public class SetSummaryCommand
return;
}
await using MySqlConnection c = Database.GetConnection();
c.Open();
MySqlCommand update = new MySqlCommand(@"UPDATE tickets SET summary = @summary WHERE channel_id = @channel_id", c);
update.Parameters.AddWithValue("@summary", summary);
update.Parameters.AddWithValue("@channel_id", channelID);
await update.PrepareAsync();
update.ExecuteNonQuery();
update.Dispose();
await command.RespondAsync(new DiscordEmbedBuilder
if (Database.Ticket.SetSummary(command.Channel.Id, summary))
{
Color = DiscordColor.Green,
Description = "Summary set."
}, true);
await command.RespondAsync(new DiscordEmbedBuilder
{
Color = DiscordColor.Green,
Description = "Summary set."
}, true);
await LogChannel.Success(command.User.Mention + " set the summary for " + command.Channel.Mention + " to:\n\n" + summary, ticket.id);
await LogChannel.Success(command.User.Mention + " set the summary for " + command.Channel.Mention + " to:\n\n" + summary, ticket.id);
}
else
{
await command.RespondAsync(new DiscordEmbedBuilder
{
Color = DiscordColor.Red,
Description = "Error: Failed setting the summary of a ticket in database."
}, true);
}
}
}

View file

@ -14,11 +14,11 @@ public class StatusCommand
[Description("Shows bot status and information.")]
public async Task OnExecute(SlashCommandContext command)
{
long openTickets = Database.GetNumberOfTickets();
long closedTickets = Database.GetNumberOfClosedTickets();
long openTickets = Database.Ticket.GetNumberOfTickets();
long closedTickets = Database.Ticket.GetNumberOfClosedTickets();
DiscordEmbed botInfo = new DiscordEmbedBuilder()
.WithAuthor("Emotion-stuff/supportchild @ Toastielab", "https://toastielab.dev/toastie-stuff/SupportChild", "https://cdn.discordapp.com/attachments/765441543100170271/914327948667011132/Ellie_Concept_2_transparent_ver.png")
.WithAuthor("toastie-stuff/supportchild @ Toastielab", "https://toastielab.dev/toastie-stuff/SupportChild", "https://cdn.discordapp.com/attachments/765441543100170271/914327948667011132/Ellie_Concept_2_transparent_ver.png")
.WithTitle("Bot information")
.WithColor(DiscordColor.Cyan)
.AddField("Version:", SupportChild.GetVersion(), true)

View file

@ -14,7 +14,7 @@ public class SummaryCommand
[Description("Shows ticket information and summary if there is one.")]
public async Task OnExecute(SlashCommandContext command)
{
if (!Database.TryGetOpenTicket(command.Channel.Id, out Database.Ticket ticket))
if (!Database.Ticket.TryGetOpenTicket(command.Channel.Id, out Database.Ticket ticket))
{
await command.RespondAsync(new DiscordEmbedBuilder
{

View file

@ -18,7 +18,7 @@ public class ToggleActiveCommand
DiscordUser staffUser = user == null ? command.User : user;
// Check if ticket exists in the database
if (!Database.TryGetStaff(staffUser.Id, out Database.StaffMember staffMember))
if (!Database.StaffMember.TryGetStaff(staffUser.Id, out Database.StaffMember staffMember))
{
await command.RespondAsync(new DiscordEmbedBuilder
{
@ -28,7 +28,7 @@ public class ToggleActiveCommand
return;
}
if (Database.SetStaffActive(staffUser.Id, !staffMember.active))
if (Database.StaffMember.SetStaffActive(staffUser.Id, !staffMember.active))
{
if (user != null && user.Id != command.User.Id)
{

View file

@ -23,7 +23,7 @@ public class TranscriptCommand
Database.Ticket ticket;
if (ticketID == 0) // If there are no arguments use current channel
{
if (Database.TryGetOpenTicket(command.Channel.Id, out ticket))
if (Database.Ticket.TryGetOpenTicket(command.Channel.Id, out ticket))
{
try
{
@ -52,7 +52,7 @@ public class TranscriptCommand
else
{
// If the ticket is still open, generate a new fresh transcript
if (Database.TryGetOpenTicketByID((uint)ticketID, out ticket) && ticket?.creatorID == command.Member.Id)
if (Database.Ticket.TryGetOpenTicketByID((uint)ticketID, out ticket) && ticket?.creatorID == command.Member.Id)
{
try
{
@ -70,7 +70,7 @@ public class TranscriptCommand
}
// If there is no open or closed ticket, send an error. If there is a closed ticket we will simply use the old transcript from when the ticket was closed.
else if (!Database.TryGetClosedTicket((uint)ticketID, out ticket) || (ticket?.creatorID != command.Member.Id && !Database.IsStaff(command.Member.Id)))
else if (!Database.Ticket.TryGetClosedTicket((uint)ticketID, out ticket) || (ticket?.creatorID != command.Member.Id && !Database.StaffMember.IsStaff(command.Member.Id)))
{
await command.EditResponseAsync(new DiscordWebhookBuilder().AddEmbed(new DiscordEmbedBuilder
{
@ -83,18 +83,22 @@ public class TranscriptCommand
string fileName = Transcriber.GetZipFilename(ticket.id);
string filePath = Transcriber.GetZipPath(ticket.id);
long zipSize = 0;
bool zipTooLarge = false;
// If the zip transcript doesn't exist, use the html file.
// If the zip transcript doesn't exist or is too large, use the html file.
try
{
FileInfo fileInfo = new FileInfo(filePath);
if (!fileInfo.Exists || fileInfo.Length >= 26214400)
FileInfo zipFile = new(filePath);
if (!zipFile.Exists || zipFile.Length >= 26214400)
{
fileName = Transcriber.GetHTMLFilename(ticket.id);
filePath = Transcriber.GetHtmlPath(ticket.id);
}
zipSize = fileInfo.Length;
if (zipFile.Exists && zipFile.Length >= 26214400)
{
zipTooLarge = true;
}
}
catch (Exception e)
{
@ -121,7 +125,7 @@ public class TranscriptCommand
try
{
await using FileStream file = new FileStream(filePath, FileMode.Open, FileAccess.Read);
await using FileStream file = new(filePath, FileMode.Open, FileAccess.Read);
await LogChannel.Success("Transcript generated by " + command.User.Mention + ".", ticket.id, new Utilities.File(fileName, file));
}
catch (Exception e)
@ -129,26 +133,14 @@ public class TranscriptCommand
Logger.Error("Failed to log transcript generation.", e);
}
if (await SendDirectMessage(command, fileName, filePath, zipSize, ticket.id))
{
await command.EditResponseAsync(new DiscordWebhookBuilder().AddEmbed(new DiscordEmbedBuilder
{
Color = DiscordColor.Green,
Description = "Transcript sent!\n"
}));
}
}
private static async Task<bool> SendDirectMessage(SlashCommandContext command, string fileName, string filePath, long zipSize, uint ticketID)
{
try
{
// Send transcript in a direct message
await using FileStream file = new FileStream(filePath, FileMode.Open, FileAccess.Read);
await using FileStream file = new(filePath, FileMode.Open, FileAccess.Read);
DiscordMessageBuilder directMessage = new DiscordMessageBuilder();
DiscordMessageBuilder directMessage = new();
if (zipSize >= 26214400)
if (zipTooLarge)
{
directMessage.AddEmbed(new DiscordEmbedBuilder
{
@ -176,7 +168,12 @@ public class TranscriptCommand
directMessage.AddFiles(new Dictionary<string, Stream> { { fileName, file } });
await command.Member.SendMessageAsync(directMessage);
return true;
await command.EditResponseAsync(new DiscordWebhookBuilder().AddEmbed(new DiscordEmbedBuilder
{
Color = DiscordColor.Green,
Description = "Transcript sent!\n"
}));
}
catch (UnauthorizedException)
{
@ -185,7 +182,6 @@ public class TranscriptCommand
Color = DiscordColor.Red,
Description = "Not allowed to send direct message to you, please check your privacy settings.\n"
}));
return false;
}
}
}

View file

@ -16,7 +16,7 @@ public class UnassignCommand
public async Task OnExecute(SlashCommandContext command)
{
// Check if ticket exists in the database
if (!Database.TryGetOpenTicket(command.Channel.Id, out Database.Ticket ticket))
if (!Database.Ticket.TryGetOpenTicket(command.Channel.Id, out Database.Ticket ticket))
{
await command.RespondAsync(new DiscordEmbedBuilder
{
@ -26,7 +26,7 @@ public class UnassignCommand
return;
}
if (!Database.UnassignStaff(ticket))
if (!Database.StaffMember.UnassignStaff(ticket))
{
await command.RespondAsync(new DiscordEmbedBuilder
{

View file

@ -18,7 +18,7 @@ public class UnblacklistCommand
{
try
{
if (!Database.Unblacklist(user.Id))
if (!Database.Blacklist.Unban(user.Id))
{
await command.RespondAsync(new DiscordEmbedBuilder
{

View file

@ -1,983 +0,0 @@
using System;
using System.Linq;
using System.Collections.Generic;
using System.Security.Cryptography;
using DSharpPlus;
using MySqlConnector;
using Newtonsoft.Json;
using Newtonsoft.Json.Serialization;
using SupportChild.Interviews;
namespace SupportChild;
public static class Database
{
private static string connectionString = "";
public static void SetConnectionString(string host, int port, string database, string username, string password)
{
connectionString = "server=" + host +
";database=" + database +
";port=" + port +
";userid=" + username +
";password=" + password;
}
public static MySqlConnection GetConnection()
{
return new MySqlConnection(connectionString);
}
public static long GetNumberOfTickets()
{
try
{
using MySqlConnection c = GetConnection();
using MySqlCommand countTickets = new MySqlCommand("SELECT COUNT(*) FROM tickets", c);
c.Open();
return (long)countTickets.ExecuteScalar();
}
catch (MySqlException e)
{
Logger.Error("Error occured when attempting to count number of open tickets.", e);
}
return -1;
}
public static long GetNumberOfClosedTickets()
{
try
{
using MySqlConnection c = GetConnection();
using MySqlCommand countTickets = new MySqlCommand("SELECT COUNT(*) FROM ticket_history", c);
c.Open();
return (long)countTickets.ExecuteScalar();
}
catch (MySqlException e)
{
Logger.Error("Error occured when attempting to count number of open tickets.", e);
}
return -1;
}
public static void SetupTables()
{
using MySqlConnection c = GetConnection();
using MySqlCommand createTickets = new MySqlCommand(
"CREATE TABLE IF NOT EXISTS tickets(" +
"id INT UNSIGNED NOT NULL PRIMARY KEY AUTO_INCREMENT," +
"created_time DATETIME NOT NULL," +
"creator_id BIGINT UNSIGNED NOT NULL," +
"assigned_staff_id BIGINT UNSIGNED NOT NULL DEFAULT 0," +
"summary VARCHAR(5000) NOT NULL," +
"channel_id BIGINT UNSIGNED NOT NULL UNIQUE," +
"INDEX(created_time, assigned_staff_id, channel_id))",
c);
using MySqlCommand createTicketHistory = new MySqlCommand(
"CREATE TABLE IF NOT EXISTS ticket_history(" +
"id INT UNSIGNED NOT NULL PRIMARY KEY," +
"created_time DATETIME NOT NULL," +
"closed_time DATETIME NOT NULL," +
"creator_id BIGINT UNSIGNED NOT NULL," +
"assigned_staff_id BIGINT UNSIGNED NOT NULL DEFAULT 0," +
"summary VARCHAR(5000) NOT NULL," +
"channel_id BIGINT UNSIGNED NOT NULL UNIQUE," +
"INDEX(created_time, closed_time, channel_id))",
c);
using MySqlCommand createBlacklisted = new MySqlCommand(
"CREATE TABLE IF NOT EXISTS blacklisted_users(" +
"user_id BIGINT UNSIGNED NOT NULL PRIMARY KEY," +
"time DATETIME NOT NULL," +
"moderator_id BIGINT UNSIGNED NOT NULL," +
"INDEX(user_id, time))",
c);
using MySqlCommand createStaffList = new MySqlCommand(
"CREATE TABLE IF NOT EXISTS staff(" +
"user_id BIGINT UNSIGNED NOT NULL PRIMARY KEY," +
"name VARCHAR(256) NOT NULL," +
"active BOOLEAN NOT NULL DEFAULT true)",
c);
using MySqlCommand createMessages = new MySqlCommand(
"CREATE TABLE IF NOT EXISTS messages(" +
"identifier VARCHAR(256) NOT NULL PRIMARY KEY," +
"user_id BIGINT UNSIGNED NOT NULL," +
"message VARCHAR(5000) NOT NULL)",
c);
using MySqlCommand createCategories = new MySqlCommand(
"CREATE TABLE IF NOT EXISTS categories(" +
"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," +
"definitions JSON NOT NULL)",
c);
using MySqlCommand createInterviewTemplates = new MySqlCommand(
"CREATE TABLE IF NOT EXISTS interview_templates(" +
"category_id BIGINT UNSIGNED NOT NULL PRIMARY KEY," +
"template JSON NOT NULL)",
c);
c.Open();
createTickets.ExecuteNonQuery();
createBlacklisted.ExecuteNonQuery();
createTicketHistory.ExecuteNonQuery();
createStaffList.ExecuteNonQuery();
createMessages.ExecuteNonQuery();
createCategories.ExecuteNonQuery();
createInterviews.ExecuteNonQuery();
createInterviewTemplates.ExecuteNonQuery();
}
public static bool IsOpenTicket(ulong channelID)
{
using MySqlConnection c = GetConnection();
c.Open();
using MySqlCommand selection = new MySqlCommand(@"SELECT * FROM tickets 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())
{
return false;
}
results.Close();
return true;
}
public static bool TryGetOpenTicket(ulong channelID, out Ticket ticket)
{
using MySqlConnection c = GetConnection();
c.Open();
using MySqlCommand selection = new MySqlCommand(@"SELECT * FROM tickets 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())
{
ticket = null;
return false;
}
ticket = new Ticket(results);
results.Close();
return true;
}
public static bool TryGetOpenTicketByID(uint id, out Ticket ticket)
{
using MySqlConnection c = GetConnection();
c.Open();
using MySqlCommand selection = new MySqlCommand(@"SELECT * FROM tickets WHERE id=@id", c);
selection.Parameters.AddWithValue("@id", id);
selection.Prepare();
MySqlDataReader results = selection.ExecuteReader();
// Check if open ticket exists in the database
if (results.Read())
{
ticket = new Ticket(results);
results.Close();
return true;
}
results.Close();
ticket = null;
return false;
}
public static bool TryGetClosedTicket(uint id, out Ticket ticket)
{
using MySqlConnection c = GetConnection();
c.Open();
using MySqlCommand selection = new MySqlCommand(@"SELECT * FROM ticket_history WHERE id=@id", c);
selection.Parameters.AddWithValue("@id", id);
selection.Prepare();
MySqlDataReader results = selection.ExecuteReader();
// Check if closed ticket exists in the database
if (results.Read())
{
ticket = new Ticket(results);
results.Close();
return true;
}
ticket = null;
results.Close();
return false;
}
public static bool TryGetOpenTickets(ulong userID, out List<Ticket> tickets)
{
tickets = null;
using MySqlConnection c = GetConnection();
c.Open();
using MySqlCommand selection = new MySqlCommand(@"SELECT * FROM tickets WHERE creator_id=@creator_id", c);
selection.Parameters.AddWithValue("@creator_id", userID);
selection.Prepare();
MySqlDataReader results = selection.ExecuteReader();
if (!results.Read())
{
return false;
}
tickets = new List<Ticket> { new Ticket(results) };
while (results.Read())
{
tickets.Add(new Ticket(results));
}
results.Close();
return true;
}
public static bool TryGetOpenTickets(out List<Ticket> tickets)
{
tickets = null;
using MySqlConnection c = GetConnection();
c.Open();
using MySqlCommand selection = new MySqlCommand(@"SELECT * FROM tickets ORDER BY channel_id ASC", c);
selection.Prepare();
MySqlDataReader results = selection.ExecuteReader();
if (!results.Read())
{
return false;
}
tickets = new List<Ticket> { new Ticket(results) };
while (results.Read())
{
tickets.Add(new Ticket(results));
}
results.Close();
return true;
}
public static bool TryGetClosedTickets(ulong userID, out List<Ticket> tickets)
{
tickets = null;
using MySqlConnection c = GetConnection();
c.Open();
using MySqlCommand selection = new MySqlCommand(@"SELECT * FROM ticket_history WHERE creator_id=@creator_id", c);
selection.Parameters.AddWithValue("@creator_id", userID);
selection.Prepare();
MySqlDataReader results = selection.ExecuteReader();
if (!results.Read())
{
return false;
}
tickets = new List<Ticket> { new Ticket(results) };
while (results.Read())
{
tickets.Add(new Ticket(results));
}
results.Close();
return true;
}
public static bool TryGetAssignedTickets(ulong staffID, out List<Ticket> tickets)
{
tickets = null;
using MySqlConnection c = GetConnection();
c.Open();
using MySqlCommand selection = new MySqlCommand(@"SELECT * FROM tickets WHERE assigned_staff_id=@assigned_staff_id", c);
selection.Parameters.AddWithValue("@assigned_staff_id", staffID);
selection.Prepare();
MySqlDataReader results = selection.ExecuteReader();
if (!results.Read())
{
return false;
}
tickets = new List<Ticket> { new Ticket(results) };
while (results.Read())
{
tickets.Add(new Ticket(results));
}
results.Close();
return true;
}
public static long NewTicket(ulong memberID, ulong staffID, ulong ticketID)
{
using MySqlConnection c = GetConnection();
c.Open();
using MySqlCommand cmd = new MySqlCommand(@"INSERT INTO tickets (created_time, creator_id, assigned_staff_id, summary, channel_id) VALUES (UTC_TIMESTAMP(), @creator_id, @assigned_staff_id, @summary, @channel_id);", c);
cmd.Parameters.AddWithValue("@creator_id", memberID);
cmd.Parameters.AddWithValue("@assigned_staff_id", staffID);
cmd.Parameters.AddWithValue("@summary", "");
cmd.Parameters.AddWithValue("@channel_id", ticketID);
cmd.ExecuteNonQuery();
return cmd.LastInsertedId;
}
public static void ArchiveTicket(Ticket ticket)
{
// Check if ticket already exists in the archive
if (TryGetClosedTicket(ticket.id, out Ticket _))
{
using MySqlConnection c = GetConnection();
using MySqlCommand deleteTicket = new MySqlCommand(@"DELETE FROM ticket_history WHERE id=@id OR channel_id=@channel_id", c);
deleteTicket.Parameters.AddWithValue("@id", ticket.id);
deleteTicket.Parameters.AddWithValue("@channel_id", ticket.channelID);
c.Open();
deleteTicket.Prepare();
deleteTicket.ExecuteNonQuery();
}
// Create an entry in the ticket history database
using MySqlConnection conn = GetConnection();
using MySqlCommand archiveTicket = new MySqlCommand(@"INSERT INTO ticket_history (id, created_time, closed_time, creator_id, assigned_staff_id, summary, channel_id) VALUES (@id, @created_time, UTC_TIMESTAMP(), @creator_id, @assigned_staff_id, @summary, @channel_id);", conn);
archiveTicket.Parameters.AddWithValue("@id", ticket.id);
archiveTicket.Parameters.AddWithValue("@created_time", ticket.channelID.GetSnowflakeTime());
archiveTicket.Parameters.AddWithValue("@creator_id", ticket.creatorID);
archiveTicket.Parameters.AddWithValue("@assigned_staff_id", ticket.assignedStaffID);
archiveTicket.Parameters.AddWithValue("@summary", ticket.summary);
archiveTicket.Parameters.AddWithValue("@channel_id", ticket.channelID);
conn.Open();
archiveTicket.Prepare();
archiveTicket.ExecuteNonQuery();
}
public static bool DeleteOpenTicket(uint ticketID)
{
try
{
using MySqlConnection c = GetConnection();
using MySqlCommand deletion = new MySqlCommand(@"DELETE FROM tickets WHERE id=@id", c);
deletion.Parameters.AddWithValue("@id", ticketID);
c.Open();
deletion.Prepare();
return deletion.ExecuteNonQuery() > 0;
}
catch (MySqlException e)
{
Logger.Warn("Could not delete open ticket in database.", e);
return false;
}
}
public static bool IsBlacklisted(ulong userID)
{
using MySqlConnection c = GetConnection();
c.Open();
using MySqlCommand selection = new MySqlCommand(@"SELECT * FROM blacklisted_users WHERE user_id=@user_id", c);
selection.Parameters.AddWithValue("@user_id", userID);
selection.Prepare();
MySqlDataReader results = selection.ExecuteReader();
// Check if user is blacklisted
if (results.Read())
{
results.Close();
return true;
}
results.Close();
return false;
}
public static bool Blacklist(ulong blacklistedID, ulong staffID)
{
try
{
using MySqlConnection c = GetConnection();
c.Open();
using MySqlCommand cmd = new MySqlCommand(@"INSERT INTO blacklisted_users (user_id,time,moderator_id) VALUES (@user_id, UTC_TIMESTAMP(), @moderator_id);", c);
cmd.Parameters.AddWithValue("@user_id", blacklistedID);
cmd.Parameters.AddWithValue("@moderator_id", staffID);
cmd.Prepare();
return cmd.ExecuteNonQuery() > 0;
}
catch (MySqlException e)
{
Logger.Warn("Could not blacklist user in database.", e);
return false;
}
}
public static bool Unblacklist(ulong blacklistedID)
{
try
{
using MySqlConnection c = GetConnection();
c.Open();
using MySqlCommand cmd = new MySqlCommand(@"DELETE FROM blacklisted_users WHERE user_id=@user_id", c);
cmd.Parameters.AddWithValue("@user_id", blacklistedID);
cmd.Prepare();
return cmd.ExecuteNonQuery() > 0;
}
catch (MySqlException e)
{
Logger.Warn("Could not unblacklist user in database.", e);
return false;
}
}
public static bool AssignStaff(Ticket ticket, ulong staffID)
{
try
{
using MySqlConnection c = GetConnection();
c.Open();
using MySqlCommand update = new MySqlCommand(@"UPDATE tickets SET assigned_staff_id = @assigned_staff_id WHERE id = @id", c);
update.Parameters.AddWithValue("@assigned_staff_id", staffID);
update.Parameters.AddWithValue("@id", ticket.id);
update.Prepare();
return update.ExecuteNonQuery() > 0;
}
catch (MySqlException e)
{
Logger.Warn("Could not add staff to ticket in database.", e);
return false;
}
}
public static bool UnassignStaff(Ticket ticket)
{
return AssignStaff(ticket, 0);
}
public static bool SetStaffActive(ulong staffID, bool active)
{
try
{
using MySqlConnection c = GetConnection();
c.Open();
MySqlCommand update = new MySqlCommand(@"UPDATE staff SET active = @active WHERE user_id = @user_id", c);
update.Parameters.AddWithValue("@user_id", staffID);
update.Parameters.AddWithValue("@active", active);
update.Prepare();
return update.ExecuteNonQuery() > 0;
}
catch (MySqlException e)
{
Logger.Warn("Could not set staff member as active in database.", e);
return false;
}
}
public static List<StaffMember> GetActiveStaff(params ulong[] ignoredUserIDs)
{
using MySqlConnection c = GetConnection();
c.Open();
using MySqlCommand selection = new MySqlCommand(@"SELECT * FROM staff WHERE active = true AND user_id NOT IN (@user_ids)", c);
selection.Parameters.AddWithValue("@user_ids", string.Join(",", ignoredUserIDs));
selection.Prepare();
MySqlDataReader results = selection.ExecuteReader();
if (!results.Read())
{
return new List<StaffMember>();
}
List<StaffMember> staffMembers = new List<StaffMember> { new StaffMember(results) };
while (results.Read())
{
staffMembers.Add(new StaffMember(results));
}
results.Close();
return staffMembers;
}
public static List<StaffMember> GetAllStaff(params ulong[] ignoredUserIDs)
{
using MySqlConnection c = GetConnection();
c.Open();
using MySqlCommand selection = new MySqlCommand(@"SELECT * FROM staff WHERE user_id NOT IN (@user_ids)", c);
selection.Parameters.AddWithValue("@user_ids", string.Join(",", ignoredUserIDs));
selection.Prepare();
MySqlDataReader results = selection.ExecuteReader();
if (!results.Read())
{
return new List<StaffMember>();
}
List<StaffMember> staffMembers = new List<StaffMember> { new StaffMember(results) };
while (results.Read())
{
staffMembers.Add(new StaffMember(results));
}
results.Close();
return staffMembers;
}
public static bool IsStaff(ulong staffID)
{
using MySqlConnection c = GetConnection();
c.Open();
using MySqlCommand selection = new MySqlCommand(@"SELECT * FROM staff WHERE user_id=@user_id", c);
selection.Parameters.AddWithValue("@user_id", staffID);
selection.Prepare();
MySqlDataReader results = selection.ExecuteReader();
if (!results.Read())
{
return false;
}
results.Close();
return true;
}
public static bool TryGetStaff(ulong staffID, out StaffMember staffMember)
{
using MySqlConnection c = GetConnection();
c.Open();
using MySqlCommand selection = new MySqlCommand(@"SELECT * FROM staff WHERE user_id=@user_id", c);
selection.Parameters.AddWithValue("@user_id", staffID);
selection.Prepare();
MySqlDataReader results = selection.ExecuteReader();
if (!results.Read())
{
staffMember = null;
return false;
}
staffMember = new StaffMember(results);
results.Close();
return true;
}
public static List<Message> GetAllMessages()
{
using MySqlConnection c = GetConnection();
c.Open();
using MySqlCommand selection = new MySqlCommand(@"SELECT * FROM messages", c);
selection.Prepare();
MySqlDataReader results = selection.ExecuteReader();
if (!results.Read())
{
return new List<Message>();
}
List<Message> messages = new List<Message> { new Message(results) };
while (results.Read())
{
messages.Add(new Message(results));
}
results.Close();
return messages;
}
public static bool TryGetMessage(string identifier, out Message message)
{
using MySqlConnection c = GetConnection();
c.Open();
using MySqlCommand selection = new MySqlCommand(@"SELECT * FROM messages WHERE identifier=@identifier", c);
selection.Parameters.AddWithValue("@identifier", identifier);
selection.Prepare();
MySqlDataReader results = selection.ExecuteReader();
// Check if ticket exists in the database
if (!results.Read())
{
message = null;
return false;
}
message = new Message(results);
results.Close();
return true;
}
public static bool AddMessage(string identifier, ulong userID, string message)
{
try
{
using MySqlConnection c = GetConnection();
c.Open();
using MySqlCommand cmd = new MySqlCommand(@"INSERT INTO messages (identifier,user_id,message) VALUES (@identifier, @user_id, @message);", c);
cmd.Parameters.AddWithValue("@identifier", identifier);
cmd.Parameters.AddWithValue("@user_id", userID);
cmd.Parameters.AddWithValue("@message", message);
cmd.Prepare();
return cmd.ExecuteNonQuery() > 0;
}
catch (MySqlException e)
{
Logger.Error("Could not add message to database.", e);
return false;
}
}
public static bool RemoveMessage(string identifier)
{
try
{
using MySqlConnection c = GetConnection();
c.Open();
using MySqlCommand cmd = new MySqlCommand(@"DELETE FROM messages WHERE identifier=@identifier", c);
cmd.Parameters.AddWithValue("@identifier", identifier);
cmd.Prepare();
return cmd.ExecuteNonQuery() > 0;
}
catch (MySqlException e)
{
Logger.Error("Could not remove message from database.", e);
return false;
}
}
public static List<Category> GetAllCategories()
{
using MySqlConnection c = GetConnection();
c.Open();
using MySqlCommand selection = new MySqlCommand(@"SELECT * FROM categories", c);
selection.Prepare();
MySqlDataReader results = selection.ExecuteReader();
if (!results.Read())
{
return new List<Category>();
}
List<Category> categories = new List<Category> { new Category(results) };
while (results.Read())
{
categories.Add(new Category(results));
}
results.Close();
return categories;
}
public static bool TryGetCategory(ulong categoryID, out Category message)
{
using MySqlConnection c = GetConnection();
c.Open();
using MySqlCommand selection = new MySqlCommand(@"SELECT * FROM categories WHERE category_id=@category_id", c);
selection.Parameters.AddWithValue("@category_id", categoryID);
selection.Prepare();
MySqlDataReader results = selection.ExecuteReader();
if (!results.Read())
{
message = null;
return false;
}
message = new Category(results);
results.Close();
return true;
}
public static bool TryGetCategory(string name, out Category message)
{
using MySqlConnection c = GetConnection();
c.Open();
using MySqlCommand selection = new MySqlCommand(@"SELECT * FROM categories WHERE name=@name", c);
selection.Parameters.AddWithValue("@name", name);
selection.Prepare();
MySqlDataReader results = selection.ExecuteReader();
if (!results.Read())
{
message = null;
return false;
}
message = new Category(results);
results.Close();
return true;
}
public static bool AddCategory(string name, ulong categoryID)
{
try
{
using MySqlConnection c = GetConnection();
c.Open();
using MySqlCommand cmd = new MySqlCommand(@"INSERT INTO categories (name,category_id) VALUES (@name, @category_id);", c);
cmd.Parameters.AddWithValue("@name", name);
cmd.Parameters.AddWithValue("@category_id", categoryID);
cmd.Prepare();
return cmd.ExecuteNonQuery() > 0;
}
catch (MySqlException e)
{
Logger.Error("Could not add category to database.", e);
return false;
}
}
public static bool RemoveCategory(ulong categoryID)
{
try
{
using MySqlConnection c = GetConnection();
c.Open();
using MySqlCommand cmd = new MySqlCommand(@"DELETE FROM categories WHERE category_id=@category_id", c);
cmd.Parameters.AddWithValue("@category_id", categoryID);
cmd.Prepare();
return cmd.ExecuteNonQuery() > 0;
}
catch (MySqlException e)
{
Logger.Error("Could not remove category from database.", e);
return false;
}
}
public static bool TryGetInterviewTemplateJSON(ulong categoryID, out string templateJSON)
{
using MySqlConnection c = GetConnection();
c.Open();
using MySqlCommand selection = new MySqlCommand("SELECT * FROM interview_templates WHERE category_id=@category_id", c);
selection.Parameters.AddWithValue("@category_id", categoryID);
selection.Prepare();
MySqlDataReader results = selection.ExecuteReader();
if (!results.Read())
{
templateJSON = null;
return false;
}
templateJSON = results.GetString("template");
results.Close();
return true;
}
public static bool TryGetInterviewFromTemplate(ulong categoryID, ulong channelID, out Interview interview)
{
using MySqlConnection c = GetConnection();
c.Open();
using MySqlCommand selection = new MySqlCommand("SELECT * FROM interview_templates WHERE category_id=@category_id", c);
selection.Parameters.AddWithValue("@category_id", categoryID);
selection.Prepare();
MySqlDataReader results = selection.ExecuteReader();
// Check if messages exist in the database
if (!results.Read())
{
interview = null;
return false;
}
string templateString = results.GetString("template");
results.Close();
try
{
Template template = JsonConvert.DeserializeObject<Template>(templateString, new JsonSerializerSettings
{
Error = delegate (object sender, ErrorEventArgs args)
{
Logger.Error("Error occured when trying to read interview template '" + categoryID + "' from database: " + args.ErrorContext.Error.Message);
Logger.Debug("Detailed exception:", args.ErrorContext.Error);
args.ErrorContext.Handled = false;
}
});
interview = new Interview(channelID, template.interview, template.definitions);
return true;
}
catch (Exception e)
{
Logger.Warn("Unable to create interview object from the current template for category '" + categoryID + "' in the database.", e);
interview = null;
return false;
}
}
public static bool SetInterviewTemplate(Template template)
{
try
{
string templateString = JsonConvert.SerializeObject(template, new JsonSerializerSettings
{
NullValueHandling = NullValueHandling.Ignore,
MissingMemberHandling = MissingMemberHandling.Error,
Formatting = Formatting.Indented,
ContractResolver = new InterviewStep.StripInternalPropertiesResolver()
});
string query;
if (TryGetInterviewTemplateJSON(template.categoryID, out _))
{
query = "UPDATE interview_templates SET template = @template WHERE category_id=@category_id";
}
else
{
query = "INSERT INTO interview_templates (category_id,template) VALUES (@category_id, @template)";
}
using MySqlConnection c = GetConnection();
c.Open();
using MySqlCommand cmd = new MySqlCommand(query, c);
cmd.Parameters.AddWithValue("@category_id", template.categoryID);
cmd.Parameters.AddWithValue("@template", templateString);
cmd.Prepare();
return cmd.ExecuteNonQuery() > 0;
}
catch (MySqlException e)
{
Logger.Error("Could not set interview template in database.", e);
return false;
}
}
public static bool TryDeleteInterviewTemplate(ulong categoryID)
{
try
{
using MySqlConnection c = GetConnection();
c.Open();
using MySqlCommand deletion = new MySqlCommand(@"DELETE FROM interview_templates WHERE category_id=@category_id", c);
deletion.Parameters.AddWithValue("@category_id", categoryID);
deletion.Prepare();
return deletion.ExecuteNonQuery() > 0;
}
catch (MySqlException)
{
return false;
}
}
public static bool SaveInterview(Interview interview)
{
try
{
string query;
if (TryGetInterview(interview.channelID, out _))
{
query = "UPDATE interviews SET interview = @interview, definitions = @definitions WHERE channel_id = @channel_id";
}
else
{
query = "INSERT INTO interviews (channel_id,interview, definitions) VALUES (@channel_id, @interview, @definitions)";
}
using MySqlConnection c = GetConnection();
c.Open();
using MySqlCommand cmd = new MySqlCommand(query, c);
cmd.Parameters.AddWithValue("@channel_id", interview.channelID);
cmd.Parameters.AddWithValue("@interview", JsonConvert.SerializeObject(interview.interviewRoot, Formatting.Indented));
cmd.Parameters.AddWithValue("@definitions", JsonConvert.SerializeObject(interview.definitions, Formatting.Indented));
cmd.Prepare();
return cmd.ExecuteNonQuery() > 0;
}
catch (MySqlException e)
{
Logger.Error("Could not save interview to database.", e);
return false;
}
}
public static bool TryGetInterview(ulong channelID, out Interview 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 = new Interview(channelID,
JsonConvert.DeserializeObject<InterviewStep>(results.GetString("interview")),
JsonConvert.DeserializeObject<Dictionary<string, InterviewStep>>(results.GetString("definitions")));
results.Close();
return true;
}
public static bool TryDeleteInterview(ulong channelID)
{
try
{
using MySqlConnection c = GetConnection();
c.Open();
using MySqlCommand deletion = new MySqlCommand(@"DELETE FROM interviews WHERE channel_id=@channel_id", c);
deletion.Parameters.AddWithValue("@channel_id", channelID);
deletion.Prepare();
return deletion.ExecuteNonQuery() > 0;
}
catch (MySqlException)
{
return false;
}
}
public class Ticket
{
public uint id;
public ulong creatorID;
public ulong assignedStaffID;
public string summary;
public ulong channelID;
public Ticket(MySqlDataReader reader)
{
id = reader.GetUInt32("id");
creatorID = reader.GetUInt64("creator_id");
assignedStaffID = reader.GetUInt64("assigned_staff_id");
summary = reader.GetString("summary");
channelID = reader.GetUInt64("channel_id");
}
public string DiscordRelativeTime()
{
return Formatter.Timestamp(channelID.GetSnowflakeTime(), Config.timestampFormat);
}
}
public class StaffMember
{
public ulong userID;
public string name;
public bool active;
public StaffMember(MySqlDataReader reader)
{
userID = reader.GetUInt64("user_id");
name = reader.GetString("name");
active = reader.GetBoolean("active");
}
}
public class Message
{
public string identifier;
public ulong userID;
public string message;
public Message(MySqlDataReader reader)
{
identifier = reader.GetString("identifier");
userID = reader.GetUInt64("user_id");
message = reader.GetString("message");
}
}
public class Category
{
public string name;
public ulong id;
public Category(MySqlDataReader reader)
{
name = reader.GetString("name");
id = reader.GetUInt64("category_id");
}
}
}

63
Database/Blacklist.cs Normal file
View file

@ -0,0 +1,63 @@
using MySqlConnector;
namespace SupportChild.Database;
public class Blacklist
{
public static bool IsBanned(ulong userID)
{
using MySqlConnection c = Connection.GetConnection();
c.Open();
using MySqlCommand selection = new MySqlCommand(@"SELECT * FROM blacklisted_users WHERE user_id=@user_id", c);
selection.Parameters.AddWithValue("@user_id", userID);
selection.Prepare();
MySqlDataReader results = selection.ExecuteReader();
// Check if user is blacklisted
if (results.Read())
{
results.Close();
return true;
}
results.Close();
return false;
}
public static bool Ban(ulong blacklistedID, ulong staffID)
{
try
{
using MySqlConnection c = Connection.GetConnection();
c.Open();
using MySqlCommand cmd = new MySqlCommand(@"INSERT INTO blacklisted_users (user_id,time,moderator_id) VALUES (@user_id, UTC_TIMESTAMP(), @moderator_id);", c);
cmd.Parameters.AddWithValue("@user_id", blacklistedID);
cmd.Parameters.AddWithValue("@moderator_id", staffID);
cmd.Prepare();
return cmd.ExecuteNonQuery() > 0;
}
catch (MySqlException e)
{
Logger.Warn("Could not blacklist user in database.", e);
return false;
}
}
public static bool Unban(ulong blacklistedID)
{
try
{
using MySqlConnection c = Connection.GetConnection();
c.Open();
using MySqlCommand cmd = new MySqlCommand(@"DELETE FROM blacklisted_users WHERE user_id=@user_id", c);
cmd.Parameters.AddWithValue("@user_id", blacklistedID);
cmd.Prepare();
return cmd.ExecuteNonQuery() > 0;
}
catch (MySqlException e)
{
Logger.Warn("Could not unblacklist user in database.", e);
return false;
}
}
}

108
Database/Category.cs Normal file
View file

@ -0,0 +1,108 @@
using System.Collections.Generic;
using MySqlConnector;
namespace SupportChild.Database;
public class Category(MySqlDataReader reader)
{
public readonly string name = reader.GetString("name");
public readonly ulong id = reader.GetUInt64("category_id");
public static List<Category> GetAllCategories()
{
using MySqlConnection c = Connection.GetConnection();
c.Open();
using MySqlCommand selection = new MySqlCommand(@"SELECT * FROM categories", c);
selection.Prepare();
MySqlDataReader results = selection.ExecuteReader();
if (!results.Read())
{
return [];
}
List<Category> categories = [new(results)];
while (results.Read())
{
categories.Add(new Category(results));
}
results.Close();
return categories;
}
public static bool TryGetCategory(ulong categoryID, out Category message)
{
using MySqlConnection c = Connection.GetConnection();
c.Open();
using MySqlCommand selection = new MySqlCommand(@"SELECT * FROM categories WHERE category_id=@category_id", c);
selection.Parameters.AddWithValue("@category_id", categoryID);
selection.Prepare();
MySqlDataReader results = selection.ExecuteReader();
if (!results.Read())
{
message = null;
return false;
}
message = new Category(results);
results.Close();
return true;
}
public static bool TryGetCategory(string name, out Category message)
{
using MySqlConnection c = Connection.GetConnection();
c.Open();
using MySqlCommand selection = new MySqlCommand(@"SELECT * FROM categories WHERE name=@name", c);
selection.Parameters.AddWithValue("@name", name);
selection.Prepare();
MySqlDataReader results = selection.ExecuteReader();
if (!results.Read())
{
message = null;
return false;
}
message = new Category(results);
results.Close();
return true;
}
public static bool AddCategory(string name, ulong categoryID)
{
try
{
using MySqlConnection c = Connection.GetConnection();
c.Open();
using MySqlCommand cmd = new MySqlCommand(@"INSERT INTO categories (name,category_id) VALUES (@name, @category_id);", c);
cmd.Parameters.AddWithValue("@name", name);
cmd.Parameters.AddWithValue("@category_id", categoryID);
cmd.Prepare();
return cmd.ExecuteNonQuery() > 0;
}
catch (MySqlException e)
{
Logger.Error("Could not add category to database.", e);
return false;
}
}
public static bool RemoveCategory(ulong categoryID)
{
try
{
using MySqlConnection c = Connection.GetConnection();
c.Open();
using MySqlCommand cmd = new MySqlCommand(@"DELETE FROM categories WHERE category_id=@category_id", c);
cmd.Parameters.AddWithValue("@category_id", categoryID);
cmd.Prepare();
return cmd.ExecuteNonQuery() > 0;
}
catch (MySqlException e)
{
Logger.Error("Could not remove category from database.", e);
return false;
}
}
}

92
Database/Database.cs Normal file
View file

@ -0,0 +1,92 @@
using MySqlConnector;
namespace SupportChild.Database;
public static class Connection
{
private static string connectionString = "";
public static void SetConnectionString(string host, int port, string database, string username, string password)
{
connectionString = "server=" + host +
";database=" + database +
";port=" + port +
";userid=" + username +
";password=" + password;
}
public static MySqlConnection GetConnection()
{
return new MySqlConnection(connectionString);
}
public static void SetupTables()
{
using MySqlConnection c = GetConnection();
using MySqlCommand createTickets = new MySqlCommand(
"CREATE TABLE IF NOT EXISTS tickets(" +
"id INT UNSIGNED NOT NULL PRIMARY KEY AUTO_INCREMENT," +
"created_time DATETIME NOT NULL," +
"creator_id BIGINT UNSIGNED NOT NULL," +
"assigned_staff_id BIGINT UNSIGNED NOT NULL DEFAULT 0," +
"summary VARCHAR(5000) NOT NULL," +
"channel_id BIGINT UNSIGNED NOT NULL UNIQUE," +
"INDEX(created_time, assigned_staff_id, channel_id))",
c);
using MySqlCommand createTicketHistory = new MySqlCommand(
"CREATE TABLE IF NOT EXISTS ticket_history(" +
"id INT UNSIGNED NOT NULL PRIMARY KEY," +
"created_time DATETIME NOT NULL," +
"closed_time DATETIME NOT NULL," +
"creator_id BIGINT UNSIGNED NOT NULL," +
"assigned_staff_id BIGINT UNSIGNED NOT NULL DEFAULT 0," +
"summary VARCHAR(5000) NOT NULL," +
"channel_id BIGINT UNSIGNED NOT NULL UNIQUE," +
"INDEX(created_time, closed_time, channel_id))",
c);
using MySqlCommand createBlacklisted = new MySqlCommand(
"CREATE TABLE IF NOT EXISTS blacklisted_users(" +
"user_id BIGINT UNSIGNED NOT NULL PRIMARY KEY," +
"time DATETIME NOT NULL," +
"moderator_id BIGINT UNSIGNED NOT NULL," +
"INDEX(user_id, time))",
c);
using MySqlCommand createStaffList = new MySqlCommand(
"CREATE TABLE IF NOT EXISTS staff(" +
"user_id BIGINT UNSIGNED NOT NULL PRIMARY KEY," +
"name VARCHAR(256) NOT NULL," +
"active BOOLEAN NOT NULL DEFAULT true)",
c);
using MySqlCommand createMessages = new MySqlCommand(
"CREATE TABLE IF NOT EXISTS messages(" +
"identifier VARCHAR(256) NOT NULL PRIMARY KEY," +
"user_id BIGINT UNSIGNED NOT NULL," +
"message VARCHAR(5000) NOT NULL)",
c);
using MySqlCommand createCategories = new MySqlCommand(
"CREATE TABLE IF NOT EXISTS categories(" +
"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," +
"definitions JSON NOT NULL)",
c);
using MySqlCommand createInterviewTemplates = new MySqlCommand(
"CREATE TABLE IF NOT EXISTS interview_templates(" +
"category_id BIGINT UNSIGNED NOT NULL PRIMARY KEY," +
"template JSON NOT NULL)",
c);
c.Open();
createTickets.ExecuteNonQuery();
createBlacklisted.ExecuteNonQuery();
createTicketHistory.ExecuteNonQuery();
createStaffList.ExecuteNonQuery();
createMessages.ExecuteNonQuery();
createCategories.ExecuteNonQuery();
createInterviews.ExecuteNonQuery();
createInterviewTemplates.ExecuteNonQuery();
}
}

195
Database/Interviews.cs Normal file
View file

@ -0,0 +1,195 @@
using System;
using System.Collections.Generic;
using MySqlConnector;
using Newtonsoft.Json;
using Newtonsoft.Json.Serialization;
using SupportChild.Interviews;
namespace SupportChild.Database;
public class Interviews
{
public static bool TryGetInterviewTemplateJSON(ulong categoryID, out string templateJSON)
{
using MySqlConnection c = Connection.GetConnection();
c.Open();
using MySqlCommand selection = new("SELECT * FROM interview_templates WHERE category_id=@category_id", c);
selection.Parameters.AddWithValue("@category_id", categoryID);
selection.Prepare();
MySqlDataReader results = selection.ExecuteReader();
if (!results.Read())
{
templateJSON = null;
return false;
}
templateJSON = results.GetString("template");
results.Close();
return true;
}
public static bool TryGetInterviewFromTemplate(ulong categoryID, ulong channelID, out Interview interview)
{
using MySqlConnection c = Connection.GetConnection();
c.Open();
using MySqlCommand selection = new("SELECT * FROM interview_templates WHERE category_id=@category_id", c);
selection.Parameters.AddWithValue("@category_id", categoryID);
selection.Prepare();
MySqlDataReader results = selection.ExecuteReader();
// Check if messages exist in the database
if (!results.Read())
{
interview = null;
return false;
}
string templateString = results.GetString("template");
results.Close();
try
{
Template template = JsonConvert.DeserializeObject<Template>(templateString, new JsonSerializerSettings
{
Error = delegate (object _, ErrorEventArgs args)
{
Logger.Error("Error occured when trying to read interview template '" + categoryID + "' from database: " + args.ErrorContext.Error.Message);
Logger.Debug("Detailed exception:", args.ErrorContext.Error);
args.ErrorContext.Handled = false;
}
});
interview = new Interview(channelID, template.interview, template.definitions);
return true;
}
catch (Exception e)
{
Logger.Warn("Unable to create interview object from the current template for category '" + categoryID + "' in the database.", e);
interview = null;
return false;
}
}
public static bool SetInterviewTemplate(Template template)
{
try
{
string templateString = JsonConvert.SerializeObject(template, new JsonSerializerSettings
{
NullValueHandling = NullValueHandling.Ignore,
MissingMemberHandling = MissingMemberHandling.Error,
Formatting = Formatting.Indented,
ContractResolver = new InterviewStep.StripInternalPropertiesResolver()
});
string query;
if (TryGetInterviewTemplateJSON(template.categoryID, out _))
{
query = "UPDATE interview_templates SET template = @template WHERE category_id=@category_id";
}
else
{
query = "INSERT INTO interview_templates (category_id,template) VALUES (@category_id, @template)";
}
using MySqlConnection c = Connection.GetConnection();
c.Open();
using MySqlCommand cmd = new(query, c);
cmd.Parameters.AddWithValue("@category_id", template.categoryID);
cmd.Parameters.AddWithValue("@template", templateString);
cmd.Prepare();
return cmd.ExecuteNonQuery() > 0;
}
catch (MySqlException e)
{
Logger.Error("Could not set interview template in database.", e);
return false;
}
}
public static bool TryDeleteInterviewTemplate(ulong categoryID)
{
try
{
using MySqlConnection c = Connection.GetConnection();
c.Open();
using MySqlCommand deletion = new("DELETE FROM interview_templates WHERE category_id=@category_id", c);
deletion.Parameters.AddWithValue("@category_id", categoryID);
deletion.Prepare();
return deletion.ExecuteNonQuery() > 0;
}
catch (MySqlException)
{
return false;
}
}
public static bool SaveInterview(Interview interview)
{
try
{
string query;
if (TryGetInterview(interview.channelID, out _))
{
query = "UPDATE interviews SET interview = @interview, definitions = @definitions WHERE channel_id = @channel_id";
}
else
{
query = "INSERT INTO interviews (channel_id,interview, definitions) VALUES (@channel_id, @interview, @definitions)";
}
using MySqlConnection c = Connection.GetConnection();
c.Open();
using MySqlCommand cmd = new(query, c);
cmd.Parameters.AddWithValue("@channel_id", interview.channelID);
cmd.Parameters.AddWithValue("@interview", JsonConvert.SerializeObject(interview.interviewRoot, Formatting.Indented));
cmd.Parameters.AddWithValue("@definitions", JsonConvert.SerializeObject(interview.definitions, Formatting.Indented));
cmd.Prepare();
return cmd.ExecuteNonQuery() > 0;
}
catch (MySqlException e)
{
Logger.Error("Could not save interview to database.", e);
return false;
}
}
public static bool TryGetInterview(ulong channelID, out Interview interview)
{
using MySqlConnection c = Connection.GetConnection();
c.Open();
using MySqlCommand selection = new("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 = new Interview(channelID,
JsonConvert.DeserializeObject<InterviewStep>(results.GetString("interview")),
JsonConvert.DeserializeObject<Dictionary<string, InterviewStep>>(results.GetString("definitions")));
results.Close();
return true;
}
public static bool TryDeleteInterview(ulong channelID)
{
try
{
using MySqlConnection c = Connection.GetConnection();
c.Open();
using MySqlCommand deletion = new("DELETE FROM interviews WHERE channel_id=@channel_id", c);
deletion.Parameters.AddWithValue("@channel_id", channelID);
deletion.Prepare();
return deletion.ExecuteNonQuery() > 0;
}
catch (MySqlException)
{
return false;
}
}
}

112
Database/Message.cs Normal file
View file

@ -0,0 +1,112 @@
using System.Collections.Generic;
using MySqlConnector;
namespace SupportChild.Database;
public class Message(MySqlDataReader reader)
{
public readonly string identifier = reader.GetString("identifier");
public readonly ulong userID = reader.GetUInt64("user_id");
public readonly string message = reader.GetString("message");
public static List<Message> GetAllMessages()
{
using MySqlConnection c = Connection.GetConnection();
c.Open();
using MySqlCommand selection = new("SELECT * FROM messages", c);
selection.Prepare();
MySqlDataReader results = selection.ExecuteReader();
if (!results.Read())
{
return [];
}
List<Message> messages = [new(results)];
while (results.Read())
{
messages.Add(new Message(results));
}
results.Close();
return messages;
}
public static bool TryGetMessage(string identifier, out Message message)
{
using MySqlConnection c = Connection.GetConnection();
c.Open();
using MySqlCommand selection = new("SELECT * FROM messages WHERE identifier=@identifier", c);
selection.Parameters.AddWithValue("@identifier", identifier);
selection.Prepare();
MySqlDataReader results = selection.ExecuteReader();
// Check if ticket exists in the database
if (!results.Read())
{
message = null;
return false;
}
message = new Message(results);
results.Close();
return true;
}
public static bool AddMessage(string identifier, ulong userID, string message)
{
try
{
using MySqlConnection c = Connection.GetConnection();
c.Open();
using MySqlCommand cmd = new("INSERT INTO messages (identifier,user_id,message) VALUES (@identifier, @user_id, @message);", c);
cmd.Parameters.AddWithValue("@identifier", identifier);
cmd.Parameters.AddWithValue("@user_id", userID);
cmd.Parameters.AddWithValue("@message", message);
cmd.Prepare();
return cmd.ExecuteNonQuery() > 0;
}
catch (MySqlException e)
{
Logger.Error("Could not add message to database.", e);
return false;
}
}
public static bool UpdateMessage(string identifier, ulong userID, string message)
{
try
{
using MySqlConnection c = Connection.GetConnection();
c.Open();
using MySqlCommand cmd = new("UPDATE messages SET message = @message, user_id = @user_id WHERE identifier=@identifier", c);
cmd.Parameters.AddWithValue("@identifier", identifier);
cmd.Parameters.AddWithValue("@user_id", userID);
cmd.Parameters.AddWithValue("@message", message);
cmd.Prepare();
return cmd.ExecuteNonQuery() > 0;
}
catch (MySqlException e)
{
Logger.Error("Could not add message to database.", e);
return false;
}
}
public static bool RemoveMessage(string identifier)
{
try
{
using MySqlConnection c = Connection.GetConnection();
c.Open();
using MySqlCommand cmd = new("DELETE FROM messages WHERE identifier=@identifier", c);
cmd.Parameters.AddWithValue("@identifier", identifier);
cmd.Prepare();
return cmd.ExecuteNonQuery() > 0;
}
catch (MySqlException e)
{
Logger.Error("Could not remove message from database.", e);
return false;
}
}
}

176
Database/StaffMember.cs Normal file
View file

@ -0,0 +1,176 @@
using System.Collections.Generic;
using MySqlConnector;
namespace SupportChild.Database;
public class StaffMember(MySqlDataReader reader)
{
public readonly ulong userID = reader.GetUInt64("user_id");
public string name = reader.GetString("name");
public readonly bool active = reader.GetBoolean("active");
public static bool AddStaff(string name, ulong userID)
{
try
{
using MySqlConnection c = Connection.GetConnection();
c.Open();
using MySqlCommand update = IsStaff(userID) ? new MySqlCommand("UPDATE staff SET name = @name WHERE user_id = @user_id", c)
: new MySqlCommand("INSERT INTO staff (user_id, name) VALUES (@user_id, @name);", c);
update.Parameters.AddWithValue("@name", name);
update.Parameters.AddWithValue("@user_id", userID);
update.Prepare();
return update.ExecuteNonQuery() > 0;
}
catch (MySqlException e)
{
Logger.Warn("Could not add staff to database.", e);
return false;
}
}
public static bool RemoveStaff(ulong userID)
{
try
{
using MySqlConnection c = Connection.GetConnection();
c.Open();
using MySqlCommand update = new("DELETE FROM staff WHERE user_id = @user_id", c);
update.Parameters.AddWithValue("@user_id", userID);
update.Prepare();
return update.ExecuteNonQuery() > 0;
}
catch (MySqlException e)
{
Logger.Warn("Could not remove staff from database.", e);
return false;
}
}
public static bool AssignStaff(Ticket ticket, ulong staffID)
{
try
{
using MySqlConnection c = Connection.GetConnection();
c.Open();
using MySqlCommand update = new("UPDATE tickets SET assigned_staff_id = @assigned_staff_id WHERE id = @id", c);
update.Parameters.AddWithValue("@assigned_staff_id", staffID);
update.Parameters.AddWithValue("@id", ticket.id);
update.Prepare();
return update.ExecuteNonQuery() > 0;
}
catch (MySqlException e)
{
Logger.Warn("Could not add staff to ticket in database.", e);
return false;
}
}
public static bool UnassignStaff(Ticket ticket)
{
return AssignStaff(ticket, 0);
}
public static bool SetStaffActive(ulong staffID, bool active)
{
try
{
using MySqlConnection c = Connection.GetConnection();
c.Open();
MySqlCommand update = new("UPDATE staff SET active = @active WHERE user_id = @user_id", c);
update.Parameters.AddWithValue("@user_id", staffID);
update.Parameters.AddWithValue("@active", active);
update.Prepare();
return update.ExecuteNonQuery() > 0;
}
catch (MySqlException e)
{
Logger.Warn("Could not set staff member as active in database.", e);
return false;
}
}
public static List<StaffMember> GetActiveStaff(params ulong[] ignoredUserIDs)
{
using MySqlConnection c = Connection.GetConnection();
c.Open();
using MySqlCommand selection = new("SELECT * FROM staff WHERE active = true AND user_id NOT IN (@user_ids)", c);
selection.Parameters.AddWithValue("@user_ids", string.Join(",", ignoredUserIDs));
selection.Prepare();
MySqlDataReader results = selection.ExecuteReader();
if (!results.Read())
{
return [];
}
List<StaffMember> staffMembers = [new(results)];
while (results.Read())
{
staffMembers.Add(new StaffMember(results));
}
results.Close();
return staffMembers;
}
public static List<StaffMember> GetAllStaff(params ulong[] ignoredUserIDs)
{
using MySqlConnection c = Connection.GetConnection();
c.Open();
using MySqlCommand selection = new("SELECT * FROM staff WHERE user_id NOT IN (@user_ids)", c);
selection.Parameters.AddWithValue("@user_ids", string.Join(",", ignoredUserIDs));
selection.Prepare();
MySqlDataReader results = selection.ExecuteReader();
if (!results.Read())
{
return [];
}
List<StaffMember> staffMembers = [new(results)];
while (results.Read())
{
staffMembers.Add(new StaffMember(results));
}
results.Close();
return staffMembers;
}
public static bool IsStaff(ulong staffID)
{
using MySqlConnection c = Connection.GetConnection();
c.Open();
using MySqlCommand selection = new("SELECT * FROM staff WHERE user_id=@user_id", c);
selection.Parameters.AddWithValue("@user_id", staffID);
selection.Prepare();
MySqlDataReader results = selection.ExecuteReader();
if (!results.Read())
{
return false;
}
results.Close();
return true;
}
public static bool TryGetStaff(ulong staffID, out StaffMember staffMember)
{
using MySqlConnection c = Connection.GetConnection();
c.Open();
using MySqlCommand selection = new("SELECT * FROM staff WHERE user_id=@user_id", c);
selection.Parameters.AddWithValue("@user_id", staffID);
selection.Prepare();
MySqlDataReader results = selection.ExecuteReader();
if (!results.Read())
{
staffMember = null;
return false;
}
staffMember = new StaffMember(results);
results.Close();
return true;
}
}

312
Database/Ticket.cs Normal file
View file

@ -0,0 +1,312 @@
using System.Collections.Generic;
using DSharpPlus;
using MySqlConnector;
namespace SupportChild.Database;
public class Ticket(MySqlDataReader reader)
{
public readonly uint id = reader.GetUInt32("id");
public readonly ulong creatorID = reader.GetUInt64("creator_id");
public readonly ulong assignedStaffID = reader.GetUInt64("assigned_staff_id");
public readonly string summary = reader.GetString("summary");
public readonly ulong channelID = reader.GetUInt64("channel_id");
public string DiscordRelativeTime()
{
return Formatter.Timestamp(channelID.GetSnowflakeTime(), Config.timestampFormat);
}
public static long GetNumberOfTickets()
{
try
{
using MySqlConnection c = Connection.GetConnection();
using MySqlCommand countTickets = new MySqlCommand("SELECT COUNT(*) FROM tickets", c);
c.Open();
return (long)(countTickets?.ExecuteScalar() ?? 0);
}
catch (MySqlException e)
{
Logger.Error("Error occured when attempting to count number of open tickets.", e);
}
return -1;
}
public static long GetNumberOfClosedTickets()
{
try
{
using MySqlConnection c = Connection.GetConnection();
using MySqlCommand countTickets = new MySqlCommand("SELECT COUNT(*) FROM ticket_history", c);
c.Open();
return (long)(countTickets?.ExecuteScalar() ?? 0);
}
catch (MySqlException e)
{
Logger.Error("Error occured when attempting to count number of open tickets.", e);
}
return -1;
}
public static bool IsOpenTicket(ulong channelID)
{
using MySqlConnection c = Connection.GetConnection();
c.Open();
using MySqlCommand selection = new("SELECT * FROM tickets 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())
{
return false;
}
results.Close();
return true;
}
public static bool TryGetOpenTicket(ulong channelID, out Ticket ticket)
{
using MySqlConnection c = Connection.GetConnection();
c.Open();
using MySqlCommand selection = new("SELECT * FROM tickets 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())
{
ticket = null;
return false;
}
ticket = new Ticket(results);
results.Close();
return true;
}
public static bool TryGetOpenTicketByID(uint id, out Ticket ticket)
{
using MySqlConnection c = Connection.GetConnection();
c.Open();
using MySqlCommand selection = new("SELECT * FROM tickets WHERE id=@id", c);
selection.Parameters.AddWithValue("@id", id);
selection.Prepare();
MySqlDataReader results = selection.ExecuteReader();
// Check if open ticket exists in the database
if (results.Read())
{
ticket = new Ticket(results);
results.Close();
return true;
}
results.Close();
ticket = null;
return false;
}
public static bool TryGetClosedTicket(uint id, out Ticket ticket)
{
using MySqlConnection c = Connection.GetConnection();
c.Open();
using MySqlCommand selection = new("SELECT * FROM ticket_history WHERE id=@id", c);
selection.Parameters.AddWithValue("@id", id);
selection.Prepare();
MySqlDataReader results = selection.ExecuteReader();
// Check if closed ticket exists in the database
if (results.Read())
{
ticket = new Ticket(results);
results.Close();
return true;
}
ticket = null;
results.Close();
return false;
}
public static bool TryGetOpenTickets(ulong userID, out List<Ticket> tickets)
{
tickets = null;
using MySqlConnection c = Connection.GetConnection();
c.Open();
using MySqlCommand selection = new("SELECT * FROM tickets WHERE creator_id=@creator_id", c);
selection.Parameters.AddWithValue("@creator_id", userID);
selection.Prepare();
MySqlDataReader results = selection.ExecuteReader();
if (!results.Read())
{
return false;
}
tickets = [new Ticket(results)];
while (results.Read())
{
tickets.Add(new Ticket(results));
}
results.Close();
return true;
}
public static bool TryGetOpenTickets(out List<Ticket> tickets)
{
tickets = null;
using MySqlConnection c = Connection.GetConnection();
c.Open();
using MySqlCommand selection = new("SELECT * FROM tickets ORDER BY channel_id", c);
selection.Prepare();
MySqlDataReader results = selection.ExecuteReader();
if (!results.Read())
{
return false;
}
tickets = [new Ticket(results)];
while (results.Read())
{
tickets.Add(new Ticket(results));
}
results.Close();
return true;
}
public static bool TryGetClosedTickets(ulong userID, out List<Ticket> tickets)
{
tickets = null;
using MySqlConnection c = Connection.GetConnection();
c.Open();
using MySqlCommand selection = new("SELECT * FROM ticket_history WHERE creator_id=@creator_id", c);
selection.Parameters.AddWithValue("@creator_id", userID);
selection.Prepare();
MySqlDataReader results = selection.ExecuteReader();
if (!results.Read())
{
return false;
}
tickets = [new Ticket(results)];
while (results.Read())
{
tickets.Add(new Ticket(results));
}
results.Close();
return true;
}
public static bool TryGetAssignedTickets(ulong staffID, out List<Ticket> tickets)
{
tickets = null;
using MySqlConnection c = Connection.GetConnection();
c.Open();
using MySqlCommand selection = new("SELECT * FROM tickets WHERE assigned_staff_id=@assigned_staff_id", c);
selection.Parameters.AddWithValue("@assigned_staff_id", staffID);
selection.Prepare();
MySqlDataReader results = selection.ExecuteReader();
if (!results.Read())
{
return false;
}
tickets = [new Ticket(results)];
while (results.Read())
{
tickets.Add(new Ticket(results));
}
results.Close();
return true;
}
public static long NewTicket(ulong memberID, ulong staffID, ulong ticketID)
{
using MySqlConnection c = Connection.GetConnection();
c.Open();
using MySqlCommand cmd = new("INSERT INTO tickets (created_time, creator_id, assigned_staff_id, summary, channel_id) VALUES (UTC_TIMESTAMP(), @creator_id, @assigned_staff_id, @summary, @channel_id);", c);
cmd.Parameters.AddWithValue("@creator_id", memberID);
cmd.Parameters.AddWithValue("@assigned_staff_id", staffID);
cmd.Parameters.AddWithValue("@summary", "");
cmd.Parameters.AddWithValue("@channel_id", ticketID);
cmd.ExecuteNonQuery();
return cmd.LastInsertedId;
}
public static void ArchiveTicket(Ticket ticket)
{
// Check if ticket already exists in the archive
if (TryGetClosedTicket(ticket.id, out Ticket _))
{
using MySqlConnection c = Connection.GetConnection();
using MySqlCommand deleteTicket = new("DELETE FROM ticket_history WHERE id=@id OR channel_id=@channel_id", c);
deleteTicket.Parameters.AddWithValue("@id", ticket.id);
deleteTicket.Parameters.AddWithValue("@channel_id", ticket.channelID);
c.Open();
deleteTicket.Prepare();
deleteTicket.ExecuteNonQuery();
}
// Create an entry in the ticket history database
using MySqlConnection conn = Connection.GetConnection();
using MySqlCommand archiveTicket = new("INSERT INTO ticket_history (id, created_time, closed_time, creator_id, assigned_staff_id, summary, channel_id) VALUES (@id, @created_time, UTC_TIMESTAMP(), @creator_id, @assigned_staff_id, @summary, @channel_id);", conn);
archiveTicket.Parameters.AddWithValue("@id", ticket.id);
archiveTicket.Parameters.AddWithValue("@created_time", ticket.channelID.GetSnowflakeTime());
archiveTicket.Parameters.AddWithValue("@creator_id", ticket.creatorID);
archiveTicket.Parameters.AddWithValue("@assigned_staff_id", ticket.assignedStaffID);
archiveTicket.Parameters.AddWithValue("@summary", ticket.summary);
archiveTicket.Parameters.AddWithValue("@channel_id", ticket.channelID);
conn.Open();
archiveTicket.Prepare();
archiveTicket.ExecuteNonQuery();
}
public static bool DeleteOpenTicket(uint ticketID)
{
try
{
using MySqlConnection c = Connection.GetConnection();
using MySqlCommand deletion = new("DELETE FROM tickets WHERE id=@id", c);
deletion.Parameters.AddWithValue("@id", ticketID);
c.Open();
deletion.Prepare();
return deletion.ExecuteNonQuery() > 0;
}
catch (MySqlException e)
{
Logger.Warn("Could not delete open ticket in database.", e);
return false;
}
}
public static bool SetSummary(ulong channelID, string summary)
{
try
{
using MySqlConnection c = Connection.GetConnection();
c.Open();
using MySqlCommand update = new("UPDATE tickets SET summary = @summary WHERE channel_id = @channel_id", c);
update.Parameters.AddWithValue("@summary", summary);
update.Parameters.AddWithValue("@channel_id", channelID);
update.Prepare();
return update.ExecuteNonQuery() > 0;
}
catch (MySqlException e)
{
Logger.Warn("Could not set summary in database.", e);
return false;
}
}
}

View file

@ -66,7 +66,7 @@ public static class EventHandler
}
// Ignore messages outside of tickets.
if (!Database.TryGetOpenTicket(e.Channel.Id, out Database.Ticket ticket))
if (!Database.Ticket.TryGetOpenTicket(e.Channel.Id, out Database.Ticket ticket))
{
return;
}
@ -87,7 +87,7 @@ public static class EventHandler
private static async Task SendTicketUpdatedMessage(MessageCreatedEventArgs e, Database.Ticket ticket)
{
// Ignore staff messages
if (Database.IsStaff(e.Author.Id))
if (Database.StaffMember.IsStaff(e.Author.Id))
{
return;
}
@ -112,7 +112,7 @@ public static class EventHandler
public static async Task OnMemberAdded(DiscordClient client, GuildMemberAddedEventArgs e)
{
if (!Database.TryGetOpenTickets(e.Member.Id, out List<Database.Ticket> ownTickets))
if (!Database.Ticket.TryGetOpenTickets(e.Member.Id, out List<Database.Ticket> ownTickets))
{
return;
}
@ -147,7 +147,7 @@ public static class EventHandler
public static async Task OnMemberRemoved(DiscordClient client, GuildMemberRemovedEventArgs e)
{
if (Database.TryGetOpenTickets(e.Member.Id, out List<Database.Ticket> ownTickets))
if (Database.Ticket.TryGetOpenTickets(e.Member.Id, out List<Database.Ticket> ownTickets))
{
foreach (Database.Ticket ticket in ownTickets)
{
@ -167,7 +167,7 @@ public static class EventHandler
}
}
if (LogChannel.IsEnabled && Database.TryGetAssignedTickets(e.Member.Id, out List<Database.Ticket> assignedTickets))
if (LogChannel.IsEnabled && Database.Ticket.TryGetAssignedTickets(e.Member.Id, out List<Database.Ticket> assignedTickets))
{
foreach (Database.Ticket ticket in assignedTickets)
{
@ -205,6 +205,18 @@ public static class EventHandler
case not null when e.Id.StartsWith("supportchild_interviewbutton"):
await Interviewer.ProcessButtonOrSelectorResponse(e.Interaction);
return;
case not null when e.Id.StartsWith("supportchild_confirmmessagedelete"):
await SetMessageCommand.ConfirmMessageDeletion(e.Interaction, GetIDFromCustomID(e.Id));
return;
case not null when e.Id.StartsWith("supportchild_cancelmessagedelete"):
await SetMessageCommand.CancelMessageDeletion(e.Interaction, GetIDFromCustomID(e.Id));
return;
case not null when e.Id.StartsWith("supportchild_confirmmessageupdate"):
await SetMessageCommand.ConfirmMessageUpdate(e.Interaction, GetIDFromCustomID(e.Id));
return;
case not null when e.Id.StartsWith("supportchild_cancelmessageupdate"):
await SetMessageCommand.CancelMessageUpdate(e.Interaction, GetIDFromCustomID(e.Id));
return;
case "right":
case "left":
case "rightskip":
@ -289,6 +301,17 @@ public static class EventHandler
}
}
private static ulong GetIDFromCustomID(string customID)
{
List<string> values = customID.Split(' ').ToList();
if (values.Count < 2 || !ulong.TryParse(values[1], out ulong id))
{
Logger.Warn("Got an invalid button/selector ID: " + customID);
return 0;
}
return id;
}
private static async Task OnNewTicketButtonUsed(DiscordInteraction interaction)
{
await interaction.CreateResponseAsync(DiscordInteractionResponseType.DeferredChannelMessageWithSource, new DiscordInteractionResponseBuilder().AsEphemeral());

View file

@ -13,7 +13,7 @@ public static class Interviewer
{
public static async Task<bool> StartInterview(DiscordChannel channel)
{
if (!Database.TryGetInterviewFromTemplate(channel.Parent.Id, channel.Id, out Interview interview))
if (!Database.Interviews.TryGetInterviewFromTemplate(channel.Parent.Id, channel.Id, out Interview interview))
{
return false;
}
@ -26,12 +26,12 @@ public static class Interviewer
Color = DiscordColor.Red
});
interview.interviewRoot.AddRelatedMessageIDs(errorMessage.Id);
Database.SaveInterview(interview);
Database.Interviews.SaveInterview(interview);
return false;
}
await SendNextMessage(interview, channel, interview.interviewRoot);
return Database.SaveInterview(interview);
return Database.Interviews.SaveInterview(interview);
}
public static async Task<bool> RestartInterview(DiscordChannel channel)
@ -47,14 +47,14 @@ public static class Interviewer
public static async Task<bool> StopInterview(DiscordChannel channel)
{
if (Database.TryGetInterview(channel.Id, out Interview interview))
if (Database.Interviews.TryGetInterview(channel.Id, out Interview interview))
{
if (Config.deleteMessagesAfterInterviewEnd)
{
await DeletePreviousMessages(interview, channel);
}
if (!Database.TryDeleteInterview(channel.Id))
if (!Database.Interviews.TryDeleteInterview(channel.Id))
{
Logger.Warn("Could not delete interview from database. Channel ID: " + channel.Id);
}
@ -70,7 +70,7 @@ public static class Interviewer
return;
}
if (!Database.TryGetOpenTicket(interaction.Channel.Id, out Database.Ticket ticket))
if (!Database.Ticket.TryGetOpenTicket(interaction.Channel.Id, out Database.Ticket ticket))
{
await interaction.CreateResponseAsync(DiscordInteractionResponseType.ChannelMessageWithSource, new DiscordInteractionResponseBuilder()
.AddEmbed(new DiscordEmbedBuilder()
@ -98,7 +98,7 @@ public static class Interviewer
}
// Return if there is no active interview in this channel
if (!Database.TryGetInterview(interaction.Channel.Id, out Interview interview))
if (!Database.Interviews.TryGetInterview(interaction.Channel.Id, out Interview interview))
{
await interaction.CreateResponseAsync(DiscordInteractionResponseType.ChannelMessageWithSource, new DiscordInteractionResponseBuilder()
.AddEmbed(new DiscordEmbedBuilder()
@ -171,7 +171,7 @@ public static class Interviewer
componentID = interaction.Data.Values[0];
break;
case DiscordComponentType.Button:
componentID = interaction.Data.CustomId.Replace("supportchild_interviewbutton ", "");
componentID = interaction.Data.CustomId.Replace("supportboi_interviewbutton ", "");
break;
case DiscordComponentType.ActionRow:
case DiscordComponentType.FormInput:
@ -214,7 +214,7 @@ public static class Interviewer
Description = "Error: Could not determine the next question based on your answer. Check your response and ask an admin to check the bot logs if this seems incorrect."
}).AsEphemeral());
currentStep.AddRelatedMessageIDs(followupMessage.Id);
Database.SaveInterview(interview);
Database.Interviews.SaveInterview(interview);
}
else
{
@ -265,7 +265,7 @@ public static class Interviewer
}
// The channel does not have an active interview.
if (!Database.TryGetInterview(answerMessage.ReferencedMessage.Channel.Id,
if (!Database.Interviews.TryGetInterview(answerMessage.ReferencedMessage.Channel.Id,
out Interview interview))
{
return;
@ -299,7 +299,7 @@ public static class Interviewer
Color = DiscordColor.Red
});
currentStep.AddRelatedMessageIDs(answerMessage.Id, lengthMessage.Id);
Database.SaveInterview(interview);
Database.Interviews.SaveInterview(interview);
return;
}
@ -311,7 +311,7 @@ public static class Interviewer
Color = DiscordColor.Red
});
currentStep.AddRelatedMessageIDs(answerMessage.Id, lengthMessage.Id);
Database.SaveInterview(interview);
Database.Interviews.SaveInterview(interview);
return;
}
@ -334,7 +334,7 @@ public static class Interviewer
Color = DiscordColor.Red
});
currentStep.AddRelatedMessageIDs(answerMessage.Id, errorMessage.Id);
Database.SaveInterview(interview);
Database.Interviews.SaveInterview(interview);
}
private static async Task HandleAnswer(string answer,
@ -375,13 +375,13 @@ public static class Interviewer
nextStep.AddRelatedMessageIDs(answerMessage.Id, errorMessage.Id);
previousStep.answer = null;
previousStep.answerID = 0;
Database.SaveInterview(interview);
Database.Interviews.SaveInterview(interview);
}
return;
}
await SendNextMessage(interview, channel, nextStep);
Database.SaveInterview(interview);
Database.Interviews.SaveInterview(interview);
break;
case StepType.INTERVIEW_END:
DiscordEmbedBuilder endEmbed = new()
@ -403,7 +403,7 @@ public static class Interviewer
await DeletePreviousMessages(interview, channel);
}
if (!Database.TryDeleteInterview(channel.Id))
if (!Database.Interviews.TryDeleteInterview(channel.Id))
{
Logger.Error("Could not delete interview from database. Channel ID: " + channel.Id);
}
@ -451,7 +451,7 @@ public static class Interviewer
previousStep.AddRelatedMessageIDs(errorMessage.Id, answerMessage.Id);
}
Database.SaveInterview(interview);
Database.Interviews.SaveInterview(interview);
Logger.Error("Could not find a step to return to after a reference step in channel " + channel.Id);
return;
@ -480,7 +480,7 @@ public static class Interviewer
previousStep.AddRelatedMessageIDs(errorMessage.Id, answerMessage.Id);
}
Database.SaveInterview(interview);
Database.Interviews.SaveInterview(interview);
return;
}
}
@ -556,7 +556,7 @@ public static class Interviewer
for (; nrOfButtons < 5 * (nrOfButtonRows + 1) && nrOfButtons < step.steps.Count; nrOfButtons++)
{
(string stepPattern, InterviewStep nextStep) = step.steps.ToArray()[nrOfButtons];
buttonRow.Add(new DiscordButtonComponent(nextStep.GetButtonStyle(), "supportchild_interviewbutton " + nrOfButtons, stepPattern));
buttonRow.Add(new DiscordButtonComponent(nextStep.GetButtonStyle(), "supportboi_interviewbutton " + nrOfButtons, stepPattern));
}
msgBuilder.AddComponents(buttonRow);
}
@ -574,26 +574,26 @@ public static class Interviewer
categoryOptions.Add(new DiscordSelectComponentOption(stepPattern, selectionOptions.ToString(), nextStep.selectorDescription));
}
selectionComponents.Add(new DiscordSelectComponent("supportchild_interviewselector " + selectionBoxes,
selectionComponents.Add(new DiscordSelectComponent("supportboi_interviewselector " + selectionBoxes,
string.IsNullOrWhiteSpace(step.selectorPlaceholder) ? "Select an option..." : step.selectorPlaceholder, categoryOptions));
}
msgBuilder.AddComponents(selectionComponents);
break;
case StepType.ROLE_SELECTOR:
msgBuilder.AddComponents(new DiscordRoleSelectComponent("supportchild_interviewroleselector",
msgBuilder.AddComponents(new DiscordRoleSelectComponent("supportboi_interviewroleselector",
string.IsNullOrWhiteSpace(step.selectorPlaceholder) ? "Select a role..." : step.selectorPlaceholder));
break;
case StepType.USER_SELECTOR:
msgBuilder.AddComponents(new DiscordUserSelectComponent("supportchild_interviewuserselector",
msgBuilder.AddComponents(new DiscordUserSelectComponent("supportboi_interviewuserselector",
string.IsNullOrWhiteSpace(step.selectorPlaceholder) ? "Select a user..." : step.selectorPlaceholder));
break;
case StepType.CHANNEL_SELECTOR:
msgBuilder.AddComponents(new DiscordChannelSelectComponent("supportchild_interviewchannelselector",
msgBuilder.AddComponents(new DiscordChannelSelectComponent("supportboi_interviewchannelselector",
string.IsNullOrWhiteSpace(step.selectorPlaceholder) ? "Select a channel..." : step.selectorPlaceholder));
break;
case StepType.MENTIONABLE_SELECTOR:
msgBuilder.AddComponents(new DiscordMentionableSelectComponent("supportchild_interviewmentionableselector",
msgBuilder.AddComponents(new DiscordMentionableSelectComponent("supportboi_interviewmentionableselector",
string.IsNullOrWhiteSpace(step.selectorPlaceholder) ? "Select a user or role..." : step.selectorPlaceholder));
break;
case StepType.TEXT_INPUT:
@ -604,7 +604,7 @@ public static class Interviewer
}
else
{
lengthInfo = " (Maximum " + (step?.maxLength ?? InterviewStep.DEFAULT_MAX_FIELD_LENGTH) + " characters)";
lengthInfo = " (Maximum " + (step.maxLength ?? InterviewStep.DEFAULT_MAX_FIELD_LENGTH) + " characters)";
}
embed.WithFooter("Reply to this message with your answer" + lengthInfo + ". You cannot include images or files.");
break;

View file

@ -1,3 +1,4 @@
[![Build Status](https://ci.toastielab.dev/job/toastie-stuff/job/SupportChild/job/main/badge/icon)](https://ci.toastielab.dev/job/toastie-stuff/job/SupportChild/job/main/)
# SupportChild
A support ticket Discord bot. Uses a MySQL database for storage of ticket information. Creates formatted HTML ticket transcripts when tickets are closed.
@ -8,11 +9,11 @@ A support ticket Discord bot. Uses a MySQL database for storage of ticket inform
1. Set up a mysql-compatible server, create a user and empty database for the bot to use.
2. (Optional) Install .NET 8 if it doesn't already exist on your system.
2. (Optional) Install .NET 9 if it doesn't already exist on your system.
3. Create a new bot application and invite it to your server.
4. Download the bot for your operating system, either a [release version](https://toastielab.dev/toastie-stuff/SupportChild/releases). Get the normal version if you have installed .NET 8 on your system, get the self contained version otherwise.
4. Download the bot for your operating system, either a [release version](https://toastielab.dev/toastie-stuff/SupportChild/releases) or a [dev build](https://ci.toastielab.dev/job/toastie-stuff/job/SupportChild/job/main/). Get the normal version if you have installed .NET 8 on your system, get the self contained version otherwise.
| Application | Description |
|-----------------------------|-----------------------------------------------------------------------|
@ -22,7 +23,7 @@ A support ticket Discord bot. Uses a MySQL database for storage of ticket inform
| `SupportChild-Windows.exe` | Larger Windows version which does not require .NET 9 to be installed. |
5. Run `./SupportChild_Linux` on Linux or `./SupportChild_Windows.exe` on Windows, this creates a config file in the current directory.
5. Run the bot application, `./SupportChild_<version>.exe`, this creates a config file in the current directory.
6. Set up the config, there are instructions inside. If you need more help either contact me in Discord or through an issue here.

View file

@ -147,8 +147,8 @@ internal static class SupportChild
try
{
Logger.Log("Connecting to database. (" + Config.hostName + ":" + Config.port + ")");
Database.SetConnectionString(Config.hostName, Config.port, Config.database, Config.username, Config.password);
Database.SetupTables();
Database.Connection.SetConnectionString(Config.hostName, Config.port, Config.database, Config.username, Config.password);
Database.Connection.SetupTables();
}
catch (Exception e)
{
@ -193,7 +193,6 @@ internal static class SupportChild
[
typeof(AddCategoryCommand),
typeof(AddCommand),
typeof(AddMessageCommand),
typeof(AddStaffCommand),
typeof(AdminCommands),
typeof(AssignCommand),
@ -201,6 +200,7 @@ internal static class SupportChild
typeof(CloseCommand),
typeof(CreateButtonPanelCommand),
typeof(CreateSelectionBoxPanelCommand),
typeof(InterviewCommands),
typeof(InterviewTemplateCommands),
typeof(ListAssignedCommand),
typeof(ListCommand),
@ -211,10 +211,9 @@ internal static class SupportChild
typeof(NewCommand),
typeof(RandomAssignCommand),
typeof(RemoveCategoryCommand),
typeof(RemoveMessageCommand),
typeof(RemoveStaffCommand),
typeof(InterviewCommands),
typeof(SayCommand),
typeof(SetMessageCommand),
typeof(SetSummaryCommand),
typeof(StatusCommand),
typeof(SummaryCommand),

View file

@ -2,7 +2,7 @@
<PropertyGroup>
<OutputType>Exe</OutputType>
<Version>4.0.0</Version>
<Version>4.0.1</Version>
<ApplicationIcon>ellie_icon.ico</ApplicationIcon>
<TargetFramework>net9.0</TargetFramework>
<StartupObject>SupportChild.SupportChild</StartupObject>
@ -19,7 +19,7 @@
<PackageIconUrl>https://cdn.elliebot.net/Ellie.png</PackageIconUrl>
<PackageLicenseFile>LICENSE</PackageLicenseFile>
<PackageProjectUrl>https://toastielab.dev/toastie-stuff/SupportChild</PackageProjectUrl>
<PackageVersion>4.0.0</PackageVersion>
<PackageVersion>4.0.1</PackageVersion>
<LangVersion>default</LangVersion>
<IncludeSourceRevisionInInformationalVersion>false</IncludeSourceRevisionInInformationalVersion>
<GitVersion>false</GitVersion>

View file

@ -20,6 +20,11 @@ public static class Extensions
{
return needles.Any(haystack.Contains);
}
public static string Truncate(this string value, int maxChars)
{
return value.Length <= maxChars ? value : string.Concat(value.AsSpan(0, maxChars - 3), "...");
}
}
public static class Utilities
@ -65,7 +70,7 @@ public static class Utilities
public static async Task<List<Database.Category>> GetVerifiedCategories()
{
List<Database.Category> verifiedCategories = new List<Database.Category>();
foreach (Database.Category category in Database.GetAllCategories())
foreach (Database.Category category in Database.Category.GetAllCategories())
{
DiscordChannel channel = null;
try