diff --git a/Commands/General/profile.js b/Commands/General/profile.js
index 855f9ed..c48cd5d 100644
--- a/Commands/General/profile.js
+++ b/Commands/General/profile.js
@@ -1,5 +1,4 @@
const {
- EmbedBuilder,
ModalBuilder,
TextInputBuilder,
TextInputStyle,
@@ -7,7 +6,14 @@ const {
ActionRowBuilder,
ButtonBuilder,
ButtonStyle,
- ApplicationCommandOptionType
+ ApplicationCommandOptionType,
+ MessageFlags,
+ ContainerBuilder,
+ TextDisplayBuilder,
+ SeparatorBuilder,
+ ThumbnailBuilder,
+ MediaGalleryBuilder,
+ MediaGalleryItemBuilder,
} = require("discord.js");
const fs = require("fs");
const yaml = require("js-yaml");
@@ -15,18 +21,182 @@ const db = require("../../Structures/Database.js");
const Command = require("../../Structures/Command.js");
const supportbot = yaml.load(fs.readFileSync("./Configs/supportbot.yml", "utf8"));
-const cmdconfig = yaml.load(fs.readFileSync("./Configs/commands.yml", "utf8"));
-const msgconfig = yaml.load(fs.readFileSync("./Configs/messages.yml", "utf8"));
+const cmdconfig = yaml.load(fs.readFileSync("./Configs/commands.yml", "utf8"));
+const msgconfig = yaml.load(fs.readFileSync("./Configs/messages.yml", "utf8"));
const clockedInUsers = new Set();
-function updateClockedInStatus(embed, clockedIn) {
- embed.data.fields = embed.data.fields.map(f =>
- f.name === "Clocked In Status"
- ? { ...f, value: clockedIn ? "✅ Clocked In" : "❌ Clocked Out" }
- : f
+function safeGetProfile(userId) {
+ try {
+ if (typeof db.getProfile !== "function") {
+ console.error("db.getProfile is not a function");
+ return {
+ bio: "",
+ timezone: "",
+ clockedIn: false,
+ };
+ }
+
+ const profile = db.getProfile(userId);
+
+ return {
+ bio: profile?.bio || "",
+ timezone: profile?.timezone || "",
+ clockedIn: Boolean(profile?.clockedIn),
+ };
+ } catch (error) {
+ console.error("Error getting profile:", error);
+ return {
+ bio: "",
+ timezone: "",
+ clockedIn: false,
+ };
+ }
+}
+
+function safeSetClockedIn(userId, value) {
+ try {
+ if (typeof db.setClockedIn !== "function") {
+ console.error("db.setClockedIn is not a function");
+ return false;
+ }
+
+ db.setClockedIn(userId, value);
+ return true;
+ } catch (error) {
+ console.error("Error setting clocked in status:", error);
+ return false;
+ }
+}
+
+function buildProfileContainer({
+ viewingUser,
+ viewingUserId,
+ bio,
+ timezone,
+ clockedIn,
+ isStaff,
+}) {
+ const container = new ContainerBuilder();
+
+ if (supportbot.Embed?.Colours?.General) {
+ container.setAccentColor(
+ parseInt(String(supportbot.Embed.Colours.General).replace("#", ""), 16),
+ );
+ }
+
+ const avatarUrl = viewingUser.displayAvatarURL({ size: 128, extension: "png" });
+
+ const gallery = new MediaGalleryBuilder().addItems(
+ new MediaGalleryItemBuilder().setURL(avatarUrl)
);
- return embed;
+
+ container.addMediaGalleryComponents(gallery);
+
+ container.addTextDisplayComponents(
+ new TextDisplayBuilder().setContent(`## ${viewingUser.username}'s Profile`)
+ );
+
+ container.addSeparatorComponents(new SeparatorBuilder().setDivider(true));
+
+ container.addTextDisplayComponents(
+ new TextDisplayBuilder().setContent(
+ [
+ `**Bio:** ${bio || "No bio set."}`,
+ `**Timezone:** ${timezone || "No timezone set."}`,
+ ...(isStaff
+ ? [`**Clocked In:** ${clockedIn ? "✅ Clocked In" : "❌ Clocked Out"}`]
+ : []),
+ ].join("\n")
+ )
+ );
+
+ return container;
+}
+
+function buildProfileButtons({ isOwnProfile, isStaff, clockedIn }) {
+ const row = new ActionRowBuilder();
+
+ if (isOwnProfile) {
+ row.addComponents(
+ new ButtonBuilder()
+ .setCustomId("editProfile")
+ .setLabel("Edit Profile")
+ .setStyle(ButtonStyle.Secondary),
+ );
+
+ if (isStaff) {
+ row.addComponents(
+ new ButtonBuilder()
+ .setCustomId("clockInOut")
+ .setLabel(clockedIn ? "Clock Out" : "Clock In")
+ .setStyle(ButtonStyle.Secondary),
+ );
+ }
+ }
+
+ if (isStaff && supportbot.Ticket?.ClaimTickets?.Enabled) {
+ row.addComponents(
+ new ButtonBuilder()
+ .setCustomId("viewTicketStats")
+ .setLabel("View Ticket Stats")
+ .setStyle(ButtonStyle.Secondary),
+ );
+ }
+
+ return row.components.length ? [row] : [];
+}
+
+function buildTicketStatsContainer(viewingUserId, ticketsClaimed, ticketsOpen, avgMinutes) {
+ const container = new ContainerBuilder();
+
+ if (supportbot.Embed?.Colours?.General) {
+ container.setAccentColor(
+ parseInt(String(supportbot.Embed.Colours.General).replace("#", ""), 16),
+ );
+ }
+
+ container.addTextDisplayComponents(
+ new TextDisplayBuilder().setContent(`# ${msgconfig.TicketStats.Title || "Ticket Stats"}`),
+ );
+
+ container.addSeparatorComponents(new SeparatorBuilder().setDivider(true));
+
+ container.addTextDisplayComponents(
+ new TextDisplayBuilder().setContent(
+ [
+ `> Ticket stats for <@${viewingUserId}>`,
+ ``,
+ `**${msgconfig.TicketStats.OpenTickets || "Open Tickets"}:** ${ticketsOpen.length}`,
+ `**${msgconfig.TicketStats.TotalTickets || "Total Tickets"}:** ${ticketsClaimed.length}`,
+ `**${msgconfig.TicketStats.ResponseTime || "Average Response Time"}:** ${avgMinutes} minutes`,
+ ].join("\n"),
+ ),
+ );
+
+ return container;
+}
+
+function buildOpenTicketsContainer(ticketsOpen) {
+ const container = new ContainerBuilder();
+
+ if (supportbot.Embed?.Colours?.General) {
+ container.setAccentColor(
+ parseInt(String(supportbot.Embed.Colours.General).replace("#", ""), 16),
+ );
+ }
+
+ container.addTextDisplayComponents(new TextDisplayBuilder().setContent(`# Active Tickets`));
+
+ container.addSeparatorComponents(new SeparatorBuilder().setDivider(true));
+
+ container.addTextDisplayComponents(
+ new TextDisplayBuilder().setContent(
+ ticketsOpen.length ? ticketsOpen.map((t) => `<#${t.id}>`).join("\n") : "No active tickets.",
+ ),
+ );
+
+ return container;
}
module.exports = new Command({
@@ -39,258 +209,319 @@ module.exports = new Command({
name: "user",
description: "The user to view the profile of",
type: ApplicationCommandOptionType.User,
- required: false
- }
+ required: false,
+ },
],
async run(interaction) {
- await interaction.deferReply({ ephemeral: true });
+ await interaction.deferReply({ flags: MessageFlags.Ephemeral });
- const viewingUser = interaction.options.getUser("user") || interaction.user;
- const viewingUserId = viewingUser.id;
- const interactionUserId = interaction.user.id;
+ try {
+ const viewingUser = interaction.options.getUser("user") || interaction.user;
+ const viewingUserId = viewingUser.id;
+ const interactionUserId = interaction.user.id;
- const member = await interaction.guild.members.fetch(viewingUserId);
- const { getRole } = interaction.client;
- const Staff = await getRole(supportbot.Roles.StaffMember.Staff, interaction.guild);
- const Admin = await getRole(supportbot.Roles.StaffMember.Admin, interaction.guild);
+ const member = await interaction.guild.members.fetch(viewingUserId);
+ const { getRole } = interaction.client;
- if (!Staff || !Admin) {
- return interaction.editReply({
- content: "Some roles seem to be missing!\nPlease check for errors when starting the bot.",
- ephemeral: true
- });
- }
+ const Staff = await getRole(supportbot.Roles.StaffMember.Staff, interaction.guild);
+ const Admin = await getRole(supportbot.Roles.StaffMember.Admin, interaction.guild);
- const isStaff = member.roles.cache.has(Staff.id) || member.roles.cache.has(Admin.id);
- const isOwnProfile = interactionUserId === viewingUserId;
-
- const profileData = db.getProfile(viewingUserId);
- let { bio, timezone, clockedIn } = profileData;
- clockedIn = Boolean(clockedIn);
-
- let profileEmbed = new EmbedBuilder()
- .setTitle(`${viewingUser.username}'s Profile`)
- .setColor(supportbot.Embed.Colours.General)
- .setThumbnail(viewingUser.displayAvatarURL())
- .addFields(
- { name: "Bio", value: bio || "No bio set.", inline: false },
- { name: "Timezone", value: timezone || "No timezone set.", inline: true }
- );
-
- if (isStaff) {
- profileEmbed.addFields({
- name: "Clocked In Status",
- value: clockedIn ? "✅ Clocked In" : "❌ Clocked Out",
- inline: true
- });
- }
-
- const buttonRow = new ActionRowBuilder();
- if (isOwnProfile) {
- buttonRow.addComponents(
- new ButtonBuilder()
- .setCustomId("editProfile")
- .setLabel("Edit Profile")
- .setStyle(ButtonStyle.Secondary)
- );
-
- if (isStaff) {
- buttonRow.addComponents(
- new ButtonBuilder()
- .setCustomId("clockInOut")
- .setLabel(clockedIn ? "Clock Out" : "Clock In")
- .setStyle(ButtonStyle.Secondary)
- );
- }
- }
-
- if (isStaff && supportbot.Ticket.ClaimTickets.Enabled) {
- buttonRow.addComponents(
- new ButtonBuilder()
- .setCustomId("viewTicketStats")
- .setLabel("View Ticket Stats")
- .setStyle(ButtonStyle.Secondary)
- );
- }
-
- await interaction.editReply({
- embeds: [profileEmbed],
- components: buttonRow.components.length ? [buttonRow] : []
- });
-
- const filter = i =>
- ["editProfile", "clockInOut", "viewTicketStats", "showOpenTickets", "backToStats", "backToProfile"]
- .includes(i.customId) &&
- i.user.id === interactionUserId;
-
- const collector = interaction.channel.createMessageComponentCollector({ filter, time: 60000 });
-
- collector.on("collect", async i => {
- if (i.customId === "editProfile") {
- const modal = new ModalBuilder()
- .setCustomId("editProfileModal")
- .setTitle("Edit Profile")
- .addComponents(
- new ActionRowBuilder().addComponents(
- new TextInputBuilder()
- .setCustomId("bio")
- .setLabel("Bio")
- .setStyle(TextInputStyle.Paragraph)
- .setPlaceholder("Enter your bio")
- .setValue(String(bio || ""))
- .setRequired(false)
- ),
- new ActionRowBuilder().addComponents(
- new TextInputBuilder()
- .setCustomId("timezone")
- .setLabel("Timezone")
- .setStyle(TextInputStyle.Short)
- .setPlaceholder("Enter your timezone")
- .setValue(String(timezone || ""))
- .setRequired(false)
- )
- );
-
- return i.showModal(modal);
+ if (!Staff || !Admin) {
+ return interaction.editReply({
+ content: "Some roles seem to be missing!\nPlease check for errors when starting the bot.",
+ });
}
- if (i.customId === "clockInOut") {
- const newStatus = !clockedIn;
- db.setClockedIn(viewingUserId, newStatus);
- clockedIn = newStatus;
- profileEmbed = updateClockedInStatus(profileEmbed, newStatus);
+ const isStaff = member.roles.cache.has(Staff.id) || member.roles.cache.has(Admin.id);
+ const isOwnProfile = interactionUserId === viewingUserId;
- if (newStatus) clockedInUsers.add(viewingUserId);
- else clockedInUsers.delete(viewingUserId);
+ if (typeof db.ensureProfile === "function") {
+ db.ensureProfile(viewingUserId);
+ }
- const updatedRow = new ActionRowBuilder().addComponents(
- new ButtonBuilder()
- .setCustomId("editProfile")
- .setLabel("Edit Profile")
- .setStyle(ButtonStyle.Secondary)
- );
- if (isStaff) {
- updatedRow.addComponents(
- new ButtonBuilder()
- .setCustomId("clockInOut")
- .setLabel(newStatus ? "Clock Out" : "Clock In")
- .setStyle(ButtonStyle.Secondary)
- );
- if (supportbot.Ticket.ClaimTickets.Enabled) {
- updatedRow.addComponents(
- new ButtonBuilder()
- .setCustomId("viewTicketStats")
- .setLabel("View Ticket Stats")
- .setStyle(ButtonStyle.Secondary)
+ let { bio, timezone, clockedIn } = safeGetProfile(viewingUserId);
+
+ const profileContainer = buildProfileContainer({
+ viewingUser,
+ viewingUserId,
+ bio,
+ timezone,
+ clockedIn,
+ isStaff,
+ });
+
+ const buttonRows = buildProfileButtons({
+ isOwnProfile,
+ isStaff,
+ clockedIn,
+ });
+
+ await interaction.editReply({
+ flags: MessageFlags.IsComponentsV2,
+ components: [profileContainer, ...buttonRows],
+ });
+
+ const filter = (i) =>
+ [
+ "editProfile",
+ "clockInOut",
+ "viewTicketStats",
+ "showOpenTickets",
+ "backToStats",
+ "backToProfile",
+ ].includes(i.customId) && i.user.id === interactionUserId;
+
+ const collector = interaction.channel.createMessageComponentCollector({
+ filter,
+ time: 60000,
+ });
+
+ collector.on("collect", async (i) => {
+ if (i.customId === "editProfile") {
+ const modal = new ModalBuilder()
+ .setCustomId("editProfileModal")
+ .setTitle("Edit Profile")
+ .addComponents(
+ new ActionRowBuilder().addComponents(
+ new TextInputBuilder()
+ .setCustomId("bio")
+ .setLabel("Bio")
+ .setStyle(TextInputStyle.Paragraph)
+ .setPlaceholder("Enter your bio")
+ .setValue(String(bio || ""))
+ .setRequired(false),
+ ),
+ new ActionRowBuilder().addComponents(
+ new TextInputBuilder()
+ .setCustomId("timezone")
+ .setLabel("Timezone")
+ .setStyle(TextInputStyle.Short)
+ .setPlaceholder("Enter your timezone")
+ .setValue(String(timezone || ""))
+ .setRequired(false),
+ ),
);
- }
+
+ return i.showModal(modal);
}
- await i.update({ embeds: [profileEmbed], components: [updatedRow] });
+ if (i.customId === "clockInOut") {
+ const newStatus = !clockedIn;
+ const saved = safeSetClockedIn(viewingUserId, newStatus);
- const clockEmbed = new EmbedBuilder()
- .setTitle(newStatus ? "Clocked In!" : "Clocked Out!")
- .setDescription(newStatus ? "You have successfully clocked in." : "You have successfully clocked out.")
- .setColor(supportbot.Embed.Colours.General);
-
- return interaction.followUp({ embeds: [clockEmbed], ephemeral: true });
- }
-
- if (i.customId === "viewTicketStats") {
- const TicketData = JSON.parse(fs.readFileSync("./Data/TicketData.json", "utf8"));
- const ticketsClaimed = TicketData.tickets.filter(t => t.claimedBy === viewingUserId);
- const ticketsOpen = ticketsClaimed.filter(t => t.open);
- const totalTickets = ticketsClaimed.length;
-
- let totalResponseTime = 0;
- let responseCount = 0;
- ticketsClaimed.forEach(t => {
- if (t.claimedAt && t.createdAt) {
- totalResponseTime += new Date(t.claimedAt) - new Date(t.createdAt);
- responseCount++;
+ if (!saved) {
+ return i.reply({
+ content:
+ "Could not update clock in status because the profile database is not available.",
+ flags: MessageFlags.Ephemeral,
+ });
}
- });
- const avgMinutes = responseCount ? Math.round(totalResponseTime / responseCount / 60000) : 0;
+ clockedIn = newStatus;
- const statsEmbed = new EmbedBuilder()
- .setTitle(msgconfig.TicketStats.Title)
- .setDescription(`> Ticket stats for <@${viewingUserId}>`)
- .setColor(supportbot.Embed.Colours.General)
- .addFields(
- { name: msgconfig.TicketStats.OpenTickets, value: `${ticketsOpen.length}`, inline: true },
- { name: msgconfig.TicketStats.TotalTickets, value: `${totalTickets}`, inline: true },
- { name: msgconfig.TicketStats.ResponseTime, value: `${avgMinutes} minutes`, inline: false }
+ if (newStatus) clockedInUsers.add(viewingUserId);
+ else clockedInUsers.delete(viewingUserId);
+
+ const updatedProfileContainer = buildProfileContainer({
+ viewingUser,
+ viewingUserId,
+ bio,
+ timezone,
+ clockedIn,
+ isStaff,
+ });
+
+ const updatedButtonRows = buildProfileButtons({
+ isOwnProfile,
+ isStaff,
+ clockedIn,
+ });
+
+ await i.update({
+ flags: MessageFlags.IsComponentsV2,
+ components: [updatedProfileContainer, ...updatedButtonRows],
+ });
+
+ return interaction.followUp({
+ content: newStatus
+ ? "You have successfully clocked in."
+ : "You have successfully clocked out.",
+ flags: MessageFlags.Ephemeral,
+ });
+ }
+
+ if (i.customId === "viewTicketStats") {
+ let TicketData = { tickets: [] };
+
+ try {
+ TicketData = JSON.parse(fs.readFileSync("./Data/TicketData.json", "utf8"));
+ } catch (error) {
+ console.error("Error reading TicketData.json:", error);
+ }
+
+ const ticketsClaimed = TicketData.tickets.filter((t) => t.claimedBy === viewingUserId);
+ const ticketsOpen = ticketsClaimed.filter((t) => t.open);
+ const totalTickets = ticketsClaimed.length;
+
+ let totalResponseTime = 0;
+ let responseCount = 0;
+
+ ticketsClaimed.forEach((t) => {
+ if (t.claimedAt && t.createdAt) {
+ totalResponseTime += new Date(t.claimedAt) - new Date(t.createdAt);
+ responseCount++;
+ }
+ });
+
+ const avgMinutes = responseCount
+ ? Math.round(totalResponseTime / responseCount / 60000)
+ : 0;
+
+ const statsContainer = buildTicketStatsContainer(
+ viewingUserId,
+ ticketsClaimed,
+ ticketsOpen,
+ avgMinutes,
);
- const statsRow = new ActionRowBuilder().addComponents(
- new ButtonBuilder().setCustomId("showOpenTickets").setLabel("Show Open Tickets").setStyle(ButtonStyle.Secondary),
- new ButtonBuilder().setCustomId("backToProfile").setLabel("Back to Profile").setStyle(ButtonStyle.Secondary)
- );
-
- return i.update({ embeds: [statsEmbed], components: [statsRow] });
- }
-
- if (i.customId === "showOpenTickets") {
- const TicketData = JSON.parse(fs.readFileSync("./Data/TicketData.json", "utf8"));
- const ticketsOpen = TicketData.tickets.filter(t => t.claimedBy === viewingUserId && t.open);
-
- const openEmbed = new EmbedBuilder()
- .setTitle("Active Tickets")
- .setDescription(ticketsOpen.map(t => `<#${t.id}>`).join("\n") || "No active tickets.")
- .setColor(supportbot.Embed.Colours.General);
-
- const backRow = new ActionRowBuilder().addComponents(
- new ButtonBuilder().setCustomId("backToStats").setLabel("Back to Stats").setStyle(ButtonStyle.Secondary)
- );
-
- return i.update({ embeds: [openEmbed], components: [backRow] });
- }
-
- if (i.customId === "backToStats") {
-
- const TicketData = JSON.parse(fs.readFileSync("./Data/TicketData.json", "utf8"));
- const ticketsClaimed = TicketData.tickets.filter(t => t.claimedBy === viewingUserId);
- const ticketsOpen = ticketsClaimed.filter(t => t.open);
- const totalTickets = ticketsClaimed.length;
-
- let totalResponseTime = 0;
- let responseCount = 0;
- ticketsClaimed.forEach(t => {
- if (t.claimedAt && t.createdAt) {
- totalResponseTime += new Date(t.claimedAt) - new Date(t.createdAt);
- responseCount++;
- }
- });
- const avgMinutes = responseCount ? Math.round(totalResponseTime / responseCount / 60000) : 0;
-
- const statsEmbed = new EmbedBuilder()
- .setTitle(msgconfig.TicketStats.Title)
- .setDescription(`> Ticket stats for <@${viewingUserId}>`)
- .setColor(supportbot.Embed.Colours.General)
- .addFields(
- { name: msgconfig.TicketStats.OpenTickets, value: `${ticketsOpen.length}`, inline: true },
- { name: msgconfig.TicketStats.TotalTickets, value: `${totalTickets}`, inline: true },
- { name: msgconfig.TicketStats.ResponseTime, value: `${avgMinutes} minutes`, inline: false }
+ const statsRow = new ActionRowBuilder().addComponents(
+ new ButtonBuilder()
+ .setCustomId("showOpenTickets")
+ .setLabel("Show Open Tickets")
+ .setStyle(ButtonStyle.Secondary),
+ new ButtonBuilder()
+ .setCustomId("backToProfile")
+ .setLabel("Back to Profile")
+ .setStyle(ButtonStyle.Secondary),
);
- const statsRow = new ActionRowBuilder().addComponents(
- new ButtonBuilder().setCustomId("showOpenTickets").setLabel("Show Open Tickets").setStyle(ButtonStyle.Secondary),
- new ButtonBuilder().setCustomId("backToProfile").setLabel("Back to Profile").setStyle(ButtonStyle.Secondary)
- );
+ return i.update({
+ flags: MessageFlags.IsComponentsV2,
+ components: [statsContainer, statsRow],
+ });
+ }
- return i.update({ embeds: [statsEmbed], components: [statsRow] });
+ if (i.customId === "showOpenTickets") {
+ let TicketData = { tickets: [] };
+
+ try {
+ TicketData = JSON.parse(fs.readFileSync("./Data/TicketData.json", "utf8"));
+ } catch (error) {
+ console.error("Error reading TicketData.json:", error);
+ }
+
+ const ticketsOpen = TicketData.tickets.filter(
+ (t) => t.claimedBy === viewingUserId && t.open,
+ );
+
+ const openContainer = buildOpenTicketsContainer(ticketsOpen);
+
+ const backRow = new ActionRowBuilder().addComponents(
+ new ButtonBuilder()
+ .setCustomId("backToStats")
+ .setLabel("Back to Stats")
+ .setStyle(ButtonStyle.Secondary),
+ );
+
+ return i.update({
+ flags: MessageFlags.IsComponentsV2,
+ components: [openContainer, backRow],
+ });
+ }
+
+ if (i.customId === "backToStats") {
+ let TicketData = { tickets: [] };
+
+ try {
+ TicketData = JSON.parse(fs.readFileSync("./Data/TicketData.json", "utf8"));
+ } catch (error) {
+ console.error("Error reading TicketData.json:", error);
+ }
+
+ const ticketsClaimed = TicketData.tickets.filter((t) => t.claimedBy === viewingUserId);
+ const ticketsOpen = ticketsClaimed.filter((t) => t.open);
+
+ let totalResponseTime = 0;
+ let responseCount = 0;
+
+ ticketsClaimed.forEach((t) => {
+ if (t.claimedAt && t.createdAt) {
+ totalResponseTime += new Date(t.claimedAt) - new Date(t.createdAt);
+ responseCount++;
+ }
+ });
+
+ const avgMinutes = responseCount
+ ? Math.round(totalResponseTime / responseCount / 60000)
+ : 0;
+
+ const statsContainer = buildTicketStatsContainer(
+ viewingUserId,
+ ticketsClaimed,
+ ticketsOpen,
+ avgMinutes,
+ );
+
+ const statsRow = new ActionRowBuilder().addComponents(
+ new ButtonBuilder()
+ .setCustomId("showOpenTickets")
+ .setLabel("Show Open Tickets")
+ .setStyle(ButtonStyle.Secondary),
+ new ButtonBuilder()
+ .setCustomId("backToProfile")
+ .setLabel("Back to Profile")
+ .setStyle(ButtonStyle.Secondary),
+ );
+
+ return i.update({
+ flags: MessageFlags.IsComponentsV2,
+ components: [statsContainer, statsRow],
+ });
+ }
+
+ if (i.customId === "backToProfile") {
+ const updatedProfileContainer = buildProfileContainer({
+ viewingUser,
+ viewingUserId,
+ bio,
+ timezone,
+ clockedIn,
+ isStaff,
+ });
+
+ const updatedButtonRows = buildProfileButtons({
+ isOwnProfile,
+ isStaff,
+ clockedIn,
+ });
+
+ return i.update({
+ flags: MessageFlags.IsComponentsV2,
+ components: [updatedProfileContainer, ...updatedButtonRows],
+ });
+ }
+ });
+ } catch (error) {
+ console.error("Error executing profile command:", error);
+
+ if (interaction.deferred || interaction.replied) {
+ return interaction
+ .editReply({
+ content: "An error occurred while loading the profile.",
+ components: [],
+ })
+ .catch(() => {});
}
- if (i.customId === "backToProfile") {
- return i.update({
- embeds: [profileEmbed],
- components: buttonRow.components.length ? [buttonRow] : []
- });
- }
- });
- }
+ return interaction
+ .reply({
+ content: "An error occurred while loading the profile.",
+ flags: MessageFlags.Ephemeral,
+ })
+ .catch(() => {});
+ }
+ },
});
module.exports.clockedInUsers = clockedInUsers;
diff --git a/Commands/General/translate.js b/Commands/General/translate.js
index 734d348..4363f1b 100644
--- a/Commands/General/translate.js
+++ b/Commands/General/translate.js
@@ -2,116 +2,205 @@
// Translate Command
const fs = require("fs");
-
const Discord = require("discord.js");
const yaml = require("js-yaml");
-const supportbot = yaml.load(
- fs.readFileSync("./Configs/supportbot.yml", "utf8")
-);
+const { translate } = require("@vitalets/google-translate-api");
-const cmdconfig = yaml.load(
- fs.readFileSync("./Configs/commands.yml", "utf8")
-);
-
-const msgconfig = yaml.load(
- fs.readFileSync("./Configs/messages.yml", "utf8")
-)
+const supportbot = yaml.load(fs.readFileSync("./Configs/supportbot.yml", "utf8"));
+const cmdconfig = yaml.load(fs.readFileSync("./Configs/commands.yml", "utf8"));
+const msgconfig = yaml.load(fs.readFileSync("./Configs/messages.yml", "utf8"));
const Command = require("../../Structures/Command.js");
-const translate = require("@vitalets/google-translate-api");
+
+const LANGUAGES = [
+ { name: "Auto Detect", value: "auto" },
+ { name: "English", value: "en" },
+ { name: "Spanish", value: "es" },
+ { name: "French", value: "fr" },
+ { name: "German", value: "de" },
+ { name: "Italian", value: "it" },
+ { name: "Portuguese", value: "pt" },
+ { name: "Dutch", value: "nl" },
+ { name: "Polish", value: "pl" },
+ { name: "Russian", value: "ru" },
+ { name: "Ukrainian", value: "uk" },
+ { name: "Turkish", value: "tr" },
+ { name: "Arabic", value: "ar" },
+ { name: "Hebrew", value: "he" },
+ { name: "Hindi", value: "hi" },
+ { name: "Japanese", value: "ja" },
+ { name: "Korean", value: "ko" },
+ { name: "Chinese (Simplified)", value: "zh-cn" },
+ { name: "Chinese (Traditional)", value: "zh-tw" },
+ { name: "Thai", value: "th" },
+ { name: "Vietnamese", value: "vi" },
+ { name: "Indonesian", value: "id" },
+ { name: "Greek", value: "el" },
+ { name: "Swedish", value: "sv" },
+ { name: "Romanian", value: "ro" },
+];
+
+function buildContainer(title, lines = [], color = null) {
+ const container = new Discord.ContainerBuilder();
+
+ if (color) {
+ container.setAccentColor(parseInt(color.replace("#", ""), 16));
+ }
+
+ container.addTextDisplayComponents(new Discord.TextDisplayBuilder().setContent(`## ${title}`));
+
+ container.addSeparatorComponents(new Discord.SeparatorBuilder().setDivider(true));
+
+ container.addTextDisplayComponents(new Discord.TextDisplayBuilder().setContent(lines.join("\n")));
+
+ return container;
+}
+
+function trim(text, max = 1800) {
+ if (text.length <= max) return text;
+ return text.substring(0, max) + "...";
+}
module.exports = new Command({
name: cmdconfig.Translate.Command,
description: cmdconfig.Translate.Description,
type: Discord.ApplicationCommandType.ChatInput,
+
options: [
{
type: Discord.ApplicationCommandOptionType.String,
- name: "language",
- description: "A 2 letter language code",
+ name: "from",
+ description: "Language to translate FROM",
required: true,
+ choices: LANGUAGES,
+ },
+ {
+ type: Discord.ApplicationCommandOptionType.String,
+ name: "to",
+ description: "Language to translate TO",
+ required: true,
+ choices: LANGUAGES.filter((l) => l.value !== "auto"),
},
{
type: Discord.ApplicationCommandOptionType.String,
name: "text",
- description: "The text to translate",
+ description: "Text to translate",
required: true,
- }, // The input text for the translation
+ },
],
- permissions: cmdconfig.Translate.Permission, // The permission the user/role at least requires
+
+ permissions: cmdconfig.Translate.Permission,
async run(interaction) {
- let disableCommand = true;
+ const { getRole, getChannel } = interaction.client;
- const { getRole } = interaction.client;
- let SupportStaff = await getRole(supportbot.Roles.StaffMember.Staff, interaction.guild);
- let Admin = await getRole(supportbot.Roles.StaffMember.Admin, interaction.guild);
- if (!SupportStaff || !Admin)
-
- return interaction.reply(
- "Some roles seem to be missing!\nPlease check for errors when starting the bot."
- );
+ const Staff = await getRole(supportbot.Roles.StaffMember.Staff, interaction.guild);
- const NoPerms = new Discord.EmbedBuilder()
- .setTitle("Invalid Permissions!")
- .setDescription(
- `${msgconfig.Error.IncorrectPerms}\n\nRole Required: \`${supportbot.Roles.StaffMember.Staff}\` or \`${supportbot.Roles.StaffMember.Admin}\``
- )
- .setColor(supportbot.Embed.Colours.Warn);
+ const Admin = await getRole(supportbot.Roles.StaffMember.Admin, interaction.guild);
+
+ if (!Staff || !Admin) {
+ return interaction.reply({
+ content: "Some roles seem to be missing!",
+ flags: Discord.MessageFlags.Ephemeral,
+ });
+ }
if (
- !interaction.member.roles.cache.has(SupportStaff.id) &&
+ !interaction.member.roles.cache.has(Staff.id) &&
!interaction.member.roles.cache.has(Admin.id)
- )
- return interaction.reply({ embeds: [NoPerms] });
+ ) {
+ return interaction.reply({
+ flags: Discord.MessageFlags.IsComponentsV2 | Discord.MessageFlags.Ephemeral,
+ components: [
+ buildContainer(
+ "Invalid Permissions",
+ [
+ msgconfig.Error.IncorrectPerms,
+ "",
+ `Required role: \`${supportbot.Roles.StaffMember.Staff}\` or \`${supportbot.Roles.StaffMember.Admin}\``,
+ ],
+ supportbot.Embed.Colours.Warn,
+ ),
+ ],
+ });
+ }
- const { getChannel } = interaction.client;
- let lang = interaction.options.getString("language"); // Grab choice of language code by user
- let url = "https://www.science.co.il/language/Codes.php"; // URL to all available language codes
- let text = await interaction.options.getString("text"); // Grab the text to translate by the user
- let translatelog = await getChannel(
- supportbot.Translate.TranslateLog,
- interaction.guild
- ); // Grab the set logging channel for translations
+ await interaction.deferReply({ flags: Discord.MessageFlags.Ephemeral });
+ const fromLang = interaction.options.getString("from");
+ const toLang = interaction.options.getString("to");
+ const text = interaction.options.getString("text");
- // Return message if user has provided an invalid language code
- const result = await translate(text, { to: lang }).catch(async (err) => {
- if (err && err.code == 400) {
- await interaction.reply({
- embeds: [
- new Discord.EmbedBuilder()
- .setColor(supportbot.Embed.Colours.Warn)
- .setTitle("Valid Language Codes")
- .setURL(url)
- .setDescription(
- "Click the title to see which 2 letter language codes are valid."
- ),
- ],
- flags: Discord.MessageFlags.Ephemeral,
- });
- }
+ const translateLog = await getChannel(supportbot.Translate?.TranslateLog, interaction.guild);
+
+ let result;
+
+ try {
+ result = await translate(text, {
+ from: fromLang === "auto" ? undefined : fromLang,
+ to: toLang,
+ });
+ } catch (err) {
+ console.error("Translate error:", err);
+
+ return interaction.editReply({
+ flags: Discord.MessageFlags.IsComponentsV2,
+ components: [
+ buildContainer(
+ "Translation Failed",
+ ["The translation service returned an error."],
+ supportbot.Embed.Colours.Warn,
+ ),
+ ],
+ });
+ }
+
+ const detectedLanguage = result.from?.language?.iso || result.from?.language || "unknown";
+
+ const container = buildContainer(
+ "Translation",
+ [
+ `**Requested By:** <@${interaction.user.id}>`,
+ `**From:** \`${fromLang === "auto" ? detectedLanguage : fromLang}\``,
+ `**To:** \`${toLang}\``,
+ "",
+ "**Original Text**",
+ trim(text),
+ "",
+ "**Translated Text**",
+ trim(result.text),
+ ],
+ supportbot.Embed.Colours.Success,
+ );
+
+ await interaction.editReply({
+ flags: Discord.MessageFlags.IsComponentsV2,
+ components: [container],
});
- if (!result) return;
- // Embed containing language code, original text and translated text
- let transembed = new Discord.EmbedBuilder()
- .setAuthor({ name: interaction.user.tag, iconURL: interaction.user.displayAvatarURL({ dynamic: true }) })
- .setColor(supportbot.Embed.Colours.Success)
- .setDescription(`**Translation to ${lang}**`)
- .addFields(
- { name: "Original text", value: text },
- { name: "Translated text", value: result.text },
- )
- .setFooter({ text: `Author: ${interaction.user.id}` })
- .setTimestamp();
+ if (translateLog) {
+ const logContainer = buildContainer(
+ "Translation Log",
+ [
+ `User: <@${interaction.user.id}>`,
+ `Channel: <#${interaction.channel.id}>`,
+ `From: ${fromLang}`,
+ `To: ${toLang}`,
+ "",
+ trim(text),
+ "",
+ trim(result.text),
+ ],
+ supportbot.Embed.Colours.General,
+ );
- // Send embed [transembed] with translated message to channel
- await interaction.reply({ embeds: [transembed] });
-
- // Send embed [transembed] to logging channel
- await translatelog.send({ embeds: [transembed] });
+ translateLog
+ .send({
+ flags: Discord.MessageFlags.IsComponentsV2,
+ components: [logContainer],
+ })
+ .catch(() => {});
+ }
},
-
-})
\ No newline at end of file
+});
diff --git a/Commands/Tickets/addUser.js b/Commands/Tickets/addUser.js
deleted file mode 100644
index f5de800..0000000
--- a/Commands/Tickets/addUser.js
+++ /dev/null
@@ -1,119 +0,0 @@
-const fs = require("fs");
-const {
- EmbedBuilder,
- ApplicationCommandOptionType,
- ApplicationCommandType,
- ChannelType,
- MessageFlags,
-} = require("discord.js");
-const yaml = require("js-yaml");
-
-const supportbot = yaml.load(fs.readFileSync("./Configs/supportbot.yml", "utf8"));
-const msgconfig = yaml.load(fs.readFileSync("./Configs/messages.yml", "utf8"));
-const cmdconfig = yaml.load(fs.readFileSync("./Configs/commands.yml", "utf8"));
-
-const Command = require("../../Structures/Command.js");
-const db = require("../../Structures/Database.js");
-
-module.exports = new Command({
- name: cmdconfig.ForceAddUser.Command,
- description: cmdconfig.ForceAddUser.Description,
- type: ApplicationCommandType.ChatInput,
- options: [
- {
- name: "user",
- description: "The user to forcefully add",
- type: ApplicationCommandOptionType.User,
- required: true,
- },
- ],
- permissions: cmdconfig.ForceAddUser.Permission,
-
- async run(interaction) {
- const userToAdd = interaction.options.getUser("user");
- const ticketChannel = interaction.channel;
-
- if (
- (supportbot.Ticket.TicketType === "threads" && !ticketChannel.isThread()) ||
- (supportbot.Ticket.TicketType === "channels" && ticketChannel.type !== ChannelType.GuildText)
- ) {
- const onlyInTicket = new EmbedBuilder()
- .setTitle(msgconfig.ForceAddUser.NotInTicket_Title)
- .setDescription(msgconfig.ForceAddUser.NotInTicket_Description)
- .setColor(supportbot.Embed.Colours.Error);
-
- return interaction.reply({
- embeds: [onlyInTicket],
- flags: MessageFlags.Ephemeral,
- });
- }
-
- const ticketRow = db.getTicket(ticketChannel.id);
- if (!ticketRow) {
- const notFound = new EmbedBuilder()
- .setTitle(msgconfig.ForceAddUser.NotInTicket_Title)
- .setDescription(msgconfig.ForceAddUser.NotInTicket_Description)
- .setColor(supportbot.Embed.Colours.Error);
-
- return interaction.reply({
- embeds: [notFound],
- flags: Discord.MessageFlags.Ephemeral,
- });
- }
-
- try {
- if (supportbot.Ticket.TicketType === "threads") {
- const botMember = await ticketChannel.members.fetch(interaction.client.user.id).catch(() => null);
- if (!botMember) await ticketChannel.join();
- await new Promise(res => setTimeout(res, 1000));
- await ticketChannel.members.add(userToAdd.id);
- } else if (supportbot.Ticket.TicketType === "channels") {
- await ticketChannel.permissionOverwrites.create(userToAdd.id, {
- ViewChannel: true,
- SendMessages: true,
- ReadMessageHistory: true,
- });
- }
-
- db.addUserToTicket(ticketChannel.id, userToAdd.id);
-
- const addedEmbed = new EmbedBuilder()
- .setTitle(msgconfig.ForceAddUser.Added_Title)
- .setDescription(
- msgconfig.ForceAddUser.Added_Description.replace("%username%", userToAdd.username)
- )
- .setColor(supportbot.Embed.Colours.Success);
-
- await interaction.reply({
- embeds: [addedEmbed],
- flags: Discord.MessageFlags.Ephemeral,
- });
-
- const addedToTicketEmbed = new EmbedBuilder()
- .setTitle(msgconfig.ForceAddUser.AddedToTicket_Title)
- .setDescription(
- msgconfig.ForceAddUser.AddedToTicket_Description.replace(
- "%channel_link%",
- `[${ticketChannel.name}](https://discord.com/channels/${ticketChannel.guild.id}/${ticketChannel.id})`
- )
- )
- .setColor(supportbot.Embed.Colours.General);
-
- await userToAdd.send({
- embeds: [addedToTicketEmbed],
- });
- } catch (err) {
- console.error("Error adding user to the ticket:", err);
-
- const errorEmbed = new EmbedBuilder()
- .setTitle(msgconfig.ForceAddUser.Error_Title)
- .setDescription(msgconfig.ForceAddUser.Error_Adding)
- .setColor(supportbot.Embed.Colours.Error);
-
- await interaction.reply({
- embeds: [errorEmbed],
- flags: Discord.MessageFlags.Ephemeral,
- });
- }
- },
-});
diff --git a/Commands/Tickets/close.js b/Commands/Tickets/close.js
index 6d618e8..9d0e8bd 100644
--- a/Commands/Tickets/close.js
+++ b/Commands/Tickets/close.js
@@ -1,14 +1,400 @@
+
+
const fs = require("fs");
const Discord = require("discord.js");
const yaml = require("js-yaml");
const db = require("../../Structures/Database.js");
+const TicketManager = require("../../Structures/TicketManager.js");
+const Command = require("../../Structures/Command.js");
const supportbot = yaml.load(fs.readFileSync("./Configs/supportbot.yml", "utf8"));
const cmdconfig = yaml.load(fs.readFileSync("./Configs/commands.yml", "utf8"));
const msgconfig = yaml.load(fs.readFileSync("./Configs/messages.yml", "utf8"));
-const Command = require("../../Structures/Command.js");
+function getDepartmentConfig(departmentKey) {
+ return supportbot.Ticket?.DepartmentSystem?.Departments?.[departmentKey] || null;
+}
+
+function getTicketUserId(ticket) {
+ return ticket?.user || ticket?.user_id || null;
+}
+
+function buildContainer(title, lines = []) {
+ const container = new Discord.ContainerBuilder();
+
+ if (supportbot.Embed?.Colours?.General) {
+ container.setAccentColor(
+ parseInt(String(supportbot.Embed.Colours.General).replace("#", ""), 16),
+ );
+ }
+
+ container.addTextDisplayComponents(new Discord.TextDisplayBuilder().setContent(`## ${title}`));
+
+ container.addSeparatorComponents(new Discord.SeparatorBuilder().setDivider(true));
+
+ if (lines.length > 0) {
+ container.addTextDisplayComponents(
+ new Discord.TextDisplayBuilder().setContent(lines.join("\n")),
+ );
+ }
+
+ return container;
+}
+
+function buildReviewSummaryContainer(ticket, rating, comment) {
+ const departmentConfig = ticket.department ? getDepartmentConfig(ticket.department) : null;
+ const departmentName = departmentConfig?.Name || ticket.department || null;
+ const departmentEmoji = departmentConfig?.Emoji || "🎫";
+
+ const claimedByEnabled = supportbot.Ticket?.ClaimTickets?.Enabled === true;
+ const claimedBy = claimedByEnabled && ticket.claimedBy ? `<@${ticket.claimedBy}>` : null;
+
+ const ticketUserId = getTicketUserId(ticket);
+ const lines = [
+ `**${msgconfig.ReviewSystem.ReviewEmbed.ReviewedByTitle}** ${ticketUserId ? `<@${ticketUserId}>` : "Unknown User"}`,
+ ];
+
+ if (claimedBy) {
+ lines.unshift(`**${msgconfig.ReviewSystem.ReviewEmbed.ReviewedStaffTitle}** ${claimedBy}`);
+ }
+
+ if (supportbot.Ticket?.DepartmentSystem?.Enabled === true && departmentName) {
+ lines.push(`**Department** ${departmentEmoji} ${departmentName}`);
+ }
+
+ lines.push("");
+ lines.push(`**${msgconfig.ReviewSystem.ReviewEmbed.RatingTitle}**`);
+ lines.push(`${msgconfig.ReviewSystem.ReviewEmbed.ReviewEmoji.repeat(Number(rating))}`);
+ lines.push("");
+ lines.push(`**${msgconfig.ReviewSystem.ReviewEmbed.CommentTitle}**`);
+ lines.push(comment || "No Comment Provided");
+
+ const container = new Discord.ContainerBuilder();
+
+ if (msgconfig.ReviewSystem?.ReviewEmbed?.Color) {
+ container.setAccentColor(
+ parseInt(String(msgconfig.ReviewSystem.ReviewEmbed.Color).replace("#", ""), 16),
+ );
+ } else if (supportbot.Embed?.Colours?.General) {
+ container.setAccentColor(
+ parseInt(String(supportbot.Embed.Colours.General).replace("#", ""), 16),
+ );
+ }
+
+ container.addTextDisplayComponents(
+ new Discord.TextDisplayBuilder().setContent(`## ${msgconfig.ReviewSystem.ReviewEmbed.Title}`),
+ );
+
+ container.addSeparatorComponents(new Discord.SeparatorBuilder().setDivider(true));
+
+ container.addTextDisplayComponents(new Discord.TextDisplayBuilder().setContent(lines.join("\n")));
+
+ return container;
+}
+
+async function collectReview(interaction, ticket) {
+ const ticketUserId = getTicketUserId(ticket);
+ if (!ticketUserId) return null;
+
+ const ratingContainer = buildContainer(msgconfig.ReviewSystem.Rate.Title, [
+ msgconfig.ReviewSystem.Rate.Description,
+ "",
+ ]);
+
+ const ratingRow = new Discord.ActionRowBuilder().addComponents(
+ new Discord.StringSelectMenuBuilder()
+ .setCustomId(`closeReviewRating:${interaction.id}`)
+ .setPlaceholder("Select a rating")
+ .addOptions([
+ { label: msgconfig.ReviewSystem.Stars.One, value: "1" },
+ { label: msgconfig.ReviewSystem.Stars.Two, value: "2" },
+ { label: msgconfig.ReviewSystem.Stars.Three, value: "3" },
+ { label: msgconfig.ReviewSystem.Stars.Four, value: "4" },
+ { label: msgconfig.ReviewSystem.Stars.Five, value: "5" },
+ ]),
+ );
+
+ ratingContainer.addActionRowComponents(ratingRow);
+
+ const ratingPrompt = await interaction.followUp({
+ flags: Discord.MessageFlags.IsComponentsV2,
+ components: [ratingContainer],
+ });
+
+ let rating = null;
+
+ try {
+ const ratingInteraction = await ratingPrompt.awaitMessageComponent({
+ componentType: Discord.ComponentType.StringSelect,
+ time: 120000,
+ filter: (i) =>
+ i.customId === `closeReviewRating:${interaction.id}` && i.user.id === ticketUserId,
+ });
+
+ rating = ratingInteraction.values[0];
+ await ratingInteraction.deferUpdate().catch(() => {});
+ } catch {
+ return null;
+ }
+
+ const commentContainer = buildContainer(msgconfig.ReviewSystem.Comment.Title, [
+ msgconfig.ReviewSystem.Comment.Description,
+ "",
+ ]);
+
+ const commentRow = new Discord.ActionRowBuilder().addComponents(
+ new Discord.ButtonBuilder()
+ .setCustomId(`closeReviewWrite:${interaction.id}`)
+ .setLabel("Write Comment")
+ .setStyle(Discord.ButtonStyle.Secondary),
+ new Discord.ButtonBuilder()
+ .setCustomId(`closeReviewSkip:${interaction.id}`)
+ .setLabel("Skip Comment")
+ .setStyle(Discord.ButtonStyle.Danger),
+ );
+
+ commentContainer.addActionRowComponents(commentRow);
+
+ const commentPrompt = await interaction.followUp({
+ flags: Discord.MessageFlags.IsComponentsV2,
+ components: [commentContainer],
+ });
+
+ let comment = "No Comment Provided";
+
+ try {
+ const buttonInteraction = await commentPrompt.awaitMessageComponent({
+ componentType: Discord.ComponentType.Button,
+ time: 120000,
+ filter: (i) =>
+ (i.customId === `closeReviewWrite:${interaction.id}` ||
+ i.customId === `closeReviewSkip:${interaction.id}`) &&
+ i.user.id === ticketUserId,
+ });
+
+ if (buttonInteraction.customId === `closeReviewSkip:${interaction.id}`) {
+ await buttonInteraction.deferUpdate().catch(() => {});
+ return { rating, comment };
+ }
+
+ await buttonInteraction
+ .update({
+ flags: Discord.MessageFlags.IsComponentsV2,
+ components: [
+ buildContainer(msgconfig.ReviewSystem.Comment.Title, [
+ `Requested from: <@${ticketUserId}>`,
+ "",
+ "Please send your review comment in the channel now.",
+ "Type `no` to skip.",
+ ]),
+ ],
+ })
+ .catch(() => {});
+
+ const collected = await interaction.channel.awaitMessages({
+ max: 1,
+ time: 120000,
+ filter: (m) => m.author.id === ticketUserId,
+ });
+
+ const message = collected.first();
+
+ if (message) {
+ const rawComment = message.content?.trim();
+ if (rawComment && rawComment.toLowerCase() !== "no") {
+ comment = rawComment;
+ }
+
+ await message.delete().catch(() => {});
+ }
+ } catch {
+ return { rating, comment };
+ }
+
+ return { rating, comment };
+}
+
+function getTranscriptAttachmentFromResult(result) {
+ if (!result) return null;
+
+ if (typeof result === "string") {
+ return result;
+ }
+
+ if (result.filePath) return result.filePath;
+ if (result.attachment) return result.attachment;
+ if (result.path) return result.path;
+
+ return null;
+}
+
+async function maybeDMTranscript(interaction, ticket, transcriptResult) {
+ if (supportbot.Ticket?.UserManagement?.DMTranscript !== true) {
+ return;
+ }
+
+ const ticketUserId = getTicketUserId(ticket);
+ if (!ticketUserId) return;
+
+ const ticketUser = await interaction.client.users.fetch(ticketUserId).catch(() => null);
+
+ if (!ticketUser) return;
+
+ const attachmentPath = getTranscriptAttachmentFromResult(transcriptResult);
+ if (!attachmentPath) return;
+
+ try {
+ await ticketUser.send({
+ content: `Here is a copy of your ticket transcript from \`${interaction.guild.name}\`.`,
+ files: [attachmentPath],
+ });
+ } catch (error) {
+ console.error("Failed to DM transcript to user:", error);
+ }
+}
+
+async function collectCloseConfirmation(interaction, ticket, reason) {
+ const ticketUserId = getTicketUserId(ticket);
+
+ const lines = [
+ "Are you sure you want to close this ticket?",
+ "",
+ `**Ticket:** ${interaction.channel.name}`,
+ `**Reason:** ${reason || "No Reason Provided."}`,
+ ];
+
+ if (ticketUserId) {
+ lines.push(`**Ticket User:** <@${ticketUserId}>`);
+ }
+
+ if (supportbot.Ticket?.ReviewSystem?.Enabled === true) {
+ lines.push("");
+ lines.push("A review will be requested before the ticket is closed.");
+ }
+
+ const confirmContainer = buildContainer("Close Confirmation", lines);
+
+ const confirmRow = new Discord.ActionRowBuilder().addComponents(
+ new Discord.ButtonBuilder()
+ .setCustomId(`confirmClose:${interaction.id}`)
+ .setLabel("Confirm Close")
+ .setStyle(Discord.ButtonStyle.Danger),
+ new Discord.ButtonBuilder()
+ .setCustomId(`cancelClose:${interaction.id}`)
+ .setLabel("Cancel")
+ .setStyle(Discord.ButtonStyle.Secondary),
+ );
+
+ confirmContainer.addActionRowComponents(confirmRow);
+
+ const confirmMessage = await interaction.editReply({
+ flags: Discord.MessageFlags.IsComponentsV2,
+ components: [confirmContainer],
+ });
+
+ try {
+ const buttonInteraction = await confirmMessage.awaitMessageComponent({
+ componentType: Discord.ComponentType.Button,
+ time: 120000,
+ filter: (i) =>
+ (i.customId === `confirmClose:${interaction.id}` ||
+ i.customId === `cancelClose:${interaction.id}`) &&
+ i.user.id === interaction.user.id,
+ });
+
+ if (buttonInteraction.customId === `cancelClose:${interaction.id}`) {
+ await buttonInteraction
+ .update({
+ flags: Discord.MessageFlags.IsComponentsV2,
+ components: [
+ buildContainer("Close Cancelled", ["The ticket close process has been cancelled."]),
+ ],
+ })
+ .catch(() => {});
+
+ return false;
+ }
+
+ await buttonInteraction.deferUpdate().catch(() => {});
+ return true;
+ } catch {
+ await interaction
+ .editReply({
+ flags: Discord.MessageFlags.IsComponentsV2,
+ components: [buildContainer("Close Cancelled", ["The close confirmation timed out."])],
+ })
+ .catch(() => {});
+
+ return false;
+ }
+}
+
+async function postReview(interaction, ticket, reviewData) {
+ const { getChannel } = interaction.client;
+ const reviewChannel = await getChannel(supportbot.Ticket.ReviewSystem.Channel, interaction.guild);
+
+ if (!reviewChannel || !reviewData) return;
+
+ const reviewContainer = buildReviewSummaryContainer(
+ ticket,
+ reviewData.rating,
+ reviewData.comment,
+ );
+
+ await reviewChannel
+ .send({
+ flags: Discord.MessageFlags.IsComponentsV2,
+ components: [reviewContainer],
+ })
+ .catch(() => {});
+}
+
+async function handleCloseTicket(interaction, reason, ticket) {
+ try {
+ if (typeof db.updateTicketStatus === "function") {
+ db.updateTicketStatus(interaction.channel.id, "closed");
+ }
+
+ const transcriptResult = await TicketManager.createTranscript(interaction, ticket, reason);
+
+ await maybeDMTranscript(interaction, ticket, transcriptResult);
+
+ await interaction
+ .editReply({
+ flags: Discord.MessageFlags.IsComponentsV2,
+ components: [
+ buildContainer("Ticket Closed", [
+ `**Ticket:** ${interaction.channel.name}`,
+ `**Closed By:** <@${interaction.user.id}>`,
+ `**Reason:** ${reason || "No Reason Provided."}`,
+ ]),
+ ],
+ })
+ .catch(() => {});
+
+ await interaction.channel.delete().catch(() => {});
+ } catch (err) {
+ console.error("Error handling ticket close:", err);
+
+ if (interaction.deferred || interaction.replied) {
+ await interaction
+ .editReply({
+ content: "An error occurred while closing the ticket.",
+ components: [],
+ })
+ .catch(() => {});
+ return;
+ }
+
+ await interaction
+ .reply({
+ content: "An error occurred while closing the ticket.",
+ flags: Discord.MessageFlags.Ephemeral,
+ })
+ .catch(() => {});
+ }
+}
module.exports = new Command({
name: cmdconfig.CloseTicket.Command,
@@ -19,450 +405,89 @@ module.exports = new Command({
name: "reason",
description: "Ticket Close Reason",
type: Discord.ApplicationCommandOptionType.String,
- }
+ },
],
permissions: cmdconfig.CloseTicket.Permission,
async run(interaction) {
- const { getRole, getChannel } = interaction.client;
+ const { getRole } = interaction.client;
if (supportbot.Ticket.Close.StaffOnly) {
- let SupportStaff = await getRole(supportbot.Roles.StaffMember.Staff, interaction.guild);
- let Admin = await getRole(supportbot.Roles.StaffMember.Admin, interaction.guild);
+ const SupportStaff = await getRole(supportbot.Roles.StaffMember.Staff, interaction.guild);
+ const Admin = await getRole(supportbot.Roles.StaffMember.Admin, interaction.guild);
if (!SupportStaff || !Admin) {
- return interaction.reply("Some roles seem to be missing! Please check for errors when starting the bot.");
+ return interaction.reply({
+ content: "Some roles seem to be missing! Please check for errors when starting the bot.",
+ flags: Discord.MessageFlags.Ephemeral,
+ });
}
- const NoPerms = new Discord.EmbedBuilder()
- .setTitle("Invalid Permissions!")
- .setDescription(`${msgconfig.Error.IncorrectPerms}\n\nRole Required: \`${supportbot.Roles.StaffMember.Staff}\` or \`${supportbot.Roles.StaffMember.Admin}\``)
- .setColor(supportbot.Embed.Colours.Warn);
-
- if (!interaction.member.roles.cache.has(SupportStaff.id) && !interaction.member.roles.cache.has(Admin.id)) {
- return interaction.reply({ embeds: [NoPerms] });
+ if (
+ !interaction.member.roles.cache.has(SupportStaff.id) &&
+ !interaction.member.roles.cache.has(Admin.id)
+ ) {
+ return interaction.reply({
+ flags: Discord.MessageFlags.IsComponentsV2 | Discord.MessageFlags.Ephemeral,
+ components: [
+ buildContainer("Invalid Permissions!", [
+ msgconfig.Error.IncorrectPerms,
+ "",
+ `Role Required: \`${supportbot.Roles.StaffMember.Staff}\` or \`${supportbot.Roles.StaffMember.Admin}\``,
+ ]),
+ ],
+ });
}
}
const isThread = interaction.channel.type === Discord.ChannelType.PrivateThread;
+
if (
(supportbot.Ticket.TicketType === "threads" && !isThread) ||
- (supportbot.Ticket.TicketType === "channels" && interaction.channel.type !== Discord.ChannelType.GuildText)
+ (supportbot.Ticket.TicketType === "channels" &&
+ interaction.channel.type !== Discord.ChannelType.GuildText)
) {
- const NotTicketChannel = new Discord.EmbedBuilder()
- .setTitle("Invalid Channel!")
- .setDescription(`This command can only be used in a ${supportbot.Ticket.TicketType === "threads" ? "ticket thread" : "ticket channel"}.`)
- .setColor(supportbot.Embed.Colours.Warn);
-
- return interaction.reply({ embeds: [NotTicketChannel], ephemeral: true });
+ return interaction.reply({
+ flags: Discord.MessageFlags.IsComponentsV2 | Discord.MessageFlags.Ephemeral,
+ components: [
+ buildContainer("Invalid Channel!", [
+ `This command can only be used in a ${
+ supportbot.Ticket.TicketType === "threads" ? "ticket thread" : "ticket channel"
+ }.`,
+ ]),
+ ],
+ });
}
- await interaction.deferReply();
+ await interaction.deferReply({ flags: Discord.MessageFlags.Ephemeral });
const ticket = db.getTicket(interaction.channel.id);
-
+
if (!ticket) {
- const Exists = new Discord.EmbedBuilder()
- .setTitle("No Ticket Found!")
- .setDescription(msgconfig.Error.NoValidTicket)
- .setColor(supportbot.Embed.Colours.Warn);
- return interaction.followUp({ embeds: [Exists] });
+ return interaction.editReply({
+ flags: Discord.MessageFlags.IsComponentsV2,
+ components: [buildContainer("No Ticket Found!", [msgconfig.Error.NoValidTicket])],
+ });
}
- let reason = interaction.options?.getString("reason") || "No Reason Provided.";
- const ticketUserId = ticket.user;
+ const reason = interaction.options?.getString("reason") || "No Reason Provided.";
- if (supportbot.Ticket.ReviewSystem.Enabled) {
- const rating = await collectReviewRating(interaction, ticketUserId);
- const comment = await collectReviewComment(interaction, ticketUserId);
+ const confirmed = await collectCloseConfirmation(interaction, ticket, reason);
+ if (!confirmed) {
+ return;
+ }
- const reviewChannel = await getChannel(supportbot.Ticket.ReviewSystem.Channel, interaction.guild);
- const reviewer = supportbot.Ticket.ClaimTickets ? `<@${ticket.claimedBy}>` : null;
+ if (supportbot.Ticket?.ReviewSystem?.Enabled === true) {
+ const reviewData = await collectReview(interaction, ticket);
- const reviewEmbed = new Discord.EmbedBuilder()
- .setTitle(msgconfig.ReviewSystem.ReviewEmbed.Title)
- .addFields(
- {
- name: msgconfig.ReviewSystem.ReviewEmbed.RatingTitle,
- value: `${msgconfig.ReviewSystem.ReviewEmbed.ReviewEmoji.repeat(rating)}`,
- inline: false
- },
- {
- name: msgconfig.ReviewSystem.ReviewEmbed.CommentTitle,
- value: `\`\`\`${comment}\`\`\``,
- inline: false
- }
- )
- .setColor(msgconfig.ReviewSystem.ReviewEmbed.Color);
-
- if (supportbot.Ticket.ClaimTickets.Enabled) {
- reviewEmbed.setDescription(
- `**${msgconfig.ReviewSystem.ReviewEmbed.ReviewedStaffTitle}** ${reviewer}\n**${msgconfig.ReviewSystem.ReviewEmbed.ReviewedByTitle}** <@${ticketUserId}>` || `N/A`
- );
- } else {
- reviewEmbed.setDescription(
- `**${msgconfig.ReviewSystem.ReviewEmbed.ReviewedByTitle}** <@${ticketUserId}>`
- );
- }
-
- if (reviewChannel) {
- await reviewChannel.send({ embeds: [reviewEmbed] });
- }
-
- if (!interaction.replied && !interaction.deferred) {
- await interaction.followUp({ embeds: [reviewEmbed] });
+ if (reviewData) {
+ await postReview(interaction, ticket, reviewData);
}
await handleCloseTicket(interaction, reason, ticket);
- } else {
- await handleCloseTicket(interaction, reason, ticket);
+ return;
}
+
+ await handleCloseTicket(interaction, reason, ticket);
},
});
-
-async function collectReviewRating(interaction, ticketUserId) {
- const reviewPrompt = new Discord.EmbedBuilder()
- .setTitle(msgconfig.ReviewSystem.Rate.Title)
- .setDescription(`${msgconfig.ReviewSystem.Rate.Description}`)
- .setColor(supportbot.Embed.Colours.General);
-
- const stars = new Discord.ActionRowBuilder().addComponents(
- new Discord.StringSelectMenuBuilder()
- .setCustomId("starRating")
- .setPlaceholder("Select a rating")
- .addOptions([
- { label: msgconfig.ReviewSystem.Stars.One, value: "1" },
- { label: msgconfig.ReviewSystem.Stars.Two, value: "2" },
- { label: msgconfig.ReviewSystem.Stars.Three, value: "3" },
- { label: msgconfig.ReviewSystem.Stars.Four, value: "4" },
- { label: msgconfig.ReviewSystem.Stars.Five, value: "5" },
- ])
- );
-
- await interaction.followUp({
- content: `<@${ticketUserId}>`,
- embeds: [reviewPrompt],
- components: [stars],
- flags: Discord.MessageFlags.Ephemeral,
- });
-
- const starRating = await interaction.channel.awaitMessageComponent({
- componentType: Discord.ComponentType.StringSelect,
- filter: (i) => i.customId === "starRating" && i.user.id === ticketUserId,
- });
- await starRating.deferUpdate();
- return starRating.values[0];
-}
-
-async function collectReviewComment(interaction, ticketUserId) {
- const commentPrompt = new Discord.EmbedBuilder()
- .setTitle(msgconfig.ReviewSystem.Comment.Title)
- .setDescription(`${msgconfig.ReviewSystem.Comment.Description}`)
- .setColor(supportbot.Embed.Colours.General);
-
- await interaction.followUp({
- content: `<@${ticketUserId}>`,
- embeds: [commentPrompt],
- flags: Discord.MessageFlags.Ephemeral,
- });
-
- const filter = (response) => response.author.id === ticketUserId;
-
- const commentCollection = await interaction.channel.awaitMessages({ filter, max: 1 });
- const comment = commentCollection.first()?.content;
- return comment && comment.toLowerCase() !== "no" ? comment : "No Comment Provided";
-}
-async function handleCloseTicket(interaction, reason, ticket) {
- const { getChannel } = interaction.client;
-
- let tUser = interaction.client.users.cache.get(ticket.user_id);
- let transcriptChannel = await getChannel(supportbot.Ticket.Log.TicketDataLog, interaction.guild);
-
- if (!transcriptChannel) {
- console.log("Transcript channel missing or inaccessible");
- return interaction.followUp("Error: Transcript log channel is missing or bot lacks permission.");
- }
-
- try {
- let allMessages = [];
- let lastId = null;
-
- while (true) {
- const options = { limit: 100 };
- if (lastId) {
- options.before = lastId;
- }
-
- const messages = await interaction.channel.messages.fetch(options);
- if (messages.size === 0) break;
-
- allMessages = [...allMessages, ...messages.values()];
- lastId = messages.last().id;
-
- if (messages.size < 100) break;
- }
-
- allMessages.sort((a, b) => a.createdTimestamp - b.createdTimestamp);
-
- const transcriptData = allMessages.map(msg => ({
- content: msg.content || "No content",
- username: msg.author.username,
- userId: msg.author.id,
- avatar: msg.author.displayAvatarURL(),
- timestamp: msg.createdAt.toISOString(),
- attachments: Array.from(msg.attachments.values()).map(att => ({
- url: att.url,
- name: att.name
- })),
- embeds: msg.embeds.map(embed => ({
- title: embed.title,
- description: embed.description,
- fields: embed.fields
- }))
- }));
-
- db.updateTicketStatus(interaction.channel.id, "closed");
-
- const transcriptEmbed = new Discord.EmbedBuilder()
- .setTitle(msgconfig.TicketLog.Title)
- .setColor(msgconfig.TicketLog.Colour)
- .setFooter({ text: supportbot.Embed.Footer, iconURL: interaction.user.displayAvatarURL() })
- .setDescription(
- `> **Ticket:** ${interaction.channel.name} (\`${interaction.channel.id}\`)\n` +
- `> **User:** ${tUser?.tag || "Unknown User"} (\`${tUser?.id || ticket.user_id}\`)` +
- `> **Closed by:** <@${interaction.user.id}>\n` +
- `> **Message Count:** ${transcriptData.length}`
- )
- .addFields({ name: "Reason", value: `\`\`\`${reason}\`\`\``, inline: true });
-
- const html = createTranscriptHTML(
- {
- id: interaction.channel.id,
- messages: transcriptData,
- name: interaction.channel.name
- },
- reason
- );
-
- const transcriptDir = "./Data/Transcripts";
-
- if (!fs.existsSync(transcriptDir)) {
- fs.mkdirSync(transcriptDir, { recursive: true });
- }
-
- const fileName = `${interaction.channel.id}-transcript.html`;
- fs.writeFileSync(`./Data/Transcripts/${fileName}`, html);
-
- await transcriptChannel.send({
- embeds: [transcriptEmbed],
- files: [{
- attachment: `./Data/Transcripts/${fileName}`,
- name: `SPOILER_${fileName}`,
- spoiler: true
- }]
- });
-
- if (interaction.channel.type === Discord.ChannelType.GuildText ||
- interaction.channel.type === Discord.ChannelType.PrivateThread) {
- await interaction.channel.delete();
- }
- } catch (err) {
- console.error("Error handling ticket close:", err);
- interaction.followUp("An error occurred while closing the ticket.");
- }
-}
-
-function parseMarkdown(content) {
- content = content.replace(/\*\*(.*?)\*\*/g, '$1');
-
- content = content.replace(/_(.*?)_/g, '$1');
-
- content = content.replace(/`(.*?)`/g, '$1');
-
- content = content.replace(/\[(.*?)\]\((.*?)\)/g, '$1');
-
- content = content.replace(/```(.*?)```/gs, '
$1
');
-
- return content;
-}
-
-function createTranscriptHTML(ticket, reason) {
- return `
-
-
-
-
-
- Ticket Transcript
-
-
-
-
-
-
Ticket Transcript
-
-
Channel: ${ticket.name}
-
Ticket ID: ${ticket.id}
-
Message Count: ${ticket.messages.length}
-
Close Reason: ${reason}
-
-
-
- ${ticket.messages.map(msg => `
-
-
-
- ${parseMarkdown(msg.content)}
-
- ${msg.embeds.map(embed => `
-
- ${embed.title ? `
${embed.title}
` : ''}
- ${embed.description ? `
${embed.description}
` : ''}
- ${embed.fields.map(field => `
-
-
${field.name}:
-
${field.value}
-
- `).join('')}
-
- `).join('')}
-
- ${msg.attachments.map(att => `
-
📎 ${att.name}
- `).join('')}
-
-
- `).join('')}
-
-
-
-
- `;
-}
diff --git a/Commands/Tickets/forceaddUser.js b/Commands/Tickets/forceaddUser.js
deleted file mode 100644
index 3ce1e2b..0000000
--- a/Commands/Tickets/forceaddUser.js
+++ /dev/null
@@ -1,119 +0,0 @@
-const fs = require("fs");
-const {
- EmbedBuilder,
- ApplicationCommandOptionType,
- ApplicationCommandType,
- ChannelType,
- MessageFlags,
-} = require("discord.js");
-const yaml = require("js-yaml");
-
-const supportbot = yaml.load(fs.readFileSync("./Configs/supportbot.yml", "utf8"));
-const msgconfig = yaml.load(fs.readFileSync("./Configs/messages.yml", "utf8"));
-const cmdconfig = yaml.load(fs.readFileSync("./Configs/commands.yml", "utf8"));
-
-const Command = require("../../Structures/Command.js");
-const db = require("../../Structures/Database.js");
-
-module.exports = new Command({
- name: cmdconfig.ForceAddUser.Command,
- description: cmdconfig.ForceAddUser.Description,
- type: ApplicationCommandType.ChatInput,
- options: [
- {
- name: "user",
- description: "The user to forcefully add",
- type: ApplicationCommandOptionType.User,
- required: true,
- },
- ],
- permissions: cmdconfig.ForceAddUser.Permission,
-
- async run(interaction) {
- const userToAdd = interaction.options.getUser("user");
- const ticketChannel = interaction.channel;
-
- if (
- (supportbot.Ticket.TicketType === "threads" && !ticketChannel.isThread()) ||
- (supportbot.Ticket.TicketType === "channels" && ticketChannel.type !== ChannelType.GuildText)
- ) {
- const onlyInTicket = new EmbedBuilder()
- .setTitle(msgconfig.ForceAddUser.NotInTicket_Title)
- .setDescription(msgconfig.ForceAddUser.NotInTicket_Description)
- .setColor(supportbot.Embed.Colours.Error);
-
- return interaction.reply({
- embeds: [onlyInTicket],
- flags: MessageFlags.Ephemeral,
- });
- }
-
- const ticketRow = db.getTicket(ticketChannel.id);
- if (!ticketRow) {
- const notFound = new EmbedBuilder()
- .setTitle(msgconfig.ForceAddUser.NotInTicket_Title)
- .setDescription(msgconfig.ForceAddUser.NotInTicket_Description)
- .setColor(supportbot.Embed.Colours.Error);
-
- return interaction.reply({
- embeds: [notFound],
- flags: MessageFlags.Ephemeral,
- });
- }
-
- try {
- if (supportbot.Ticket.TicketType === "threads") {
- const botMember = await ticketChannel.members.fetch(interaction.client.user.id).catch(() => null);
- if (!botMember) await ticketChannel.join();
- await new Promise(res => setTimeout(res, 1000));
- await ticketChannel.members.add(userToAdd.id);
- } else if (supportbot.Ticket.TicketType === "channels") {
- await ticketChannel.permissionOverwrites.create(userToAdd.id, {
- ViewChannel: true,
- SendMessages: true,
- ReadMessageHistory: true,
- });
- }
-
- db.addUserToTicket(ticketChannel.id, userToAdd.id);
-
- const addedEmbed = new EmbedBuilder()
- .setTitle(msgconfig.ForceAddUser.Added_Title)
- .setDescription(
- msgconfig.ForceAddUser.Added_Description.replace("%username%", userToAdd.username)
- )
- .setColor(supportbot.Embed.Colours.Success);
-
- await interaction.reply({
- embeds: [addedEmbed],
- flags: MessageFlags.Ephemeral,
- });
-
- const addedToTicketEmbed = new EmbedBuilder()
- .setTitle(msgconfig.ForceAddUser.AddedToTicket_Title)
- .setDescription(
- msgconfig.ForceAddUser.AddedToTicket_Description.replace(
- "%channel_link%",
- `[${ticketChannel.name}](https://discord.com/channels/${ticketChannel.guild.id}/${ticketChannel.id})`
- )
- )
- .setColor(supportbot.Embed.Colours.General);
-
- await userToAdd.send({
- embeds: [addedToTicketEmbed],
- });
- } catch (err) {
- console.error("Error adding user to the ticket:", err);
-
- const errorEmbed = new EmbedBuilder()
- .setTitle(msgconfig.ForceAddUser.Error_Title)
- .setDescription(msgconfig.ForceAddUser.Error_Adding)
- .setColor(supportbot.Embed.Colours.Error);
-
- await interaction.reply({
- embeds: [errorEmbed],
- flags: MessageFlags.Ephemeral,
- });
- }
- },
-});
diff --git a/Commands/Tickets/manageticket.js b/Commands/Tickets/manageticket.js
new file mode 100644
index 0000000..6b04fd6
--- /dev/null
+++ b/Commands/Tickets/manageticket.js
@@ -0,0 +1,664 @@
+const fs = require("fs");
+const yaml = require("js-yaml");
+const {
+ EmbedBuilder,
+ ApplicationCommandOptionType,
+ ApplicationCommandType,
+ ChannelType,
+ MessageFlags,
+ ModalBuilder,
+ ActionRowBuilder,
+ TextInputBuilder,
+ TextInputStyle,
+} = require("discord.js");
+
+const supportbot = yaml.load(fs.readFileSync("./Configs/supportbot.yml", "utf8"));
+const msgconfig = yaml.load(fs.readFileSync("./Configs/messages.yml", "utf8"));
+const cmdconfig = yaml.load(fs.readFileSync("./Configs/commands.yml", "utf8"));
+
+const TicketManager = require("../../Structures/TicketManager.js");
+const Command = require("../../Structures/Command.js");
+const db = require("../../Structures/Database.js");
+
+function getDepartments() {
+ return supportbot.Ticket?.DepartmentSystem?.Departments || {};
+}
+
+function getPriorities() {
+ return supportbot.Ticket?.PrioritySystem?.Priorities || {};
+}
+
+function prioritiesEnabled() {
+ return (
+ supportbot.Ticket?.TicketType !== "threads" &&
+ supportbot.Ticket?.PrioritySystem?.Enabled === true
+ );
+}
+
+function departmentsEnabled() {
+ return (
+ supportbot.Ticket?.TicketType !== "threads" &&
+ supportbot.Ticket?.DepartmentSystem?.Enabled === true
+ );
+}
+
+function buildTicketChannelName(ticketNumberID, priorityKey, departmentConfig) {
+ const defaultPrefix = supportbot.Ticket?.ChannelPrefix || supportbot.Ticket?.Channel || "ticket-";
+
+ const departmentPrefix = departmentConfig?.ChannelPrefix || defaultPrefix;
+
+ const safePrefix = String(departmentPrefix).trim() || "ticket-";
+ const baseName = `${safePrefix}${ticketNumberID}`;
+
+ if (!prioritiesEnabled() || !priorityKey) {
+ return baseName;
+ }
+
+ return `${baseName}-${String(priorityKey).toLowerCase()}`;
+}
+
+module.exports = new Command({
+ name: cmdconfig.TicketManage?.Command || "ticketmanage",
+ description: cmdconfig.TicketManage?.Description || "Manage tickets",
+ type: ApplicationCommandType.ChatInput,
+ permissions: cmdconfig.TicketManage?.Permission || ["ManageChannels"],
+
+ options: [
+ {
+ name: "transfer",
+ description: "Transfer a ticket to another department",
+ type: ApplicationCommandOptionType.Subcommand,
+ options: [
+ {
+ name: "department",
+ description: "Department to transfer this ticket to",
+ type: ApplicationCommandOptionType.String,
+ required: true,
+ choices: Object.entries(getDepartments())
+ .slice(0, 25)
+ .map(([key, dept]) => ({
+ name: dept.Name || key,
+ value: key,
+ })),
+ },
+ {
+ name: "priority",
+ description: "Optional new priority",
+ type: ApplicationCommandOptionType.String,
+ required: false,
+ choices: Object.entries(getPriorities())
+ .slice(0, 25)
+ .map(([key, priority]) => ({
+ name: priority.Name || key,
+ value: key,
+ })),
+ },
+ ],
+ },
+ {
+ name: "priority",
+ description: "Change a ticket priority",
+ type: ApplicationCommandOptionType.Subcommand,
+ options: [
+ {
+ name: "level",
+ description: "New priority level",
+ type: ApplicationCommandOptionType.String,
+ required: true,
+ choices: Object.entries(getPriorities())
+ .slice(0, 25)
+ .map(([key, priority]) => ({
+ name: priority.Name || key,
+ value: key,
+ })),
+ },
+ ],
+ },
+ {
+ name: cmdconfig.TicketManage?.ForceAdd?.Command || "forceadd",
+ description:
+ cmdconfig.TicketManage?.ForceAdd?.Description || "Force add a user to the current ticket",
+ type: ApplicationCommandOptionType.Subcommand,
+ options: [
+ {
+ name: "user",
+ description: "The user to add",
+ type: ApplicationCommandOptionType.User,
+ required: true,
+ },
+ ],
+ },
+ {
+ name: cmdconfig.TicketManage?.ForceRemove?.Command || "forceremove",
+ description:
+ cmdconfig.TicketManage?.ForceRemove?.Description || "Remove a user from the current ticket",
+ type: ApplicationCommandOptionType.Subcommand,
+ options: [
+ {
+ name: "user",
+ description: "The user to remove",
+ type: ApplicationCommandOptionType.User,
+ required: true,
+ },
+ ],
+ },
+ {
+ name: cmdconfig.TicketManage?.Rename?.Command || "rename",
+ description: cmdconfig.TicketManage?.Rename?.Description || "Rename the current ticket",
+ type: ApplicationCommandOptionType.Subcommand,
+ options: [],
+ },
+ {
+ name: cmdconfig.TicketManage?.Close?.Command || "close",
+ description: cmdconfig.TicketManage?.Close?.Description || "Close the current ticket",
+ type: ApplicationCommandOptionType.Subcommand,
+ options: [
+ {
+ name: "reason",
+ description: "Reason for closing the ticket",
+ type: ApplicationCommandOptionType.String,
+ required: false,
+ },
+ ],
+ },
+ ],
+
+ async run(interaction) {
+ try {
+ const subcommand = interaction.options.getSubcommand(false);
+ const ticket = db.getTicket(interaction.channel.id);
+
+ if (!ticket) {
+ return interaction.reply({
+ content: "This channel is not a ticket.",
+ flags: MessageFlags.Ephemeral,
+ });
+ }
+
+ const { getRole } = interaction.client;
+
+ let SupportStaff = await getRole(supportbot.Roles.StaffMember.Staff, interaction.guild);
+ let Admin = await getRole(supportbot.Roles.StaffMember.Admin, interaction.guild);
+ let Moderator = await getRole(supportbot.Roles.StaffMember.Moderator, interaction.guild);
+
+ if (!SupportStaff || !Admin || !Moderator) {
+ const missingRolesEmbed = new EmbedBuilder()
+ .setDescription(msgconfig.Error.InvalidChannel || "Required roles are missing!")
+ .setColor(supportbot.Embed.Colours.Warn);
+
+ return interaction.reply({
+ embeds: [missingRolesEmbed],
+ flags: MessageFlags.Ephemeral,
+ });
+ }
+
+ const NoPerms = new EmbedBuilder()
+ .setDescription(
+ msgconfig.Error.IncorrectPerms || "You do not have the correct permissions!",
+ )
+ .setColor(supportbot.Embed.Colours.Warn);
+
+ if (
+ !interaction.member.roles.cache.has(Admin.id) &&
+ !interaction.member.roles.cache.has(Moderator.id) &&
+ (!supportbot.Mod.AllowSupportStaff || !interaction.member.roles.cache.has(SupportStaff.id))
+ ) {
+ return interaction.reply({
+ embeds: [NoPerms],
+ flags: MessageFlags.Ephemeral,
+ });
+ }
+
+ if (subcommand === "transfer") {
+ if (!departmentsEnabled()) {
+ return interaction.reply({
+ content: "Departments are disabled, or ticket mode is set to threads.",
+ flags: MessageFlags.Ephemeral,
+ });
+ }
+
+ if (interaction.channel.type !== ChannelType.GuildText) {
+ return interaction.reply({
+ content: "Ticket transfer only works for ticket channels.",
+ flags: MessageFlags.Ephemeral,
+ });
+ }
+
+ const departmentKey = interaction.options.getString("department");
+ const requestedPriority = interaction.options.getString("priority");
+
+ const departments = getDepartments();
+ const departmentConfig = departments[departmentKey];
+
+ if (!departmentConfig) {
+ return interaction.reply({
+ content: "That department does not exist.",
+ flags: MessageFlags.Ephemeral,
+ });
+ }
+
+ const targetCategoryId = departmentConfig.Category;
+ const targetRoleId = departmentConfig.Role || null;
+
+ if (!targetCategoryId) {
+ return interaction.reply({
+ content: "That department does not have a category configured.",
+ flags: MessageFlags.Ephemeral,
+ });
+ }
+
+ const targetCategory = interaction.guild.channels.cache.get(targetCategoryId);
+ if (!targetCategory) {
+ return interaction.reply({
+ content: "The target department category could not be found.",
+ flags: MessageFlags.Ephemeral,
+ });
+ }
+
+ let finalPriority = ticket.priority || null;
+
+ if (requestedPriority) {
+ finalPriority = requestedPriority;
+ } else if (!finalPriority && departmentConfig.DefaultPriority) {
+ finalPriority = departmentConfig.DefaultPriority;
+ }
+
+ const oldDepartmentKey = ticket.department || null;
+ const oldDepartmentConfig = oldDepartmentKey ? departments[oldDepartmentKey] : null;
+ const oldRoleId = oldDepartmentConfig?.Role || null;
+
+ await interaction.channel.setParent(targetCategory.id);
+
+ if (oldRoleId && oldRoleId !== targetRoleId) {
+ await interaction.channel.permissionOverwrites.delete(oldRoleId).catch(() => {});
+ }
+
+ if (targetRoleId) {
+ await interaction.channel.permissionOverwrites.create(targetRoleId, {
+ ViewChannel: true,
+ SendMessages: true,
+ ReadMessageHistory: true,
+ });
+ }
+
+ if (typeof db.updateTicketDepartment === "function") {
+ db.updateTicketDepartment(interaction.channel.id, departmentKey);
+ }
+
+ if (finalPriority && typeof db.updateTicketPriority === "function") {
+ db.updateTicketPriority(interaction.channel.id, finalPriority);
+ }
+
+ const ticketNumber = ticket.number || interaction.channel.name.replace(/\D/g, "");
+
+ const newChannelName = buildTicketChannelName(
+ ticketNumber,
+ finalPriority,
+ departmentConfig,
+ );
+
+ if (interaction.channel.name !== newChannelName) {
+ await interaction.channel.setName(newChannelName).catch(() => {});
+ }
+
+ const deptEmoji = departmentConfig.Emoji || "🎫";
+ const deptName = departmentConfig.Name || departmentKey;
+
+ let description = `> **Department:** ${deptEmoji} ${deptName}`;
+
+ if (finalPriority && prioritiesEnabled()) {
+ const priorityConfig = getPriorities()[finalPriority];
+ const priorityEmoji = priorityConfig?.Emoji || "🟡";
+ const priorityName = priorityConfig?.Name || finalPriority;
+ description += `\n> **Priority:** ${priorityEmoji} ${priorityName}`;
+ }
+
+ description += `\n> **Transferred By:** <@${interaction.user.id}>`;
+
+ const embed = new EmbedBuilder()
+ .setTitle("Ticket Updated")
+ .setDescription(description)
+ .setColor(supportbot.Embed.Colours.Success);
+
+ return interaction.reply({
+ embeds: [embed],
+ });
+ }
+
+ if (subcommand === "priority") {
+ if (!prioritiesEnabled()) {
+ return interaction.reply({
+ content: "Priority is disabled, or ticket mode is set to threads.",
+ flags: MessageFlags.Ephemeral,
+ });
+ }
+
+ const level = interaction.options.getString("level");
+ const priorityConfig = getPriorities()[level];
+
+ if (!priorityConfig) {
+ return interaction.reply({
+ content: "That priority does not exist.",
+ flags: MessageFlags.Ephemeral,
+ });
+ }
+
+ if (typeof db.updateTicketPriority === "function") {
+ db.updateTicketPriority(interaction.channel.id, level);
+ }
+
+ const departments = getDepartments();
+ const departmentConfig = ticket.department ? departments[ticket.department] : null;
+ const ticketNumber = ticket.number || interaction.channel.name.replace(/\D/g, "");
+
+ const newChannelName = buildTicketChannelName(ticketNumber, level, departmentConfig);
+
+ if (interaction.channel.name !== newChannelName) {
+ await interaction.channel.setName(newChannelName).catch(() => {});
+ }
+
+ const priorityEmoji = priorityConfig.Emoji || "🟡";
+ const priorityName = priorityConfig.Name || level;
+
+ const embed = new EmbedBuilder()
+ .setTitle("Ticket Updated")
+ .setDescription(
+ `> **Priority:** ${priorityEmoji} ${priorityName}\n> **Updated By:** <@${interaction.user.id}>`,
+ )
+ .setColor(supportbot.Embed.Colours.Success);
+
+ return interaction.reply({
+ embeds: [embed],
+ });
+ }
+
+ if (subcommand === (cmdconfig.TicketManage?.ForceAdd?.Command || "forceadd")) {
+ const userToAdd = interaction.options.getUser("user");
+ const ticketChannel = interaction.channel;
+
+ if (
+ (supportbot.Ticket.TicketType === "threads" && !ticketChannel.isThread()) ||
+ (supportbot.Ticket.TicketType === "channels" &&
+ ticketChannel.type !== ChannelType.GuildText)
+ ) {
+ const onlyInTicket = new EmbedBuilder()
+ .setTitle(msgconfig.ForceAddUser.NotInTicket_Title)
+ .setDescription(msgconfig.ForceAddUser.NotInTicket_Description)
+ .setColor(supportbot.Embed.Colours.Error);
+
+ return interaction.reply({
+ embeds: [onlyInTicket],
+ flags: MessageFlags.Ephemeral,
+ });
+ }
+
+ const ticketRow = db.getTicket(ticketChannel.id);
+ if (!ticketRow) {
+ const notFound = new EmbedBuilder()
+ .setTitle(msgconfig.ForceAddUser.NotInTicket_Title)
+ .setDescription(msgconfig.ForceAddUser.NotInTicket_Description)
+ .setColor(supportbot.Embed.Colours.Error);
+
+ return interaction.reply({
+ embeds: [notFound],
+ flags: MessageFlags.Ephemeral,
+ });
+ }
+
+ try {
+ if (supportbot.Ticket.TicketType === "threads") {
+ const botMember = await ticketChannel.members
+ .fetch(interaction.client.user.id)
+ .catch(() => null);
+
+ if (!botMember) await ticketChannel.join();
+
+ await new Promise((res) => setTimeout(res, 1000));
+ await ticketChannel.members.add(userToAdd.id);
+ } else {
+ await ticketChannel.permissionOverwrites.create(userToAdd.id, {
+ ViewChannel: true,
+ SendMessages: true,
+ ReadMessageHistory: true,
+ });
+ }
+
+ db.addUserToTicket(ticketChannel.id, userToAdd.id);
+
+ const addedEmbed = new EmbedBuilder()
+ .setTitle(msgconfig.ForceAddUser.Added_Title)
+ .setDescription(
+ msgconfig.ForceAddUser.Added_Description.replace("%username%", userToAdd.username),
+ )
+ .setColor(supportbot.Embed.Colours.Success);
+
+ await interaction.reply({
+ embeds: [addedEmbed],
+ flags: MessageFlags.Ephemeral,
+ });
+
+ if (supportbot.Ticket?.UserManagement?.DMOnAdd !== false) {
+ const addedToTicketEmbed = new EmbedBuilder()
+ .setTitle(msgconfig.ForceAddUser.AddedToTicket_Title)
+ .setDescription(
+ msgconfig.ForceAddUser.AddedToTicket_Description.replace(
+ "%channel_link%",
+ `[${ticketChannel.name}](https://discord.com/channels/${ticketChannel.guild.id}/${ticketChannel.id})`,
+ ),
+ )
+ .setColor(supportbot.Embed.Colours.General);
+
+ await userToAdd.send({ embeds: [addedToTicketEmbed] }).catch(() => {});
+ }
+ } catch (err) {
+ console.error("Error adding user to the ticket:", err);
+
+ const errorEmbed = new EmbedBuilder()
+ .setTitle(msgconfig.ForceAddUser.Error_Title)
+ .setDescription(msgconfig.ForceAddUser.Error_Adding)
+ .setColor(supportbot.Embed.Colours.Error);
+
+ await interaction.reply({
+ embeds: [errorEmbed],
+ flags: MessageFlags.Ephemeral,
+ });
+ }
+
+ return;
+ }
+
+ if (subcommand === (cmdconfig.TicketManage?.ForceRemove?.Command || "forceremove")) {
+ const userToRemove = interaction.options.getUser("user");
+ const ticketChannel = interaction.channel;
+
+ if (
+ (supportbot.Ticket.TicketType === "threads" && !ticketChannel.isThread()) ||
+ (supportbot.Ticket.TicketType === "channels" &&
+ ticketChannel.type !== ChannelType.GuildText)
+ ) {
+ const onlyInTicket = new EmbedBuilder()
+ .setTitle(msgconfig.RemoveUser.NotInTicket_Title)
+ .setDescription(msgconfig.RemoveUser.NotInTicket_Description)
+ .setColor(supportbot.Embed.Colours.Error);
+
+ return interaction.reply({
+ embeds: [onlyInTicket],
+ flags: MessageFlags.Ephemeral,
+ });
+ }
+
+ try {
+ if (supportbot.Ticket.TicketType === "threads") {
+ await ticketChannel.members.remove(userToRemove.id);
+ } else {
+ await ticketChannel.permissionOverwrites.delete(userToRemove.id);
+ }
+
+ db.removeUserFromTicket(ticketChannel.id, userToRemove.id);
+
+ const removedEmbed = new EmbedBuilder()
+ .setTitle(msgconfig.RemoveUser.Removed_Title)
+ .setDescription(msgconfig.RemoveUser.Removed_Message)
+ .setColor(supportbot.Embed.Colours.Success);
+
+ await interaction.reply({
+ embeds: [removedEmbed],
+ flags: MessageFlags.Ephemeral,
+ });
+
+ if (supportbot.Ticket?.UserManagement?.DMOnRemove !== false) {
+ const removedFromTicketEmbed = new EmbedBuilder()
+ .setTitle(msgconfig.RemoveUser.RemovedFromTicket_Title)
+ .setDescription(
+ msgconfig.RemoveUser.RemovedFromTicket_Description.replace(
+ "%channel_link%",
+ `[${ticketChannel.name}](https://discord.com/channels/${ticketChannel.guild.id}/${ticketChannel.id})`,
+ ),
+ )
+ .setColor(supportbot.Embed.Colours.General);
+
+ await userToRemove.send({ embeds: [removedFromTicketEmbed] }).catch(() => {});
+ }
+ } catch (err) {
+ console.error("Error removing user from the ticket:", err);
+
+ const errorEmbed = new EmbedBuilder()
+ .setTitle(msgconfig.RemoveUser.Error_Title)
+ .setDescription(msgconfig.RemoveUser.Error_Removing)
+ .setColor(supportbot.Embed.Colours.Error);
+
+ await interaction.reply({
+ embeds: [errorEmbed],
+ flags: MessageFlags.Ephemeral,
+ });
+ }
+
+ return;
+ }
+
+ if (subcommand === (cmdconfig.TicketManage?.Rename?.Command || "rename")) {
+ const modal = new ModalBuilder()
+ .setCustomId("renameTicketModal")
+ .setTitle("Rename Channel")
+ .addComponents(
+ new ActionRowBuilder().addComponents(
+ new TextInputBuilder()
+ .setCustomId("renameTicket")
+ .setLabel("New Channel Name")
+ .setStyle(TextInputStyle.Short)
+ .setRequired(true),
+ ),
+ );
+
+ return interaction.showModal(modal);
+ }
+
+ if (subcommand === (cmdconfig.TicketManage?.Close?.Command || "close")) {
+ const reason = interaction.options.getString("reason") || "Closed by staff.";
+
+ if (supportbot.Ticket.Close.StaffOnly) {
+ let CloseSupportStaff = await getRole(
+ supportbot.Roles.StaffMember.Staff,
+ interaction.guild,
+ );
+ let CloseAdmin = await getRole(supportbot.Roles.StaffMember.Admin, interaction.guild);
+
+ if (!CloseSupportStaff || !CloseAdmin) {
+ return interaction.reply({
+ content:
+ "Some roles seem to be missing! Please check for errors when starting the bot.",
+ flags: MessageFlags.Ephemeral,
+ });
+ }
+
+ const noClosePerms = new EmbedBuilder()
+ .setTitle("Invalid Permissions!")
+ .setDescription(
+ `${msgconfig.Error.IncorrectPerms}\n\nRole Required: \`${supportbot.Roles.StaffMember.Staff}\` or \`${supportbot.Roles.StaffMember.Admin}\``,
+ )
+ .setColor(supportbot.Embed.Colours.Warn);
+
+ if (
+ !interaction.member.roles.cache.has(CloseSupportStaff.id) &&
+ !interaction.member.roles.cache.has(CloseAdmin.id)
+ ) {
+ return interaction.reply({
+ embeds: [noClosePerms],
+ flags: MessageFlags.Ephemeral,
+ });
+ }
+ }
+
+ const isThread = interaction.channel.type === ChannelType.PrivateThread;
+
+ if (
+ (supportbot.Ticket.TicketType === "threads" && !isThread) ||
+ (supportbot.Ticket.TicketType === "channels" &&
+ interaction.channel.type !== ChannelType.GuildText)
+ ) {
+ const notTicketChannel = new EmbedBuilder()
+ .setTitle("Invalid Channel!")
+ .setDescription(
+ `This command can only be used in a ${supportbot.Ticket.TicketType === "threads" ? "ticket thread" : "ticket channel"}.`,
+ )
+ .setColor(supportbot.Embed.Colours.Warn);
+
+ return interaction.reply({
+ embeds: [notTicketChannel],
+ flags: MessageFlags.Ephemeral,
+ });
+ }
+
+ await interaction.deferReply({ flags: MessageFlags.Ephemeral });
+
+ const ticketData = db.getTicket(interaction.channel.id);
+
+ if (!ticketData) {
+ const exists = new EmbedBuilder()
+ .setTitle("No Ticket Found!")
+ .setDescription(msgconfig.Error.NoValidTicket)
+ .setColor(supportbot.Embed.Colours.Warn);
+
+ return interaction.followUp({
+ embeds: [exists],
+ flags: MessageFlags.Ephemeral,
+ });
+ }
+
+ try {
+ await TicketManager.createTranscript(interaction, ticketData, reason);
+
+ await interaction.followUp({
+ content: "Ticket closed successfully.",
+ flags: MessageFlags.Ephemeral,
+ });
+
+ await interaction.channel.delete().catch(() => {});
+ } catch (err) {
+ console.error("Error closing ticket:", err);
+
+ await interaction.followUp({
+ content: "An error occurred while closing the ticket.",
+ flags: MessageFlags.Ephemeral,
+ });
+ }
+
+ return;
+ }
+
+ return interaction.reply({
+ content: "Unknown ticket management action.",
+ flags: MessageFlags.Ephemeral,
+ });
+ } catch (error) {
+ console.error("Error in ticket manage command:", error);
+
+ if (!interaction.replied && !interaction.deferred) {
+ return interaction.reply({
+ content: "An error occurred while managing the ticket.",
+ flags: MessageFlags.Ephemeral,
+ });
+ }
+ }
+ },
+});
diff --git a/Commands/Tickets/removeUser.js b/Commands/Tickets/removeUser.js
deleted file mode 100644
index 9241480..0000000
--- a/Commands/Tickets/removeUser.js
+++ /dev/null
@@ -1,108 +0,0 @@
-// SupportBot | Emerald Services
-// Remove user from a ticket
-
-const {
- EmbedBuilder,
- ApplicationCommandOptionType,
- ApplicationCommandType,
- ChannelType,
- MessageFlags,
-} = require("discord.js");
-const yaml = require("js-yaml");
-const fs = require("fs");
-
-const supportbot = yaml.load(fs.readFileSync("./Configs/supportbot.yml", "utf8"));
-const msgconfig = yaml.load(fs.readFileSync("./Configs/messages.yml", "utf8"));
-const cmdconfig = yaml.load(fs.readFileSync("./Configs/commands.yml", "utf8"));
-
-const Command = require("../../Structures/Command.js");
-const db = require("../../Structures/Database.js");
-
-module.exports = new Command({
- name: cmdconfig.RemoveUser.Command,
- description: cmdconfig.RemoveUser.Description,
- type: ApplicationCommandType.ChatInput,
- options: [
- {
- name: "user",
- description: "The user to remove",
- type: ApplicationCommandOptionType.User,
- required: true,
- },
- ],
- permissions: cmdconfig.RemoveUser.Permission,
-
- async run(interaction) {
- const { getRole } = interaction.client;
- const SupportStaff = await getRole(supportbot.Roles.StaffMember.Staff, interaction.guild);
- const Admin = await getRole(supportbot.Roles.StaffMember.Admin, interaction.guild);
-
- if (!SupportStaff || !Admin) {
- return interaction.reply("Missing staff/admin roles in config.");
- }
- if (
- !interaction.member.roles.cache.has(SupportStaff.id) &&
- !interaction.member.roles.cache.has(Admin.id)
- ) {
- const NoPerms = new EmbedBuilder()
- .setTitle("Invalid Permissions!")
- .setDescription(
- `${msgconfig.Error.IncorrectPerms}\n\nRole Required: \`${supportbot.Roles.StaffMember.Staff}\` or \`${supportbot.Roles.StaffMember.Admin}\``
- )
- .setColor(supportbot.Embed.Colours.Warn);
- return interaction.reply({ embeds: [NoPerms], flags: MessageFlags.Ephemeral });
- }
-
- const userToRemove = interaction.options.getUser("user");
- const ticketChannel = interaction.channel;
-
- if (
- (supportbot.Ticket.TicketType === "threads" && !ticketChannel.isThread()) ||
- (supportbot.Ticket.TicketType === "channels" && ticketChannel.type !== ChannelType.GuildText)
- ) {
- const onlyInTicket = new EmbedBuilder()
- .setTitle(msgconfig.RemoveUser.NotInTicket_Title)
- .setDescription(msgconfig.RemoveUser.NotInTicket_Description)
- .setColor(supportbot.Embed.Colours.Error);
- return interaction.reply({ embeds: [onlyInTicket], flags: MessageFlags.Ephemeral });
- }
-
- try {
- if (supportbot.Ticket.TicketType === "threads") {
- await ticketChannel.members.remove(userToRemove.id);
- } else {
- await ticketChannel.permissionOverwrites.delete(userToRemove.id);
- }
-
- db.removeUserFromTicket(ticketChannel.id, userToRemove.id);
-
- const removedEmbed = new EmbedBuilder()
- .setTitle(msgconfig.RemoveUser.Removed_Title)
- .setDescription(msgconfig.RemoveUser.Removed_Message)
- .setColor(supportbot.Embed.Colours.Success);
-
- await interaction.reply({ embeds: [removedEmbed], flags: MessageFlags.Ephemeral });
-
- const removedFromTicketEmbed = new EmbedBuilder()
- .setTitle(msgconfig.RemoveUser.RemovedFromTicket_Title)
- .setDescription(
- msgconfig.RemoveUser.RemovedFromTicket_Description.replace(
- "%channel_link%",
- `[${ticketChannel.name}](https://discord.com/channels/${ticketChannel.guild.id}/${ticketChannel.id})`
- )
- )
- .setColor(supportbot.Embed.Colours.General);
-
- await userToRemove.send({ embeds: [removedFromTicketEmbed] }).catch(() => {});
- } catch (err) {
- console.error("Error removing user from the ticket:", err);
-
- const errorEmbed = new EmbedBuilder()
- .setTitle(msgconfig.RemoveUser.Error_Title)
- .setDescription(msgconfig.RemoveUser.Error_Removing)
- .setColor(supportbot.Embed.Colours.Error);
-
- await interaction.reply({ embeds: [errorEmbed], flags: MessageFlags.Ephemeral });
- }
- },
-});
diff --git a/Commands/Tickets/renameTicket.js b/Commands/Tickets/renameTicket.js
deleted file mode 100644
index 52dbb16..0000000
--- a/Commands/Tickets/renameTicket.js
+++ /dev/null
@@ -1,64 +0,0 @@
-// SupportBot | Emerald Services
-// Info Command
-
-const fs = require("fs");
-
-const {
- EmbedBuilder,
- ApplicationCommandOptionType,
- ApplicationCommandType,
- ChannelType,
- ActionRowBuilder,
- ButtonBuilder,
- StringSelectMenuBuilder,
- StringSelectMenuOptionBuilder,
- ButtonStyle,
- MessageCollector,
- MessageFlags,
- ModalBuilder,
- TextInputBuilder,
- TextInputStyle,
- SeparatorBuilder,
- ContainerBuilder,
- TextDisplayBuilder
-} = require("discord.js");
-
-const yaml = require("js-yaml");
-
-const supportbot = yaml.load(
- fs.readFileSync("./Configs/supportbot.yml", "utf8")
-);
-
-const cmdconfig = yaml.load(
- fs.readFileSync("./Configs/commands.yml", "utf8")
-);
-
-const msgconfig = yaml.load(
- fs.readFileSync("./Configs/messages.yml", "utf8")
-);
-
-const Command = require("../../Structures/Command.js");
-
-module.exports = new Command({
- name: cmdconfig.TicketRename.Command,
- description: cmdconfig.TicketRename.Description,
- options: [],
- permissions: cmdconfig.TicketRename.Permission,
-
- async run(interaction) {
- const modal = new ModalBuilder()
- .setCustomId("renameTicketModal")
- .setTitle("Rename Channel")
- .addComponents(
- new ActionRowBuilder().addComponents(
- new TextInputBuilder()
- .setCustomId("renameTicket")
- .setLabel("New Channel Name")
- .setStyle(TextInputStyle.Short)
- .setRequired(true)
- )
- );
-
- return interaction.showModal(modal);
- },
-});
diff --git a/Commands/Tickets/ticket.js b/Commands/Tickets/ticket.js
index e070a3e..00fa9b2 100644
--- a/Commands/Tickets/ticket.js
+++ b/Commands/Tickets/ticket.js
@@ -1,3 +1,6 @@
+// SupportBot | Emerald Services
+// Ticket Command
+
const fs = require("fs");
const {
EmbedBuilder,
@@ -13,7 +16,7 @@ const {
MessageFlags,
SeparatorBuilder,
ContainerBuilder,
- TextDisplayBuilder
+ TextDisplayBuilder,
} = require("discord.js");
const yaml = require("js-yaml");
@@ -33,29 +36,121 @@ async function isBlacklisted(userId) {
blacklistData = JSON.parse(fs.readFileSync("./Data/BlacklistedUsers.json", "utf8"));
} catch (error) {
console.error("Error reading BlacklistedUsers.json:", error);
- return false;
+ return false;
}
-
+
if (!blacklistData || !Array.isArray(blacklistData.blacklistedUsers)) {
console.error("blacklistedUsers is not defined or is not an array");
return false;
}
-
+
return blacklistData.blacklistedUsers.includes(userId);
}
async function getClockedInUsers(guild) {
const clockedInUsers = new Set();
- const rows = guild.members.cache.map(m => m.id);
+
+ if (typeof db.getProfile !== "function") {
+ console.error("db.getProfile is not a function. Check Structures/Database.js");
+ return clockedInUsers;
+ }
+
+ const rows = guild.members.cache.map((m) => m.id);
+
for (const id of rows) {
- const profile = db.getProfile(id);
- if (profile && profile.clockedIn) {
- clockedInUsers.add(id);
+ try {
+ const profile = db.getProfile(id);
+ if (profile && profile.clockedIn) {
+ clockedInUsers.add(id);
+ }
+ } catch (error) {
+ console.error(`Error getting profile for ${id}:`, error);
}
}
+
return clockedInUsers;
}
+function getDepartmentSystem() {
+ return supportbot.Ticket?.DepartmentSystem || {};
+}
+
+function getPrioritySystem() {
+ return supportbot.Ticket?.PrioritySystem || {};
+}
+
+function areDepartmentsEnabled() {
+ return (
+ supportbot.Ticket?.TicketType !== "threads" &&
+ supportbot.Ticket?.DepartmentSystem?.Enabled === true
+ );
+}
+
+function arePrioritiesEnabled() {
+ return (
+ supportbot.Ticket?.TicketType !== "threads" &&
+ supportbot.Ticket?.PrioritySystem?.Enabled === true
+ );
+}
+
+function getPriorityKey(interaction, departmentKey) {
+ if (!arePrioritiesEnabled()) {
+ return null;
+ }
+
+ const prioritySystem = getPrioritySystem();
+ const departmentConfig = getDepartmentConfig(departmentKey);
+
+ return (
+ interaction.options?.getString?.("priority") ||
+ interaction.priority ||
+ departmentConfig?.DefaultPriority ||
+ prioritySystem.DefaultPriority ||
+ "medium"
+ );
+}
+
+function getDepartmentConfig(departmentKey) {
+ if (!departmentKey) return null;
+ return getDepartmentSystem().Departments?.[departmentKey] || null;
+}
+
+function getPriorityKey(interaction, departmentKey) {
+ if (!arePrioritiesEnabled()) {
+ return null;
+ }
+
+ const prioritySystem = getPrioritySystem();
+ const departmentConfig = getDepartmentConfig(departmentKey);
+
+ return (
+ interaction.priority ||
+ departmentConfig?.DefaultPriority ||
+ prioritySystem.DefaultPriority ||
+ "medium"
+ );
+}
+
+function getPriorityConfig(priorityKey) {
+ if (!priorityKey) return null;
+ return getPrioritySystem().Priorities?.[priorityKey] || null;
+}
+
+function buildTicketChannelName(ticketNumberID, priorityKey, departmentConfig) {
+ const defaultPrefix = supportbot.Ticket?.ChannelPrefix || supportbot.Ticket?.Channel || "ticket-";
+
+ const departmentPrefix = departmentConfig?.ChannelPrefix || defaultPrefix;
+
+ const safePrefix = String(departmentPrefix).trim() || "ticket-";
+ const baseName = `${safePrefix}${ticketNumberID}`;
+
+ if (!arePrioritiesEnabled() || !priorityKey) {
+ return baseName;
+ }
+
+ return `${baseName}-${String(priorityKey).toLowerCase()}`;
+}
+
async function askTicketQuestions(ticketChannel, interaction, questions) {
const answers = [];
let questionIndex = 0;
@@ -66,7 +161,9 @@ async function askTicketQuestions(ticketChannel, interaction, questions) {
.setDescription(`**${questionIndex + 1}.** ${questions[questionIndex]}`)
.setColor(supportbot.Embed.Colours.General);
- const questionMessage = await ticketChannel.send({ embeds: [questionsEmbed] });
+ const questionMessage = await ticketChannel.send({
+ embeds: [questionsEmbed],
+ });
const collector = new MessageCollector(ticketChannel, {
filter: (m) => m.author.id === interaction.user.id,
@@ -75,17 +172,23 @@ async function askTicketQuestions(ticketChannel, interaction, questions) {
});
collector.on("collect", async (message) => {
- answers.push({ question: questions[questionIndex], answer: message.content });
+ answers.push({
+ question: questions[questionIndex],
+ answer: message.content,
+ });
questionIndex++;
- await message.delete();
- await questionMessage.delete();
+ await message.delete().catch(() => {});
+ await questionMessage.delete().catch(() => {});
sendNextQuestion();
});
} else {
const summaryEmbed = new EmbedBuilder()
.setTitle(msgconfig.Ticket.TicketQuestions.Details.Title)
.setDescription(
- msgconfig.Ticket.TicketQuestions.Details.Description.replace("%user%", interaction.user.id)
+ msgconfig.Ticket.TicketQuestions.Details.Description.replace(
+ "%user%",
+ interaction.user.id,
+ ),
)
.setColor(supportbot.Embed.Colours.General)
.addFields(
@@ -93,7 +196,7 @@ async function askTicketQuestions(ticketChannel, interaction, questions) {
name: `Q: ${a.question}`,
value: `A: ${a.answer}`,
inline: false,
- }))
+ })),
)
.setTimestamp();
@@ -104,85 +207,123 @@ async function askTicketQuestions(ticketChannel, interaction, questions) {
sendNextQuestion();
}
-async function assignTicketToUser(ticketChannel, Staff, Admin, interaction, attempts = 0) {
+async function assignTicketToUser(
+ ticketChannel,
+ Staff,
+ Admin,
+ interaction,
+ departmentRole = null,
+ attempts = 0,
+) {
try {
const clockedInUsers = await getClockedInUsers(interaction.guild);
- if (clockedInUsers.size === 0 || attempts >= 2) {
- const claimChannel = await interaction.guild.channels.cache.get(supportbot.Ticket.ClaimTickets.Channel);
- if (claimChannel) {
- const claimEmbed = new EmbedBuilder()
- .setTitle(msgconfig.Ticket.ClaimTickets.ClaimTitle)
- .setDescription(msgconfig.Ticket.ClaimTickets.ClaimMessage)
- .setColor(supportbot.Embed.Colours.General);
+ let eligibleClockedInUsers = Array.from(clockedInUsers);
- const claimButton = new ButtonBuilder()
- .setCustomId(`claimticket-${ticketChannel.id}`)
- .setLabel(supportbot.Ticket.ClaimTickets.ButtonTitle || "Claim Ticket")
- .setStyle(ButtonStyle.Primary);
-
- const row = new ActionRowBuilder().addComponents(claimButton);
-
- const claimMessage = await claimChannel.send({
- content: `<@&${Staff.id}> <@&${Admin.id}>`,
- embeds: [claimEmbed],
- components: [row],
- });
-
- const filter = (i) =>
- i.customId === `claimticket-${ticketChannel.id}` &&
- i.member.roles.cache.has(Staff.id);
- const collector = claimMessage.createMessageComponentCollector({ filter, time: 120000 });
-
- collector.on("collect", async (i) => {
- await ticketChannel.send({ content: `<@${i.user.id}> has claimed the ticket.` });
- });
-
- collector.on("end", async (collected) => {
- if (collected.size === 0) {
- await claimMessage.delete();
- await claimChannel.send({
- content: `Ticket is still unclaimed. \nCC: [<@&${Staff.id}> <@&${Admin.id}>]`,
- embeds: [
- new EmbedBuilder()
- .setDescription(`Ticket is still unclaimed: <#${ticketChannel.id}>`)
- .setColor(supportbot.Embed.Colours.Warn),
- ],
- });
- }
- });
-
- return null;
- } else {
- console.error("Claim channel not found");
- return null;
- }
+ if (departmentRole) {
+ eligibleClockedInUsers = eligibleClockedInUsers.filter((userId) => {
+ const member = interaction.guild.members.cache.get(userId);
+ return member?.roles?.cache?.has(departmentRole.id);
+ });
}
- const clockedInArray = Array.from(clockedInUsers);
- const assignedUserId =
- clockedInArray[Math.floor(Math.random() * clockedInArray.length)];
- const assignedUser = await interaction.guild.members.fetch(assignedUserId);
+ const claimChannel = interaction.guild.channels.cache.get(
+ supportbot.Ticket.ClaimTickets.Channel,
+ );
+
+ if (!claimChannel) {
+ console.error("Claim channel not found");
+ return null;
+ }
+
+ const claimPing = departmentRole
+ ? `<@&${departmentRole.id}> <@&${Admin.id}>`
+ : `<@&${Staff.id}> <@&${Admin.id}>`;
const claimEmbed = new EmbedBuilder()
.setTitle(msgconfig.Ticket.ClaimTickets.ClaimTitle)
.setDescription(
- msgconfig.Ticket.ClaimTickets.ClaimMessage.replace("%user%", interaction.user.id)
+ msgconfig.Ticket.ClaimTickets.ClaimMessage.replace("%user%", interaction.user.id),
)
.setColor(supportbot.Embed.Colours.General);
const claimButton = new ButtonBuilder()
.setCustomId(`claimticket-${ticketChannel.id}`)
.setLabel(supportbot.Ticket.ClaimTickets.ButtonTitle || "Claim Ticket")
+ .setEmoji(supportbot.Ticket.ClaimTickets.ButtonEmoji || null)
.setStyle(ButtonStyle.Primary);
const row = new ActionRowBuilder().addComponents(claimButton);
- const claimChannel = await interaction.guild.channels.cache.get(
- supportbot.Ticket.ClaimTickets.Channel
- );
- if (!claimChannel) {
- console.error("Claim channel not found");
+ if (eligibleClockedInUsers.length === 0 || attempts >= 2) {
+ const claimMessage = await claimChannel.send({
+ content: claimPing,
+ embeds: [claimEmbed],
+ components: [row],
+ });
+
+ const filter = (i) => {
+ if (i.customId !== `claimticket-${ticketChannel.id}`) return false;
+
+ const hasAdmin = i.member.roles.cache.has(Admin.id);
+ const hasStaff = i.member.roles.cache.has(Staff.id);
+ const hasDepartmentRole = departmentRole
+ ? i.member.roles.cache.has(departmentRole.id)
+ : false;
+
+ return hasAdmin || hasStaff || hasDepartmentRole;
+ };
+
+ const collector = claimMessage.createMessageComponentCollector({
+ filter,
+ time: 120000,
+ });
+
+ collector.on("collect", async (i) => {
+ await i.deferUpdate().catch(() => {});
+
+ if (supportbot.Ticket.TicketType === "threads") {
+ await ticketChannel.members.add(i.user.id).catch(() => {});
+ }
+
+ await ticketChannel.send({
+ content: `<@${i.user.id}> has claimed the ticket.`,
+ });
+
+ if (typeof db.claimTicket === "function") {
+ db.claimTicket(ticketChannel.id, i.user.id);
+ }
+
+ await claimMessage
+ .edit({
+ components: [],
+ })
+ .catch(() => {});
+ });
+
+ collector.on("end", async (collected) => {
+ if (collected.size === 0) {
+ await claimMessage.delete().catch(() => {});
+ await claimChannel.send({
+ content: `Ticket is still unclaimed.\nCC: ${claimPing}`,
+ embeds: [
+ new EmbedBuilder()
+ .setDescription(`Ticket is still unclaimed: <#${ticketChannel.id}>`)
+ .setColor(supportbot.Embed.Colours.Warn),
+ ],
+ });
+ }
+ });
+
+ return null;
+ }
+
+ const assignedUserId =
+ eligibleClockedInUsers[Math.floor(Math.random() * eligibleClockedInUsers.length)];
+
+ const assignedUser = await interaction.guild.members.fetch(assignedUserId).catch(() => null);
+
+ if (!assignedUser) {
return null;
}
@@ -192,23 +333,45 @@ async function assignTicketToUser(ticketChannel, Staff, Admin, interaction, atte
components: [row],
});
- const filter = (i) =>
- i.customId === `claimticket-${ticketChannel.id}` && i.user.id === assignedUserId;
- const collector = claimMessage.createMessageComponentCollector({ filter, time: 120000 });
+ const filter = (i) => {
+ if (i.customId !== `claimticket-${ticketChannel.id}`) return false;
+ return i.user.id === assignedUserId;
+ };
+
+ const collector = claimMessage.createMessageComponentCollector({
+ filter,
+ time: 120000,
+ });
collector.on("collect", async (i) => {
+ await i.deferUpdate().catch(() => {});
+
+ if (supportbot.Ticket.TicketType === "threads") {
+ await ticketChannel.members.add(i.user.id).catch(() => {});
+ }
+
await ticketChannel
- .send({ content: `<@${assignedUserId}>` })
- .then((msg) => setTimeout(() => msg.delete(), 5000));
+ .send({ content: `<@${assignedUserId}> has claimed the ticket.` })
+ .catch(() => {});
+
+ if (typeof db.claimTicket === "function") {
+ db.claimTicket(ticketChannel.id, assignedUserId);
+ }
+
+ await claimMessage
+ .edit({
+ components: [],
+ })
+ .catch(() => {});
});
collector.on("end", async (collected) => {
if (collected.size === 0) {
- await claimMessage.delete();
+ await claimMessage.delete().catch(() => {});
if (attempts >= 1) {
await claimChannel.send({
- content: `No one claimed the ticket. Tagging roles: <@&${Staff.id}> <@&${Admin.id}>`,
+ content: `No one claimed the ticket. Tagging roles: ${claimPing}`,
embeds: [
new EmbedBuilder()
.setDescription(`Ticket is still unclaimed: <#${ticketChannel.id}>`)
@@ -216,46 +379,56 @@ async function assignTicketToUser(ticketChannel, Staff, Admin, interaction, atte
],
});
- const claimEmbed = new EmbedBuilder()
- .setTitle(msgconfig.Ticket.ClaimTickets.ClaimTitle)
- .setDescription(msgconfig.Ticket.ClaimTickets.ClaimMessage)
- .setColor(supportbot.Embed.Colours.General);
-
- const claimButton = new ButtonBuilder()
- .setCustomId(`claimticket-${ticketChannel.id}`)
- .setLabel(supportbot.Ticket.ClaimTickets.ButtonTitle)
- .setEmoji(supportbot.Ticket.ClaimTickets.ButtonEmoji)
- .setStyle(ButtonStyle.Primary);
-
- const row = new ActionRowBuilder().addComponents(claimButton);
-
- const claimMessage = await claimChannel.send({
+ const retryClaimMessage = await claimChannel.send({
+ content: claimPing,
embeds: [claimEmbed],
components: [row],
});
- const filter = (i) =>
- i.customId === `claimticket-${ticketChannel.id}` &&
- i.member.roles.cache.has(Staff.id);
- const collector = claimMessage.createMessageComponentCollector({
- filter,
+ const retryFilter = (i) => {
+ if (i.customId !== `claimticket-${ticketChannel.id}`) return false;
+
+ const hasAdmin = i.member.roles.cache.has(Admin.id);
+ const hasStaff = i.member.roles.cache.has(Staff.id);
+ const hasDepartmentRole = departmentRole
+ ? i.member.roles.cache.has(departmentRole.id)
+ : false;
+
+ return hasAdmin || hasStaff || hasDepartmentRole;
+ };
+
+ const retryCollector = retryClaimMessage.createMessageComponentCollector({
+ filter: retryFilter,
time: 120000,
});
- collector.on("collect", async (i) => {
- console.log(`Ticket ${ticketChannel.id} claimed by user ${i.user.id}`);
- await ticketChannel.member.add(i.user.id);
+ retryCollector.on("collect", async (i) => {
+ await i.deferUpdate().catch(() => {});
+
+ if (supportbot.Ticket.TicketType === "threads") {
+ await ticketChannel.members.add(i.user.id).catch(() => {});
+ }
+
+ await ticketChannel.send({
+ content: `<@${i.user.id}> has claimed the ticket.`,
+ });
+
+ if (typeof db.claimTicket === "function") {
+ db.claimTicket(ticketChannel.id, i.user.id);
+ }
+
+ await retryClaimMessage
+ .edit({
+ components: [],
+ })
+ .catch(() => {});
});
- collector.on("end", async (collected) => {
- if (collected.size === 0) {
- console.log(
- `No one claimed the ticket (${ticketChannel.id}), notifying all staff`
- );
- await claimMessage.delete();
-
+ retryCollector.on("end", async (retryCollected) => {
+ if (retryCollected.size === 0) {
+ await retryClaimMessage.delete().catch(() => {});
await claimChannel.send({
- content: `No one claimed the ticket. Tagging roles: <@&${Staff.id}> <@&${Admin.id}>`,
+ content: `No one claimed the ticket. Tagging roles: ${claimPing}`,
});
}
});
@@ -263,12 +436,23 @@ async function assignTicketToUser(ticketChannel, Staff, Admin, interaction, atte
const ReassigningEmbed = new EmbedBuilder()
.setTitle(msgconfig.Ticket.ClaimTickets.ReassigningTitle)
.setDescription(
- msgconfig.Ticket.ClaimTickets.ReassigningMessage.replace("%user%", assignedUserId)
+ msgconfig.Ticket.ClaimTickets.ReassigningMessage.replace("%user%", assignedUserId),
)
.setColor(supportbot.Embed.Colours.General);
- await claimChannel.send({ embeds: [ReassigningEmbed], components: [] });
- await assignTicketToUser(ticketChannel, Staff, Admin, interaction, attempts + 1);
+ await claimChannel.send({
+ embeds: [ReassigningEmbed],
+ components: [],
+ });
+
+ await assignTicketToUser(
+ ticketChannel,
+ Staff,
+ Admin,
+ interaction,
+ departmentRole,
+ attempts + 1,
+ );
}
}
});
@@ -284,39 +468,74 @@ module.exports = new Command({
name: cmdconfig.OpenTicket.Command,
description: cmdconfig.OpenTicket.Description,
type: ApplicationCommandType.ChatInput,
+
options: [
+ ...(areDepartmentsEnabled()
+ ? [
+ {
+ name: "department",
+ description: "Choose a department",
+ type: ApplicationCommandOptionType.String,
+ required: true,
+ choices: Object.entries(getDepartmentSystem().Departments || {})
+ .slice(0, 25)
+ .map(([key, dept]) => ({
+ name: dept.Name || key,
+ value: key,
+ })),
+ },
+ ]
+ : []),
+
+ ...(arePrioritiesEnabled()
+ ? [
+ {
+ name: "priority",
+ description: "Choose a priority",
+ type: ApplicationCommandOptionType.String,
+ required: false,
+ choices: Object.entries(getPrioritySystem().Priorities || {})
+ .slice(0, 25)
+ .map(([key, priority]) => ({
+ name: priority.Name || key,
+ value: key,
+ })),
+ },
+ ]
+ : []),
+
{
name: "reason",
description: "Ticket Reason",
type: ApplicationCommandOptionType.String,
+ required: false,
},
],
+
permissions: cmdconfig.OpenTicket.Permission,
-
+
async run(interaction) {
-
try {
-
if (await isBlacklisted(interaction.user.id)) {
return interaction.reply({
content: msgconfig.Ticket.Blacklisted,
- flags: MessageFlags.Ephemeral
+ flags: MessageFlags.Ephemeral,
});
}
let TicketReason = null;
if (supportbot.Ticket.TicketReason) {
- TicketReason = interaction.reason || null;
+ TicketReason = interaction.options?.getString?.("reason") || interaction.reason || null;
}
let TicketData = { tickets: db.getAllTickets() };
const { getRole, getChannel } = interaction.client;
- let User = interaction.guild.members.cache.get(interaction.user.id);
if (
supportbot.Ticket.TicketsPerUser &&
- TicketData.tickets.filter((t) => t.user === interaction.user.id && t.open).length >= supportbot.Ticket.TicketsPerUser
+ TicketData.tickets.filter((t) => t.user === interaction.user.id && t.open).length >=
+ supportbot.Ticket.TicketsPerUser
) {
return interaction.reply({
embeds: [
@@ -326,12 +545,27 @@ module.exports = new Command({
color: supportbot.Embed.Colours.Warn,
},
],
- flags: MessageFlags.Ephemeral
+ flags: MessageFlags.Ephemeral,
});
}
+ const usingThreads = supportbot.Ticket.TicketType === "threads";
+ const departmentsEnabled = areDepartmentsEnabled();
+ const prioritiesEnabled = arePrioritiesEnabled();
+
+ const departmentKey = departmentsEnabled ? getSelectedDepartmentKey(interaction) : null;
+ const departmentConfig = departmentsEnabled ? getDepartmentConfig(departmentKey) : null;
+
+ const priorityKey = prioritiesEnabled ? getPriorityKey(interaction, departmentKey) : null;
+ const priorityConfig = prioritiesEnabled ? getPriorityConfig(priorityKey) : null;
+
let ticketNumberID = await TicketNumberID.pad();
const TicketSubject = TicketReason || msgconfig.Ticket.InvalidSubject;
+ const ticketChannelName = buildTicketChannelName(
+ ticketNumberID,
+ priorityKey,
+ departmentConfig,
+ );
const TicketExists = new EmbedBuilder()
.setTitle("Ticket Exists!")
@@ -339,60 +573,76 @@ module.exports = new Command({
if (
await interaction.guild.channels.cache.find(
- (ticketChannel) => ticketChannel.name === `${supportbot.Ticket.Channel}${ticketNumberID}`
+ (ticketChannel) => ticketChannel.name === ticketChannelName,
)
) {
return await interaction.reply({
embeds: [TicketExists],
- flags: MessageFlags.Ephemeral
+ flags: MessageFlags.Ephemeral,
});
}
const Staff = await getRole(supportbot.Roles.StaffMember.Staff, interaction.guild);
const Admin = await getRole(supportbot.Roles.StaffMember.Admin, interaction.guild);
- if (!Staff || !Admin)
+
+ const departmentRoleId = departmentsEnabled ? departmentConfig?.Role || null : null;
+ const DepartmentRole = departmentRoleId
+ ? await getRole(departmentRoleId, interaction.guild)
+ : null;
+
+ if (!Staff || !Admin) {
return interaction.reply({
content: "Some roles seem to be missing!\nPlease check for errors when starting the bot.",
- flags: MessageFlags.Ephemeral
+ flags: MessageFlags.Ephemeral,
});
+ }
let ticketChannel;
- if (supportbot.Ticket.TicketType === "threads") {
+ if (usingThreads) {
const TicketHome = interaction.guild.channels.cache.find(
- (c) => c.name === supportbot.Ticket.TicketHome || c.id === supportbot.Ticket.TicketHome
+ (c) => c.name === supportbot.Ticket.TicketHome || c.id === supportbot.Ticket.TicketHome,
);
const channel = await getChannel(supportbot.Ticket.TicketHome, interaction.guild);
+
ticketChannel = await channel.threads.create({
- name: `${supportbot.Ticket.Channel}${ticketNumberID}`,
+ name: ticketChannelName,
type: ChannelType.PrivateThread,
parent: TicketHome,
autoArchiveDuration: 60,
- reason: 'support ticket',
+ reason: "support ticket",
});
} else if (supportbot.Ticket.TicketType === "channels") {
+ let targetCategoryId = supportbot.Ticket.TicketChannelsCategory;
+
+ if (departmentsEnabled && departmentConfig?.Category) {
+ targetCategoryId = departmentConfig.Category;
+ }
+
const category = interaction.guild.channels.cache.find(
- (c) => c.name === supportbot.Ticket.TicketChannelsCategory || c.id === supportbot.Ticket.TicketChannelsCategory
+ (c) => c.name === targetCategoryId || c.id === targetCategoryId,
);
if (!category) {
return interaction.reply({
content: "The ticket category does not exist!",
- flags: MessageFlags.Ephemeral
+ flags: MessageFlags.Ephemeral,
});
}
ticketChannel = await interaction.guild.channels.create({
- name: `${supportbot.Ticket.Channel}${ticketNumberID}`,
+ name: ticketChannelName,
type: ChannelType.GuildText,
parent: category.id,
- reason: 'support ticket',
+ reason: "support ticket",
});
- if (category.children.size >= 50) {
+ if (!departmentsEnabled && category.children.cache && category.children.cache.size >= 50) {
const secondaryCategory = interaction.guild.channels.cache.find(
- (c) => c.name === supportbot.Ticket.TicketChannelsCategory2 || c.id === supportbot.Ticket.TicketChannelsCategory2
+ (c) =>
+ c.name === supportbot.Ticket.TicketChannelsCategory2 ||
+ c.id === supportbot.Ticket.TicketChannelsCategory2,
);
if (secondaryCategory) {
@@ -400,40 +650,49 @@ module.exports = new Command({
} else {
return interaction.reply({
content: "The secondary ticket category does not exist!",
- flags: MessageFlags.Ephemeral
+ flags: MessageFlags.Ephemeral,
});
}
}
}
if (supportbot.Ticket.ClaimTickets.Enabled) {
- const assignedUserId = await assignTicketToUser(ticketChannel, Staff, Admin, interaction);
- if (!assignedUserId) {
- const claimChannel = await interaction.guild.channels.cache.get(supportbot.Ticket.ClaimTickets.Channel);
- }
+ await assignTicketToUser(ticketChannel, Staff, Admin, interaction, DepartmentRole || null);
}
- if (supportbot.Ticket.TicketType === "threads") {
- await ticketChannel.members.add(interaction.user.id);
-
- } else if (supportbot.Ticket.TicketType === "channels") {
-
+ if (usingThreads) {
+ await ticketChannel.members.add(interaction.user.id);
+ } else if (supportbot.Ticket.TicketType === "channels") {
await ticketChannel.permissionOverwrites.create(interaction.user.id, {
- ViewChannel: true,
- SendMessages: true,
- ReadMessageHistory: true,
- });
-
- await ticketChannel.permissionOverwrites.create(Admin, {
ViewChannel: true,
SendMessages: true,
ReadMessageHistory: true,
- });
+ });
- await ticketChannel.permissionOverwrites.create(interaction.guild.roles.everyone, {
+ await ticketChannel.permissionOverwrites.create(Admin.id, {
+ ViewChannel: true,
+ SendMessages: true,
+ ReadMessageHistory: true,
+ });
+
+ await ticketChannel.permissionOverwrites.create(Staff.id, {
+ ViewChannel: true,
+ SendMessages: true,
+ ReadMessageHistory: true,
+ });
+
+ if (DepartmentRole) {
+ await ticketChannel.permissionOverwrites.create(DepartmentRole.id, {
+ ViewChannel: true,
+ SendMessages: true,
+ ReadMessageHistory: true,
+ });
+ }
+
+ await ticketChannel.permissionOverwrites.create(interaction.guild.roles.everyone.id, {
ViewChannel: false,
- });
- }
+ });
+ }
db.addTicket({
id: ticketChannel.id,
@@ -445,7 +704,10 @@ module.exports = new Command({
subUsers: [],
createdAt: new Date().toISOString(),
claimedAt: null,
+ department: departmentKey,
+ priority: priorityKey,
});
+
TicketData.tickets = db.getAllTickets();
const CreatedTicket = new EmbedBuilder()
@@ -453,19 +715,21 @@ module.exports = new Command({
msgconfig.Ticket.TicketCreatedAlert.replace(/%ticketauthor%/g, interaction.user.id)
.replace(/%ticketid%/g, ticketChannel.id)
.replace(/%ticketusername%/g, interaction.user.username)
- .replace(/%ticketreason%/g, TicketSubject)
+ .replace(/%ticketreason%/g, TicketSubject),
)
.setColor(supportbot.Embed.Colours.General);
- await interaction.reply({ embeds: [CreatedTicket], flags: MessageFlags.Ephemeral });
- // TICKET MESSAGE - UPDATED WITH COMPONENTS V2
-
- const ticketMsgContainer = new ContainerBuilder()
-
+ await interaction.reply({
+ embeds: [CreatedTicket],
+ flags: MessageFlags.Ephemeral,
+ });
+
+ const ticketMsgContainer = new ContainerBuilder();
+
if (supportbot.Embed.Colours.General) {
ticketMsgContainer.setAccentColor(
- parseInt(supportbot.Embed.Colours.General.replace("#", ""), 16)
- )
+ parseInt(supportbot.Embed.Colours.General.replace("#", ""), 16),
+ );
}
const ticketAuthor = new TextDisplayBuilder().setContent(
@@ -474,112 +738,144 @@ module.exports = new Command({
.replace(/%ticketusername%/g, interaction.user.username),
);
- ticketMsgContainer.addTextDisplayComponents(ticketAuthor)
+ ticketMsgContainer.addTextDisplayComponents(ticketAuthor);
const ticketTitle = new TextDisplayBuilder().setContent(
msgconfig.Ticket.TicketTitle.replace(/%ticketauthor%/g, interaction.user.id)
.replace(/%ticketid%/g, ticketChannel.id)
- .replace(/%ticketusername%/g, interaction.user.username)
+ .replace(/%ticketusername%/g, interaction.user.username),
);
- ticketMsgContainer.addTextDisplayComponents(ticketTitle)
-
- const seperator2 = new SeparatorBuilder()
- .setDivider(true)
+ ticketMsgContainer.addTextDisplayComponents(ticketTitle);
- ticketMsgContainer.addSeparatorComponents(seperator2)
+ const seperator2 = new SeparatorBuilder().setDivider(true);
+ ticketMsgContainer.addSeparatorComponents(seperator2);
const ticketDescription = new TextDisplayBuilder().setContent(
msgconfig.Ticket.TicketMessage.replace(/%ticketauthor%/g, interaction.user.id)
.replace(/%ticketid%/g, ticketChannel.id)
.replace(/%ticketusername%/g, interaction.user.username)
- .replace(/%ticketreason%/g, TicketSubject)
+ .replace(/%ticketreason%/g, TicketSubject),
);
- ticketMsgContainer.addTextDisplayComponents(ticketDescription)
+ ticketMsgContainer.addTextDisplayComponents(ticketDescription);
- const seperator3 = new SeparatorBuilder()
- .setDivider(true)
-
- ticketMsgContainer.addSeparatorComponents(seperator3)
-
- const ticketReason = new TextDisplayBuilder().setContent(
- `**Reason:**\n ${TicketReason}`
- );
+ const seperator3 = new SeparatorBuilder().setDivider(true);
+ ticketMsgContainer.addSeparatorComponents(seperator3);
if (supportbot.Ticket.TicketReason && TicketReason) {
- ticketMsgContainer.addTextDisplayComponents(ticketReason)
+ const ticketReason = new TextDisplayBuilder().setContent(`**Reason:**\n${TicketReason}`);
+ ticketMsgContainer.addTextDisplayComponents(ticketReason);
+ }
+
+ if (departmentsEnabled && departmentConfig) {
+ const deptLabel = departmentConfig?.Name || departmentKey || "General";
+ const deptEmoji = departmentConfig?.Emoji || "🎫";
+
+ const departmentText = new TextDisplayBuilder().setContent(
+ `**Department:** ${deptEmoji} ${deptLabel}`,
+ );
+
+ ticketMsgContainer.addTextDisplayComponents(departmentText);
+ }
+
+ if (prioritiesEnabled && priorityConfig && priorityKey) {
+ const priorityLabel = priorityConfig?.Name || priorityKey || "Medium";
+ const priorityEmoji = priorityConfig?.Emoji || "🟡";
+
+ const priorityText = new TextDisplayBuilder().setContent(
+ `**Priority:** ${priorityEmoji} ${priorityLabel}`,
+ );
+
+ ticketMsgContainer.addTextDisplayComponents(priorityText);
}
const selectMenuRow = new ActionRowBuilder();
const SelectMenus = new StringSelectMenuBuilder()
- .setCustomId("ticketcontrolpanel")
- .setPlaceholder("Ticket Control Panel")
- .addOptions(
- new StringSelectMenuOptionBuilder()
- .setLabel("Close")
- .setDescription("Close the ticket.")
- .setEmoji(supportbot.SelectMenus.Tickets.CloseEmoji)
- .setValue("ticketclose"),
- new StringSelectMenuOptionBuilder()
- .setLabel("Voice Channel")
- .setDescription("Create a support voice channel.")
- .setEmoji(supportbot.SelectMenus.Tickets.VCEmoji)
- .setValue("supportvc"),
- new StringSelectMenuOptionBuilder()
- .setLabel("Archive")
- .setDescription("Archive the ticket.")
- .setEmoji(supportbot.SelectMenus.Tickets.ArchiveEmoji)
- .setValue("archiveticket"),
- ...(supportbot.Ticket.TicketType === "threads" ? [
+ .setCustomId("ticketcontrolpanel")
+ .setPlaceholder("Ticket Control Panel")
+ .addOptions(
new StringSelectMenuOptionBuilder()
- .setLabel("Lock Ticket")
- .setDescription("Lock the ticket.")
- .setEmoji(supportbot.SelectMenus.Tickets.LockEmoji)
- .setValue("lockticket")
- ] : []),
- new StringSelectMenuOptionBuilder()
- .setLabel("Rename Ticket")
- .setDescription("Rename the ticket.")
- .setEmoji(supportbot.SelectMenus.Tickets.RenameEmoji)
- .setValue("renameticket")
- );
+ .setLabel("Close")
+ .setDescription("Close the ticket.")
+ .setEmoji(supportbot.SelectMenus.Tickets.CloseEmoji)
+ .setValue("ticketclose"),
+ new StringSelectMenuOptionBuilder()
+ .setLabel("Voice Channel")
+ .setDescription("Create a support voice channel.")
+ .setEmoji(supportbot.SelectMenus.Tickets.VCEmoji)
+ .setValue("supportvc"),
+ new StringSelectMenuOptionBuilder()
+ .setLabel("Archive")
+ .setDescription("Archive the ticket.")
+ .setEmoji(supportbot.SelectMenus.Tickets.ArchiveEmoji)
+ .setValue("archiveticket"),
+ ...(supportbot.Ticket.TicketType === "threads"
+ ? [
+ new StringSelectMenuOptionBuilder()
+ .setLabel("Lock Ticket")
+ .setDescription("Lock the ticket.")
+ .setEmoji(supportbot.SelectMenus.Tickets.LockEmoji)
+ .setValue("lockticket"),
+ ]
+ : []),
+ new StringSelectMenuOptionBuilder()
+ .setLabel("Rename Ticket")
+ .setDescription("Rename the ticket.")
+ .setEmoji(supportbot.SelectMenus.Tickets.RenameEmoji)
+ .setValue("renameticket"),
+ );
- if (supportbot.Ticket.TicketType === "threads") {
- SelectMenus.addOptions(
- new StringSelectMenuOptionBuilder()
- .setLabel("Enable Invites")
- .setDescription("Enable invites.")
- .setEmoji(supportbot.SelectMenus.Tickets.EnableInvitesEmoji)
- .setValue("enableinvites"),
- new StringSelectMenuOptionBuilder()
- .setLabel("Disable Invites")
- .setDescription("Disable invites.")
- .setEmoji(supportbot.SelectMenus.Tickets.DisableInvitesEmoji)
- .setValue("disableinvites"),
- );
- }
+ if (supportbot.Ticket.TicketType === "threads") {
+ SelectMenus.addOptions(
+ new StringSelectMenuOptionBuilder()
+ .setLabel("Enable Invites")
+ .setDescription("Enable invites.")
+ .setEmoji(supportbot.SelectMenus.Tickets.EnableInvitesEmoji)
+ .setValue("enableinvites"),
+ new StringSelectMenuOptionBuilder()
+ .setLabel("Disable Invites")
+ .setDescription("Disable invites.")
+ .setEmoji(supportbot.SelectMenus.Tickets.DisableInvitesEmoji)
+ .setValue("disableinvites"),
+ );
+ }
- selectMenuRow.addComponents(SelectMenus)
-
- ticketMsgContainer.addActionRowComponents(selectMenuRow)
+ selectMenuRow.addComponents(SelectMenus);
+ ticketMsgContainer.addActionRowComponents(selectMenuRow);
await ticketChannel.send({
flags: MessageFlags.IsComponentsV2,
components: [ticketMsgContainer],
});
- if (supportbot.Ticket.Questions.Enabled) {
- const questions = supportbot.Ticket.Questions.List || [];
-
- if (questions.length > 0) {
- await askTicketQuestions(ticketChannel, interaction, questions);
- }
-}
-
+ const departmentQuestions =
+ departmentsEnabled && departmentConfig?.Questions ? departmentConfig.Questions : [];
+
+ const globalQuestionsEnabled = supportbot.Ticket.Questions?.Enabled;
+ const globalQuestions = supportbot.Ticket.Questions?.List || [];
+
+ let questionsToAsk = [];
+
+ if (departmentQuestions.length > 0) {
+ questionsToAsk = departmentQuestions;
+ } else if (globalQuestionsEnabled && globalQuestions.length > 0) {
+ questionsToAsk = globalQuestions;
+ }
+
+ if (questionsToAsk.length > 0) {
+ await askTicketQuestions(ticketChannel, interaction, questionsToAsk);
+ }
} catch (error) {
console.error("Error in run method:", error);
+
+ if (!interaction.replied && !interaction.deferred) {
+ return interaction.reply({
+ content: "An error occurred while creating the ticket.",
+ flags: MessageFlags.Ephemeral,
+ });
+ }
}
},
});
diff --git a/Commands/Tickets/ticketpanel.js b/Commands/Tickets/ticketpanel.js
index f880c5a..dd9b002 100644
--- a/Commands/Tickets/ticketpanel.js
+++ b/Commands/Tickets/ticketpanel.js
@@ -1,16 +1,19 @@
+// SupportBot | Emerald Services
+// Ticket Panel Command
+
const fs = require("fs");
const {
- EmbedBuilder,
ApplicationCommandType,
ActionRowBuilder,
ButtonBuilder,
+ ButtonStyle,
MessageFlags,
ContainerBuilder,
TextDisplayBuilder,
MediaGalleryBuilder,
SeparatorBuilder,
- ThumbnailBuilder,
- SectionBuilder
+ SectionBuilder,
+ StringSelectMenuBuilder,
} = require("discord.js");
const yaml = require("js-yaml");
@@ -21,6 +24,25 @@ const cmdconfig = yaml.load(fs.readFileSync("./Configs/commands.yml", "utf8"));
const Command = require("../../Structures/Command.js");
const db = require("../../Structures/Database.js");
+function parseButtonStyle(style) {
+ switch (String(style || "1")) {
+ case "1":
+ return ButtonStyle.Primary;
+ case "2":
+ return ButtonStyle.Secondary;
+ case "3":
+ return ButtonStyle.Danger;
+ case "4":
+ return ButtonStyle.Success;
+ default:
+ return ButtonStyle.Primary;
+ }
+}
+
+function getPanelMessage(key, fallback) {
+ return panelconfig.Messages?.[key] || fallback;
+}
+
module.exports = new Command({
name: cmdconfig.TicketPanel.Command,
description: cmdconfig.TicketPanel.Description,
@@ -34,26 +56,33 @@ module.exports = new Command({
"\u001b[31m",
`[TICKET PANEL]`,
"\u001b[33m",
- "Ticket Panel is not setup, You can set this in", `\u001b[31m`, '/Configs/ticket-panel.yml', `\n `,
+ "Ticket Panel is not setup, You can set this in",
+ `\u001b[31m`,
+ "/Configs/ticket-panel.yml",
+ `\n `,
);
+
return interaction.reply({
- content: "Ticket Panel is not set up. Please configure it in `/Configs/ticket-panel.yml`.",
- flags: MessageFlags.Ephemeral
+ content: getPanelMessage(
+ "NotConfigured",
+ "Ticket Panel is not set up. Please configure it in `/Configs/ticket-panel.yml`.",
+ ),
+ flags: MessageFlags.Ephemeral,
});
}
const { getChannel } = interaction.client;
- const channel = await getChannel(
- supportbot.Ticket.TicketHome,
- interaction.guild
- );
+ const channel = await getChannel(supportbot.Ticket.TicketHome, interaction.guild);
if (!channel) {
- console.log(`[TICKET PANEL] ${channelName} channel not found. Please check your config file.`);
+ console.log(`[TICKET PANEL] TicketHome channel not found. Please check your config file.`);
return interaction.reply({
- content: `${channelName} channel not found. Please check your config file.`,
- flags: MessageFlags.Ephemeral
+ content: getPanelMessage(
+ "InvalidChannel",
+ "TicketHome channel not found. Please check your config file.",
+ ),
+ flags: MessageFlags.Ephemeral,
});
}
@@ -61,101 +90,178 @@ module.exports = new Command({
if (panelRow) {
try {
- await channel.messages.fetch(panelRow.message_id);
- return interaction.reply({
- content: "Ticket panel message already exists.",
- flags: MessageFlags.Ephemeral
- });
- } catch (err) {}
+ const oldMessage = await channel.messages.fetch(panelRow.message_id);
+ if (oldMessage) {
+ await oldMessage.delete().catch(() => {});
+ }
+ } catch (err) {
+ console.log(`[TICKET PANEL] Previous panel message not found, creating a new one.`);
+ }
}
- const panelContainer = new ContainerBuilder()
+ const departmentSystem = supportbot.Ticket?.DepartmentSystem || {};
+ const prioritySystem = supportbot.Ticket?.PrioritySystem || {};
+ const usingThreads = supportbot.Ticket?.TicketType === "threads";
- if (panelconfig.Style.Color) {
- panelContainer.setAccentColor(
- parseInt(panelconfig.Style.Color.replace("#", ""), 16)
- )
+ const departmentsEnabled = departmentSystem.Enabled === true && !usingThreads;
+ const prioritiesEnabled = prioritySystem.Enabled === true && !usingThreads;
+ const departments = departmentSystem.Departments || {};
+
+ if (usingThreads && (departmentSystem.Enabled || prioritySystem.Enabled)) {
+ console.log(
+ "[SupportBot] Departments and Priority are disabled because TicketType is set to 'threads'.",
+ );
}
+ const departmentOptions = Object.entries(departments)
+ .slice(0, 25)
+ .map(([key, dept]) => ({
+ label: dept.Name || key,
+ description:
+ dept.PanelDescription ||
+ dept.Description ||
+ panelconfig.DepartmentMenu?.DefaultDescription ||
+ `Open a ${String(dept.Name || key).toLowerCase()} ticket`,
+ value: key,
+ emoji:
+ dept.Emoji ||
+ panelconfig.DepartmentMenu?.DefaultEmoji ||
+ panelconfig.Button?.Emoji ||
+ undefined,
+ }));
+
+ const shouldUseDepartments = departmentsEnabled && departmentOptions.length > 0;
+
+ let openerComponent;
+
+ if (shouldUseDepartments) {
+ openerComponent = new StringSelectMenuBuilder()
+ .setCustomId(panelconfig.DepartmentMenu?.CustomId || "ticketdepartmentselect")
+ .setPlaceholder(
+ panelconfig.DepartmentMenu?.Placeholder ||
+ departmentSystem.Placeholder ||
+ panelconfig.Button?.Text ||
+ "Choose a ticket department",
+ )
+ .addOptions(departmentOptions);
+ } else {
+ openerComponent = new ButtonBuilder()
+ .setCustomId(panelconfig.Button?.CustomId || "createticket")
+ .setLabel(panelconfig.Button?.Text || "Create Ticket")
+ .setStyle(parseButtonStyle(panelconfig.Button?.Color));
+
+ if (panelconfig.Button?.Emoji) {
+ openerComponent.setEmoji(panelconfig.Button.Emoji);
+ }
+ }
+
+ const panelContainer = new ContainerBuilder();
+
+ if (panelconfig.Style?.Color) {
+ panelContainer.setAccentColor(parseInt(String(panelconfig.Style.Color).replace("#", ""), 16));
+ }
+
+ const layout = String(panelconfig.Style?.Layout || "3");
+ const showImage = panelconfig.Settings?.Image === true && !!panelconfig.Style?.Image;
+ const showTopTitle = panelconfig.Settings?.TopTitle === true;
+ const showTopDivider = panelconfig.Settings?.TopDivider === true;
+ const showBottomTitle = panelconfig.Settings?.BottomTitle === true;
+ const showBottomDivider = panelconfig.Settings?.BottomDivider === true;
+
const panelTitle = new TextDisplayBuilder().setContent(
- panelconfig.Style.Title,
+ panelconfig.Style?.Title || "## SupportBot Ticket Creation",
);
- const topSeperator = new SeparatorBuilder()
- .setDivider(true)
-
- if (panelconfig.Settings.TopTitle) {
- panelContainer.addTextDisplayComponents(panelTitle)
- }
-
- if (panelconfig.Settings.TopDivider) {
- panelContainer.addSeparatorComponents(topSeperator)
- }
-
const panelDesc = new TextDisplayBuilder().setContent(
- panelconfig.Style.Description,
+ panelconfig.Style?.Description || "Click on the button below to create a support ticket.",
);
- if (panelconfig.Style.Layout === "3") {
- panelContainer.addTextDisplayComponents(panelDesc)
+ if (showTopTitle) {
+ panelContainer.addTextDisplayComponents(panelTitle);
}
- const createTicketButton = new ButtonBuilder()
- .setCustomId("createticket")
- .setLabel(panelconfig.Button.Text)
- .setEmoji(panelconfig.Button.Emoji)
- .setStyle(panelconfig.Button.Color);
-
- const middleSection = new SectionBuilder()
- .addTextDisplayComponents(panelDesc)
- .setButtonAccessory(createTicketButton)
-
- if (panelconfig.Style.Layout === "1") {
- panelContainer.addSectionComponents(middleSection)
+ if (showTopDivider) {
+ panelContainer.addSeparatorComponents(new SeparatorBuilder().setDivider(true));
}
- const panelImage = new MediaGalleryBuilder()
- .addItems([
+ if (layout === "1" || layout === "3") {
+ panelContainer.addTextDisplayComponents(panelDesc);
+ }
+
+ if (showImage && layout === "2") {
+ const panelImage = new MediaGalleryBuilder().addItems([
{
media: {
url: panelconfig.Style.Image,
},
- }
- ])
+ },
+ ]);
- if (panelconfig.Settings.Image) {
- panelContainer.addMediaGalleryComponents(panelImage)
+ panelContainer.addMediaGalleryComponents(panelImage);
}
- // START OF STYLE 2 - TEXT UNDER THE IMAGE
+ if (layout === "1") {
+ const middleSection = new SectionBuilder().addTextDisplayComponents(panelDesc);
- const bottomSeperator = new SeparatorBuilder()
- .setDivider(true)
-
- if (panelconfig.Settings.BottomTitle) {
- panelContainer.addTextDisplayComponents(panelTitle)
+ if (!shouldUseDepartments) {
+ middleSection.setButtonAccessory(openerComponent);
+ panelContainer.addSectionComponents(middleSection);
+ } else {
+ panelContainer.addSectionComponents(middleSection);
+ panelContainer.addActionRowComponents(
+ new ActionRowBuilder().addComponents(openerComponent),
+ );
+ }
}
- if (panelconfig.Settings.BottomDivider) {
- panelContainer.addSeparatorComponents(bottomSeperator)
+ if (showImage && layout === "3") {
+ const panelImage = new MediaGalleryBuilder().addItems([
+ {
+ media: {
+ url: panelconfig.Style.Image,
+ },
+ },
+ ]);
+
+ panelContainer.addMediaGalleryComponents(panelImage);
}
- if (panelconfig.Style.Layout === "2") {
- panelContainer.addSectionComponents(middleSection)
+ if (showBottomTitle) {
+ panelContainer.addTextDisplayComponents(panelTitle);
}
- // START OF STYLE 3
+ if (showBottomDivider) {
+ panelContainer.addSeparatorComponents(new SeparatorBuilder().setDivider(true));
+ }
- const buttonRow = new ActionRowBuilder();
- const ticketButtonRow = new ButtonBuilder()
- .setCustomId("createticket")
- .setLabel(panelconfig.Button.Text)
- .setEmoji(panelconfig.Button.Emoji)
- .setStyle(panelconfig.Button.Color);
- buttonRow.addComponents(ticketButtonRow)
+ if (layout === "2") {
+ const middleSection = new SectionBuilder().addTextDisplayComponents(panelDesc);
- if (panelconfig.Style.Layout === "3") {
- panelContainer.addActionRowComponents(buttonRow)
+ if (!shouldUseDepartments) {
+ middleSection.setButtonAccessory(openerComponent);
+ panelContainer.addSectionComponents(middleSection);
+ } else {
+ panelContainer.addSectionComponents(middleSection);
+ panelContainer.addActionRowComponents(
+ new ActionRowBuilder().addComponents(openerComponent),
+ );
+ }
+ }
+
+ if (layout === "3") {
+ panelContainer.addActionRowComponents(new ActionRowBuilder().addComponents(openerComponent));
+ }
+
+ if (showImage && layout === "1") {
+ const panelImage = new MediaGalleryBuilder().addItems([
+ {
+ media: {
+ url: panelconfig.Style.Image,
+ },
+ },
+ ]);
+
+ panelContainer.addMediaGalleryComponents(panelImage);
}
try {
@@ -167,14 +273,22 @@ module.exports = new Command({
db.saveTicketPanel(message.id, channel.id);
return interaction.reply({
- content: "Ticket panel message has been sent!",
- flags: MessageFlags.Ephemeral
+ content: shouldUseDepartments
+ ? getPanelMessage(
+ "PanelSentWithDepartments",
+ "Ticket panel message with department selection has been sent!",
+ )
+ : getPanelMessage("PanelSent", "Ticket panel message has been sent!"),
+ flags: MessageFlags.Ephemeral,
});
} catch (e) {
console.log("Error sending message:", e);
return interaction.reply({
- content: "An error occurred while sending the ticket panel message.",
- flags: MessageFlags.Ephemeral
+ content: getPanelMessage(
+ "SendError",
+ "An error occurred while sending the ticket panel message.",
+ ),
+ flags: MessageFlags.Ephemeral,
});
}
},
diff --git a/Configs/commands.yml b/Configs/commands.yml
index 2497433..76154db 100644
--- a/Configs/commands.yml
+++ b/Configs/commands.yml
@@ -37,38 +37,17 @@ Profile:
Permission: ["SendMessages"]
# Command is automatically disabled when the TicketClaims is disabled.
-
-
OpenTicket:
Command: "ticket"
Description: "Create a support ticket."
Permission: ["SendMessages"]
Enabled: true
-TicketRename:
- Command: "rename"
- Description: "Rename a support ticket."
- Permission: ["SendMessages"]
- Enabled: true
-
-
AddUser:
Command: "add"
Description: "Add a user to a support ticket."
Permission: ["SendMessages"]
Enabled: true
-
-ForceAddUser:
- Command: "forceadd"
- Description: "Add a user to a support ticket by bypassing the invite system."
- Permission: ["SendMessages"]
- Enabled: true
-
-RemoveUser:
- Command: "remove"
- Description: "Remove a user from a support ticket."
- Permission: ["SendMessages"]
- Enabled: true
CloseTicket:
Command: "close"
@@ -76,6 +55,28 @@ CloseTicket:
Permission: ["SendMessages"]
Enabled: true
+TicketManage:
+ Command: "ticketmanage"
+ Description: "Manage an existing support ticket."
+ Permission: ["SendMessages"]
+ Enabled: true
+
+ Close:
+ Command: "close"
+ Description: "Close a support ticket."
+
+ ForceAdd:
+ Command: "forceadd"
+ Description: "Add a user to a support ticket by bypassing the invite system."
+
+ ForceRemove:
+ Command: "forceremove"
+ Description: "Remove a user from a support ticket by bypassing the normal restrictions."
+
+ Rename:
+ Command: "rename"
+ Description: "Rename a support ticket by bypassing the normal restrictions."
+
Suggestion:
Command: "suggest"
Description: "Create a suggestion."
@@ -98,7 +99,7 @@ Translate:
Command: "translate"
Description: "Translate something to english"
Permission: ["SendMessages"]
- Enabled: false
+ Enabled: true
UserInfo:
Command: "userinfo"
@@ -164,3 +165,8 @@ HelpCommandDesc: "Get a list of all the commands."
EmbedCommandDesc: "Send a message as an embed."
SuggestCommandDesc: "Suggest a feature for the server."
UserInfoCommandDesc: "Get info about a user."
+TicketManageDesc: "Manage an existing support ticket."
+TicketManageCloseDesc: "Close a support ticket."
+TicketManageForceAddDesc: "Add a user to a support ticket by bypassing the invite system."
+TicketManageForceRemoveDesc: "Remove a user from a support ticket by bypassing the normal restrictions."
+TicketManageForceRenameDesc: "Rename a support ticket by bypassing the normal restrictions."
\ No newline at end of file
diff --git a/Configs/supportbot-ai.yml b/Configs/supportbot-ai.yml
index 43a298c..1be9da0 100644
--- a/Configs/supportbot-ai.yml
+++ b/Configs/supportbot-ai.yml
@@ -10,7 +10,7 @@ Enabled: true
General:
Name: "SupportBot AI"
# What would you like to call your AI
- Model: "gpt-4o-mini"
+ Model: "gpt-5.4"
# You can see a list of OpenAI Models here: https://platform.openai.com/docs/models
Tokens: 400
diff --git a/Configs/supportbot.yml b/Configs/supportbot.yml
index d3243a9..e42461e 100644
--- a/Configs/supportbot.yml
+++ b/Configs/supportbot.yml
@@ -13,7 +13,7 @@
#
# SupportBot Configuration (supportbot.yml)
-SupportBot_Version: "v8.5"
+SupportBot_Version: "v26.0.0-prerelease-1"
# -------------------------
# General
@@ -21,7 +21,7 @@ SupportBot_Version: "v8.5"
General:
Name: "SupportBot"
- Token: "BBOT_TOKEN_HERE"
+ Token: "BOT_TOKEN"
Addons:
Enabled: true
@@ -84,11 +84,11 @@ Roles:
Welcome:
Enabled: true
- Channel: "WELCOME_CHANNEL_ID"
+ Channel: "CHANNEL_ID"
Leave:
Enabled: true
- Channel: "LEAVE_CHANNEL_ID "
+ Channel: "CHANNEL_ID"
# ----------------------------
# Suggestions
@@ -97,7 +97,7 @@ Leave:
Suggestions:
Mode: "Channel" # Channel or Forum
ForumChannel: "FORUM_CHANNEL_ID" # A forum channel, disabled if mode is set to Channel.
- Channel: "SUGGESTIONS_CHANNEL_ID" # A text channel, disabled if mode is set to Forum.
+ Channel: "CHANNEL_ID" # A text channel, disabled if mode is set to Forum.
UpvoteEmoji: "✅"
DownvoteEmoji: "❌"
@@ -141,30 +141,34 @@ Suggestions:
VoiceTickets:
Name: "%username%'s VC"
- Category: "VC_CATEGORY_ID"
+ Category: "CATEGORY_ID"
Ticket:
- TicketType: "channels"
+ TicketType: "threads"
# Available TicketTypes:
# channels - Tickets will be created as channels in the TicketChannelsCategory
# threads - Tickets will be created as threads through the "TicketHome" channel.
- TicketHome: "TICKET_HOME_CHANNEL_ID"
+ UserManagement:
+ DMOnAdd: true # DM the user when they are added to the ticket
+ DMOnRemove: true # DM the user when they are removed from the ticket
+ DMTranscript: true # DM the user a transcript of the ticket when it is closed
+
+ TicketHome: "CHANNEL_ID"
# What is TicketHome?
# - Channel where the Ticket creation panel is sent.
# - Channel where ticket threads are made. (only if ticket threads is enabled above.)
# For TicketType: channels
- TicketChannelsCategory: "TICKET_CHANNELS_CATEGORY_ID"
- TicketChannelsCategory2: "TICKET_CHANNELS_CATEGORY_ID_2" # This will be used if the first category has reached the max channel limit.
- TicketArchiveCategory: "TICKET_ARCHIVE_CATEGORY_ID"
+ TicketChannelsCategory: "CATEGORY_ID"
+ TicketChannelsCategory2: "CATEGORY_ID" # This will be used if the first category has reached the max channel limit.
+ TicketArchiveCategory: "CATEGORY_ID"
# ---
Channel: "ticket-" # The prefix for ticket channel names
TicketsPerUser: 100
- DMTranscripts: true
TicketReason: true
# If ticket reason is set to false, It will not prompt to create a reason
@@ -181,24 +185,12 @@ Ticket:
StaffOnly: true
# Set to false is anyone is able to close a ticket.
- Confirmation_Button: "Confirm Close"
- Confirmation_Emoji: "🗑️"
- Confirmation_Style: "2"
-
- Title: "Are you sure you want to close this ticket?"
-
- # Available Button Styles:
- # 1 - Blurple
- # 2 - Grey
- # 3 - Green
- # 4 - Red
-
- Invites:
+ Invites: # Invites is for ticket threads.
StaffOnly: true
ClaimTickets:
- Enabled: false
- Channel: "CLAIM_TICKETS_CHANNEL_ID"
+ Enabled: true
+ Channel: "CHANNEL_ID"
# Channel where the ticket assignments are sent for staff to claim.
ClaimAcceptTime: 120000
@@ -221,7 +213,7 @@ Ticket:
ReviewSystem:
Enabled: true
- Channel: "REVIEW_CHANNEL_ID"
+ Channel: "CHANNEL_ID"
UseModal: true
Questions:
@@ -232,9 +224,84 @@ Ticket:
- "Have you tried resolving the issue yourself?"
Log:
- TicketDataLog: "TICKKET_DATA_LOG_CHANNEL_ID"
+ TicketDataLog: "CHANNEL_ID"
TickeDataTitle: "Ticket Data"
- TicketBlacklistLog: "TICKET_BLACKLIST_LOG_CHANNEL_ID"
+ TicketBlacklistLog: "CHANNEL_ID"
+
+# -------------------------
+# Ticket Departments
+# -------------------------
+
+ DepartmentSystem:
+ Enabled: true
+ Placeholder: "Choose a ticket department"
+ DefaultDepartment: "general"
+
+ Departments:
+ general:
+ Name: "General Support"
+ Emoji: "🎫"
+ Category: "CATEGORY_ID"
+ Role: "ROLE_ID"
+ ChannelPrefix: "support-"
+ DefaultPriority: "normal"
+ Questions:
+ - "What do you need help with?"
+ - "When did the issue start?"
+
+ purchase:
+ Name: "Purchase Support"
+ Emoji: "💳"
+ Category: "CATEGORY_ID"
+ Role: "ROLE_ID"
+ ChannelPrefix: "purchase-"
+ DefaultPriority: "high"
+ Questions:
+ - "What is your order ID?"
+ - "What product did you purchase?"
+ - "What payment issue are you having?"
+
+ reports:
+ Name: "Player Report"
+ Emoji: "🚨"
+ Category: "CATEGORY_ID"
+ Role: "ROLE_ID"
+ ChannelPrefix: "report-"
+ DefaultPriority: "normal"
+ Questions:
+ - "Who are you reporting?"
+ - "What happened?"
+ - "Do you have evidence?"
+
+ appeals:
+ Name: "Ban Appeals"
+ Emoji: "⚖️"
+ Category: "CATEGORY_ID"
+ Role: "ROLE_ID"
+ ChannelPrefix: "appeals-"
+ DefaultPriority: "low"
+ Questions:
+ - "What punishment are you appealing?"
+ - "Why should it be reviewed?"
+ - "Anything else staff should know?"
+
+ PrioritySystem:
+ Enabled: true
+ DefaultPriority: "Normal"
+ AllowUsersToChooseOnOpen: true
+ Priorities:
+ low:
+ Name: "Low"
+ Emoji: "🟢"
+ normal:
+ Name: "Normal"
+ Emoji: "🟡"
+ high:
+ Name: "High"
+ Emoji: "🟠"
+ urgent:
+ Name: "Urgent"
+ Emoji: "🔴"
# -------------------------
# Buttons
@@ -286,12 +353,12 @@ SelectMenus:
# -------------------------
MessageDelete:
- Channel: "LOG_CHANNEL_ID"
+ Channel: "CHANNEL_ID"
Colour: "#d92525"
MessageUpdate:
- Channel: "LOG_CHANNEL_ID"
+ Channel: "CHANNEL_ID"
Colour: "#f2a024"
Translate:
- TranslateLog: "LOG_CHANNEL_ID"
+ TranslateLog: "CHANNEL_ID"
diff --git a/Configs/ticket-panel.yml b/Configs/ticket-panel.yml
index b14eadf..27eb938 100644
--- a/Configs/ticket-panel.yml
+++ b/Configs/ticket-panel.yml
@@ -19,17 +19,11 @@
Panel: true
-# ----------------------------------------
-# Ticket Panel Message Configuration #2b2d31
-# ----------------------------------------
-
Style:
Title: "## SupportBot Ticket Creation"
Description: "Click on the button to file a `Ticket` and our team will try their best to assist you in the best way we can!"
Image: "https://i.imgur.com/dejQvkg.png"
Color: "#45b463"
- # Leave Color empty to remove the color from the embed/container.
-
Layout: "3"
# Available Layout Styles
@@ -40,7 +34,8 @@ Style:
Button:
Text: "Create Ticket"
Emoji: "🎫"
- Color: "3"
+ Color: "2"
+ CustomId: "createticket"
# Available Button Styles
# 1 - Blurple
@@ -48,15 +43,22 @@ Button:
# 3 - Red
# 4 - Green
+DepartmentMenu:
+ CustomId: "ticketdepartmentselect"
+ Placeholder: "Choose a ticket department"
+ DefaultDescription: "Open a support ticket"
+ DefaultEmoji: "🎫"
+
Settings:
Image: true
-
- # Text and divider will appear above the image.
TopTitle: true
TopDivider: true
-
- # Text and divider will appear below the image.
BottomTitle: false
BottomDivider: false
- # Note: If Image is set to false, the bottom and top settings will look the samme.
\ No newline at end of file
+Messages:
+ NotConfigured: "Ticket Panel is not set up. Please configure it in `/Configs/ticket-panel.yml`."
+ InvalidChannel: "TicketHome channel not found. Please check your config file."
+ PanelSent: "Ticket panel message has been sent!"
+ PanelSentWithDepartments: "Ticket panel message with department selection has been sent!"
+ SendError: "An error occurred while sending the ticket panel message."
diff --git a/Events/interactionCreate.js b/Events/interactionCreate.js
index a52dd32..62c66eb 100644
--- a/Events/interactionCreate.js
+++ b/Events/interactionCreate.js
@@ -2,7 +2,9 @@ const fs = require("fs");
const Discord = require("discord.js");
const yaml = require("js-yaml");
-const supportbot = yaml.load(fs.readFileSync("./Configs/supportbot.yml", "utf8"));
+const supportbot = yaml.load(
+ fs.readFileSync("./Configs/supportbot.yml", "utf8"),
+);
const cmdconfig = yaml.load(fs.readFileSync("./Configs/commands.yml", "utf8"));
const msgconfig = yaml.load(fs.readFileSync("./Configs/messages.yml", "utf8"));
@@ -10,10 +12,9 @@ const Event = require("../Structures/Event.js");
const db = require("../Structures/Database.js");
module.exports = new Event("interactionCreate", async (client, interaction) => {
-
if (interaction.type === Discord.InteractionType.ApplicationCommand) {
const command = client.commands.find(
- (cmd) => cmd.name.toLowerCase() === interaction.commandName
+ (cmd) => cmd.name.toLowerCase() === interaction.commandName,
);
if (interaction.user.bot || !interaction.guild) return;
@@ -32,7 +33,7 @@ module.exports = new Event("interactionCreate", async (client, interaction) => {
const ValidPerms = new Discord.EmbedBuilder()
.setDescription(
- "> :x: `Invalid Permissions` Do you have the correct permissions to execute this command?"
+ "> :x: `Invalid Permissions` Do you have the correct permissions to execute this command?",
)
.setColor(supportbot.Embed.Colours.Error);
@@ -43,7 +44,10 @@ module.exports = new Event("interactionCreate", async (client, interaction) => {
});
try {
- if (command.name === cmdconfig.OpenTicket.Command && supportbot.Ticket.TicketReason) {
+ if (
+ command.name === cmdconfig.OpenTicket.Command &&
+ supportbot.Ticket.TicketReason
+ ) {
const modal = new Discord.ModalBuilder()
.setCustomId("ticketReasonModal")
.setTitle("Ticket Reason")
@@ -53,13 +57,18 @@ module.exports = new Event("interactionCreate", async (client, interaction) => {
.setCustomId("reasonInput")
.setLabel("Reason")
.setStyle(Discord.TextInputStyle.Paragraph)
- .setRequired(true)
- )
+ .setRequired(true),
+ ),
);
await interaction.showModal(modal);
} else {
- console.log("\u001b[32m", "[Executed]", "\u001b[37;1m", `${interaction.user.username} has executed ${command.name}`);
+ console.log(
+ "\u001b[32m",
+ "[Executed]",
+ "\u001b[37;1m",
+ `${interaction.user.username} has executed ${command.name}`,
+ );
await command.run(interaction);
}
} catch (error) {
@@ -72,16 +81,29 @@ module.exports = new Event("interactionCreate", async (client, interaction) => {
}
if (interaction.isModalSubmit()) {
- if (interaction.customId === "ticketReasonModal") {
+ if (interaction.customId.startsWith("ticketReasonModal")) {
const reason = interaction.fields.getTextInputValue("reasonInput");
- interaction.customId = `createticket-${reason}`;
+ const department = interaction.customId.includes(":")
+ ? interaction.customId.split(":")[1]
+ : supportbot.Ticket.DepartmentSystem?.DefaultDepartment || "general";
+
const cmd = client.commands.get(cmdconfig.OpenTicket.Command);
- if (cmd) {
- interaction.reason = reason;
- await cmd.run(interaction);
- } else {
- console.error("Command not found.");
+ if (!cmd) {
+ return interaction.reply({
+ content: "Open ticket command not found.",
+ flags: Discord.MessageFlags.Ephemeral,
+ });
}
+
+ interaction.reason = reason;
+ interaction.department = department;
+ interaction.priority =
+ supportbot.Ticket.DepartmentSystem?.Departments?.[department]
+ ?.DefaultPriority ||
+ supportbot.Ticket.PrioritySystem?.DefaultPriority ||
+ "medium";
+
+ return await cmd.run(interaction);
}
}
@@ -90,42 +112,81 @@ module.exports = new Event("interactionCreate", async (client, interaction) => {
const ticketChannelId = interaction.channel.id;
let ticketChannel;
if (supportbot.Ticket.TicketType === "threads") {
- ticketChannel = interaction.guild.channels.cache.get(ticketChannelId);
+ ticketChannel = interaction.guild.channels.cache.get(ticketChannelId);
} else if (supportbot.Ticket.TicketType === "channels") {
- ticketChannel = interaction.guild.channels.cache.get(ticketChannelId);
+ ticketChannel = interaction.guild.channels.cache.get(ticketChannelId);
}
if (!ticketChannel) {
- return interaction.reply({
- content: "Ticket channel not found.",
- flags: Discord.MessageFlags.Ephemeral,
- });
+ return interaction.reply({
+ content: "Ticket channel not found.",
+ flags: Discord.MessageFlags.Ephemeral,
+ });
}
try {
- await ticketChannel.setName(newTicketName);
- const renamedsuccess = new Discord.EmbedBuilder()
- .setDescription(msgconfig.Ticket.RenamedSuccess)
- .setColor(supportbot.Embed.Colours.Success);
- return interaction.reply({
- embeds: [renamedsuccess],
- flags: Discord.MessageFlags.Ephemeral,
- });
+ await ticketChannel.setName(newTicketName);
+ const renamedsuccess = new Discord.EmbedBuilder()
+ .setDescription(msgconfig.Ticket.RenamedSuccess)
+ .setColor(supportbot.Embed.Colours.Success);
+ return interaction.reply({
+ embeds: [renamedsuccess],
+ flags: Discord.MessageFlags.Ephemeral,
+ });
} catch (error) {
- console.error("Error renaming the ticket:", error);
- return interaction.reply({
- content: "There was an error renaming the ticket.",
- flags: Discord.MessageFlags.Ephemeral,
- });
+ console.error("Error renaming the ticket:", error);
+ return interaction.reply({
+ content: "There was an error renaming the ticket.",
+ flags: Discord.MessageFlags.Ephemeral,
+ });
}
-}
+ }
if (interaction.isStringSelectMenu()) {
+ if (interaction.customId === "ticketdepartmentselect") {
+ const selectedDepartment = interaction.values[0];
+
+ if (supportbot.Ticket.TicketReason) {
+ const modal = new Discord.ModalBuilder()
+ .setCustomId(`ticketReasonModal:${selectedDepartment}`)
+ .setTitle("Ticket Reason")
+ .addComponents(
+ new Discord.ActionRowBuilder().addComponents(
+ new Discord.TextInputBuilder()
+ .setCustomId("reasonInput")
+ .setLabel("Reason")
+ .setStyle(Discord.TextInputStyle.Paragraph)
+ .setRequired(true),
+ ),
+ );
+
+ return await interaction.showModal(modal);
+ }
+
+ const cmd = client.commands.get(cmdconfig.OpenTicket.Command);
+ if (!cmd) {
+ return interaction.reply({
+ content: "Open ticket command not found.",
+ flags: Discord.MessageFlags.Ephemeral,
+ });
+ }
+
+ interaction.department = selectedDepartment;
+ interaction.priority =
+ supportbot.Ticket.DepartmentSystem?.Departments?.[selectedDepartment]
+ ?.DefaultPriority ||
+ supportbot.Ticket.PrioritySystem?.DefaultPriority ||
+ "medium";
+
+ return await cmd.run(interaction);
+ }
+
if (interaction.customId === "ticketcontrolpanel") {
const selectedOption = interaction.values[0];
const { getRole, getChannel } = interaction.client;
switch (selectedOption) {
-
case "ticketclose":
- const closeCommand = client.commands.get(cmdconfig.CloseTicket.Command);
+ const closeCommand = client.commands.get(
+ cmdconfig.CloseTicket.Command,
+ );
if (closeCommand) {
await closeCommand.run(interaction);
} else {
@@ -133,176 +194,206 @@ module.exports = new Event("interactionCreate", async (client, interaction) => {
}
break;
- case "supportvc":
- const ticketInfo = db.getTicket(interaction.channel.id);
- if (!ticketInfo) {
- return interaction.reply({
- content: "⚠️ This channel isn’t a valid ticket.",
- flags: Discord.MessageFlags.Ephemeral
- });
- }
+ case "supportvc":
+ const ticketInfo = db.getTicket(interaction.channel.id);
+ if (!ticketInfo) {
+ return interaction.reply({
+ content: "⚠️ This channel isn’t a valid ticket.",
+ flags: Discord.MessageFlags.Ephemeral,
+ });
+ }
- const user = interaction.guild.members.cache.get(ticketInfo.user_id) || ticketInfo.user_id;
- const username = user?.user?.username || "Unknown User";
+ const user =
+ interaction.guild.members.cache.get(ticketInfo.user_id) ||
+ ticketInfo.user_id;
+ const username = user?.user?.username || "Unknown User";
- const VCcategory = interaction.guild.channels.cache.find(
- (c) => c.name === supportbot.VoiceTickets.Category || c.id === supportbot.VoiceTickets.Category
+ const VCcategory = interaction.guild.channels.cache.find(
+ (c) =>
+ c.name === supportbot.VoiceTickets.Category ||
+ c.id === supportbot.VoiceTickets.Category,
+ );
+
+ if (!VCcategory) {
+ return interaction.reply({
+ content: "⚠️ The Support VC category does not exist!",
+ flags: Discord.MessageFlags.Ephemeral,
+ });
+ }
+
+ const SupportVoice = await interaction.guild.channels.create({
+ name: supportbot.VoiceTickets.Name.replace(/%username%/g, username),
+ type: Discord.ChannelType.GuildVoice,
+ parent: VCcategory,
+ permissionOverwrites: [
+ {
+ id: interaction.guild.id,
+ deny: [Discord.PermissionsBitField.Flags.Connect],
+ },
+ {
+ id: user.id || user, // works if user is a Member or just ID
+ allow: [
+ Discord.PermissionsBitField.Flags.Connect,
+ Discord.PermissionsBitField.Flags.Speak,
+ Discord.PermissionsBitField.Flags.ViewChannel,
+ ],
+ },
+ ],
+ });
+
+ db.updateTicketVoice(ticketInfo.ticket_id, SupportVoice.id);
+
+ const vcmadeembed = new Discord.EmbedBuilder()
+ .setDescription(`> **Support VC Created:** <#${SupportVoice.id}>`)
+ .setColor(supportbot.Embed.Colours.Success);
+
+ const VCDeleteButton = new Discord.ButtonBuilder()
+ .setCustomId("deleteSupportVC")
+ .setLabel(supportbot.Buttons.Voice.TicketDeleteText)
+ .setEmoji(supportbot.Buttons.General.Delete)
+ .setStyle(supportbot.Buttons.General.DeleteStyle);
+
+ const VCDeleteRow = new Discord.ActionRowBuilder().addComponents(
+ VCDeleteButton,
+ );
+
+ await interaction.reply({
+ embeds: [vcmadeembed],
+ components: [VCDeleteRow],
+ });
+ break;
+
+ case "archiveticket":
+ const arcembed = new Discord.EmbedBuilder()
+ .setDescription(msgconfig.Ticket.TicketArchived)
+ .setColor(supportbot.Embed.Colours.Success);
+ const CloseButton = new Discord.ButtonBuilder()
+ .setCustomId("confirmCloseTicket")
+ .setLabel(supportbot.Ticket.Close.Confirmation_Button)
+ .setEmoji(supportbot.Ticket.Close.Confirmation_Emoji)
+ .setStyle(supportbot.Ticket.Close.Confirmation_Style);
+ const unarchiveButton = new Discord.ButtonBuilder()
+ .setCustomId("unarchiveTicket")
+ .setLabel(supportbot.Buttons.Tickets.Unarchive)
+ .setEmoji(supportbot.Buttons.Tickets.Unarchive_Emoji)
+ .setStyle(supportbot.Buttons.Tickets.Unarchive_Style);
+ const ArchivedOptions = new Discord.ActionRowBuilder().addComponents(
+ CloseButton,
+ unarchiveButton,
+ );
+ await interaction.channel.send({
+ embeds: [arcembed],
+ components: [ArchivedOptions],
+ });
+ if (supportbot.Ticket.TicketType === "threads") {
+ await interaction.channel.setArchived(true);
+ }
+ if (supportbot.Ticket.TicketType === "channels") {
+ const { getRole, getChannel } = interaction.client;
+ await interaction.channel.setParent(
+ supportbot.Ticket.TicketArchiveCategory,
);
-
- if (!VCcategory) {
- return interaction.reply({
- content: "⚠️ The Support VC category does not exist!",
- flags: Discord.MessageFlags.Ephemeral,
- });
- }
-
- const SupportVoice = await interaction.guild.channels.create({
- name: supportbot.VoiceTickets.Name.replace(/%username%/g, username),
- type: Discord.ChannelType.GuildVoice,
- parent: VCcategory,
- permissionOverwrites: [
- {
- id: interaction.guild.id,
- deny: [Discord.PermissionsBitField.Flags.Connect]
- },
- {
- id: user.id || user, // works if user is a Member or just ID
- allow: [
- Discord.PermissionsBitField.Flags.Connect,
- Discord.PermissionsBitField.Flags.Speak,
- Discord.PermissionsBitField.Flags.ViewChannel,
- ],
- },
- ],
- });
-
- db.updateTicketVoice(ticketInfo.ticket_id, SupportVoice.id);
-
- const vcmadeembed = new Discord.EmbedBuilder()
- .setDescription(`> **Support VC Created:** <#${SupportVoice.id}>`)
- .setColor(supportbot.Embed.Colours.Success);
-
- const VCDeleteButton = new Discord.ButtonBuilder()
- .setCustomId("deleteSupportVC")
- .setLabel(supportbot.Buttons.Voice.TicketDeleteText)
- .setEmoji(supportbot.Buttons.General.Delete)
- .setStyle(supportbot.Buttons.General.DeleteStyle);
-
- const VCDeleteRow = new Discord.ActionRowBuilder().addComponents(VCDeleteButton);
-
- await interaction.reply({
- embeds: [vcmadeembed],
- components: [VCDeleteRow],
- });
- break;
-
- case "archiveticket":
- const arcembed = new Discord.EmbedBuilder()
- .setDescription(msgconfig.Ticket.TicketArchived)
- .setColor(supportbot.Embed.Colours.Success);
- const CloseButton = new Discord.ButtonBuilder()
- .setCustomId("confirmCloseTicket")
- .setLabel(supportbot.Ticket.Close.Confirmation_Button)
- .setEmoji(supportbot.Ticket.Close.Confirmation_Emoji)
- .setStyle(supportbot.Ticket.Close.Confirmation_Style);
- const unarchiveButton = new Discord.ButtonBuilder()
- .setCustomId("unarchiveTicket")
- .setLabel(supportbot.Buttons.Tickets.Unarchive)
- .setEmoji(supportbot.Buttons.Tickets.Unarchive_Emoji)
- .setStyle(supportbot.Buttons.Tickets.Unarchive_Style);
- const ArchivedOptions = new Discord.ActionRowBuilder()
- .addComponents(CloseButton, unarchiveButton);
- await interaction.channel.send({
- embeds: [arcembed],
- components: [ArchivedOptions],
+ const tickets = db.getAllTickets();
+ const ticketInfo = tickets.find(
+ (t) => t.ticket_id === interaction.channel.id,
+ );
+ if (ticketInfo) {
+ const user =
+ interaction.guild.members.cache.get(ticketInfo.user_id) ||
+ ticketInfo.user_id;
+ const claimedBy =
+ interaction.guild.members.cache.get(ticketInfo.claimed_by) ||
+ ticketInfo.claimed_by;
+ const Admin = await interaction.guild.roles.fetch(
+ supportbot.Roles.StaffMember.Admin,
+ );
+ const Staff = await interaction.guild.roles.fetch(
+ supportbot.Roles.StaffMember.Staff,
+ );
+ if (user && user.id) {
+ await interaction.channel.permissionOverwrites.edit(user.id, {
+ ViewChannel: false,
+ SendMessages: false,
});
- if (supportbot.Ticket.TicketType === "threads") {
- await interaction.channel.setArchived(true);
+ }
+ if (supportbot.ClaimTickets === true) {
+ if (claimedBy && claimedBy.id) {
+ await interaction.channel.permissionOverwrites.edit(
+ claimedBy.id,
+ {
+ ViewChannel: true,
+ SendMessages: true,
+ ReadMessageHistory: true,
+ },
+ );
}
- if (supportbot.Ticket.TicketType === "channels") {
- const { getRole, getChannel } = interaction.client;
- await interaction.channel.setParent(supportbot.Ticket.TicketArchiveCategory);
- const tickets = db.getAllTickets();
- const ticketInfo = tickets.find(t => t.ticket_id === interaction.channel.id);
- if (ticketInfo) {
- const user = interaction.guild.members.cache.get(ticketInfo.user_id) || ticketInfo.user_id;
- const claimedBy = interaction.guild.members.cache.get(ticketInfo.claimed_by) || ticketInfo.claimed_by;
- const Admin = await interaction.guild.roles.fetch(supportbot.Roles.StaffMember.Admin);
- const Staff = await interaction.guild.roles.fetch(supportbot.Roles.StaffMember.Staff);
- if (user && user.id) {
- await interaction.channel.permissionOverwrites.edit(user.id, {
- ViewChannel: false,
- SendMessages: false,
- });
- }
- if (supportbot.ClaimTickets === true) {
- if (claimedBy && claimedBy.id) {
- await interaction.channel.permissionOverwrites.edit(claimedBy.id, {
- ViewChannel: true,
- SendMessages: true,
- ReadMessageHistory: true,
- });
- }
- }
- if (supportbot.ClaimTickets === false) {
- if (Staff && Staff.id) {
- await interaction.channel.permissionOverwrites.edit(claimedBy.id, {
- ViewChannel: true,
- SendMessages: true,
- ReadMessageHistory: true,
- });
- }
- }
- if (Admin && Admin.id) {
- await interaction.channel.permissionOverwrites.edit(Admin.id, {
- ViewChannel: true,
- SendMessages: true,
- ReadMessageHistory: true,
- });
- }
- await interaction.channel.permissionOverwrites.edit(interaction.guild.roles.everyone.id, {
- ViewChannel: false,
- SendMessages: false,
- ReadMessageHistory: false,
- });
- }
+ }
+ if (supportbot.ClaimTickets === false) {
+ if (Staff && Staff.id) {
+ await interaction.channel.permissionOverwrites.edit(
+ claimedBy.id,
+ {
+ ViewChannel: true,
+ SendMessages: true,
+ ReadMessageHistory: true,
+ },
+ );
}
- break;
+ }
+ if (Admin && Admin.id) {
+ await interaction.channel.permissionOverwrites.edit(Admin.id, {
+ ViewChannel: true,
+ SendMessages: true,
+ ReadMessageHistory: true,
+ });
+ }
+ await interaction.channel.permissionOverwrites.edit(
+ interaction.guild.roles.everyone.id,
+ {
+ ViewChannel: false,
+ SendMessages: false,
+ ReadMessageHistory: false,
+ },
+ );
+ }
+ }
+ break;
- case "lockticket":
- const lockembed = new Discord.EmbedBuilder()
- .setDescription(msgconfig.Ticket.TicketLocked)
- .setColor(supportbot.Embed.Colours.Success);
- const unlockButton = new Discord.ButtonBuilder()
- .setCustomId("unlockTicket")
- .setLabel(supportbot.Buttons.Tickets.Unlock)
- .setEmoji(supportbot.Buttons.Tickets.Unlock_Emoji)
- .setStyle(supportbot.Buttons.Tickets.Unlock_Style);
- const LockedOptions = new Discord.ActionRowBuilder()
- .addComponents(unlockButton);
- await interaction.channel.send({
- embeds: [lockembed],
- components: [LockedOptions],
- ephemeral: false,
- });
- if (supportbot.Ticket.TicketType === "threads") {
- await interaction.channel.setLocked(true);
- }
- break;
+ case "lockticket":
+ const lockembed = new Discord.EmbedBuilder()
+ .setDescription(msgconfig.Ticket.TicketLocked)
+ .setColor(supportbot.Embed.Colours.Success);
+ const unlockButton = new Discord.ButtonBuilder()
+ .setCustomId("unlockTicket")
+ .setLabel(supportbot.Buttons.Tickets.Unlock)
+ .setEmoji(supportbot.Buttons.Tickets.Unlock_Emoji)
+ .setStyle(supportbot.Buttons.Tickets.Unlock_Style);
+ const LockedOptions = new Discord.ActionRowBuilder().addComponents(
+ unlockButton,
+ );
+ await interaction.channel.send({
+ embeds: [lockembed],
+ components: [LockedOptions],
+ ephemeral: false,
+ });
+ if (supportbot.Ticket.TicketType === "threads") {
+ await interaction.channel.setLocked(true);
+ }
+ break;
+
+ case "unlockTicket":
+ const unlockEmbed = new Discord.EmbedBuilder()
+ .setDescription(msgconfig.Ticket.TicketUnlocked)
+ .setColor(supportbot.Embed.Colours.Success);
+ await interaction.channel.send({
+ embeds: [unlockEmbed],
+ ephemeral: false,
+ });
+ if (supportbot.Ticket.TicketType === "threads") {
+ await interaction.channel.setLocked(false);
+ }
+ break;
- case "unlockTicket":
- const unlockEmbed = new Discord.EmbedBuilder()
- .setDescription(msgconfig.Ticket.TicketUnlocked)
- .setColor(supportbot.Embed.Colours.Success);
- await interaction.channel.send({
- embeds: [unlockEmbed],
- ephemeral: false,
- });
- if (supportbot.Ticket.TicketType === "threads") {
- await interaction.channel.setLocked(false);
- }
- break;
-
case "enableinvites":
await handleInvites(interaction, true);
break;
@@ -313,34 +404,39 @@ module.exports = new Event("interactionCreate", async (client, interaction) => {
case "renameticket":
const modal = new Discord.ModalBuilder()
- .setCustomId("renameTicketModal")
- .setTitle("Rename Channel")
- .addComponents(
- new Discord.ActionRowBuilder()
- .addComponents(
+ .setCustomId("renameTicketModal")
+ .setTitle("Rename Channel")
+ .addComponents(
+ new Discord.ActionRowBuilder().addComponents(
new Discord.TextInputBuilder()
.setCustomId("renameTicket")
.setLabel("New Channel Name")
.setStyle(Discord.TextInputStyle.Short)
- .setRequired(true)
- )
- );
- await interaction.showModal(modal);
- break;
+ .setRequired(true),
+ ),
+ );
+ await interaction.showModal(modal);
+ break;
}
}
}
- if (interaction.customId === 'deleteSupportVC') {
+ if (interaction.customId === "deleteSupportVC") {
const ticketData = db.getAllTickets();
- const ticketInfo = ticketData.find(t => t.ticket_id === interaction.channel.id);
+ const ticketInfo = ticketData.find(
+ (t) => t.ticket_id === interaction.channel.id,
+ );
if (ticketInfo && ticketInfo.voiceChannelId) {
try {
- const voiceChannel = await interaction.guild.channels.fetch(ticketInfo.voiceChannelId).catch(() => null);
+ const voiceChannel = await interaction.guild.channels
+ .fetch(ticketInfo.voiceChannelId)
+ .catch(() => null);
if (voiceChannel) {
await voiceChannel.delete();
const successEmbed = new Discord.EmbedBuilder()
- .setDescription('✅ Support voice channel has been successfully deleted!')
+ .setDescription(
+ "✅ Support voice channel has been successfully deleted!",
+ )
.setColor(supportbot.Embed.Colours.Success);
await interaction.reply({
embeds: [successEmbed],
@@ -348,7 +444,9 @@ module.exports = new Event("interactionCreate", async (client, interaction) => {
});
} else {
const errorEmbed = new Discord.EmbedBuilder()
- .setDescription('❌ The associated voice channel could not be found.')
+ .setDescription(
+ "❌ The associated voice channel could not be found.",
+ )
.setColor(supportbot.Embed.Colours.Error);
await interaction.reply({
embeds: [errorEmbed],
@@ -356,9 +454,11 @@ module.exports = new Event("interactionCreate", async (client, interaction) => {
});
}
} catch (error) {
- console.error('Error deleting the Voice Channel:', error);
+ console.error("Error deleting the Voice Channel:", error);
const errorEmbed = new Discord.EmbedBuilder()
- .setDescription('❌ **There was an error trying to delete this voice channel. Please try again later.**')
+ .setDescription(
+ "❌ **There was an error trying to delete this voice channel. Please try again later.**",
+ )
.setColor(supportbot.Embed.Colours.Error);
await interaction.reply({
embeds: [errorEmbed],
@@ -367,7 +467,9 @@ module.exports = new Event("interactionCreate", async (client, interaction) => {
}
} else {
const errorEmbed = new Discord.EmbedBuilder()
- .setDescription('⚠️ This button is not linked to a valid voice channel.')
+ .setDescription(
+ "⚠️ This button is not linked to a valid voice channel.",
+ )
.setColor(supportbot.Embed.Colours.Warn);
await interaction.reply({
embeds: [errorEmbed],
@@ -383,14 +485,26 @@ module.exports = new Event("interactionCreate", async (client, interaction) => {
}
if (supportbot.Ticket.TicketType === "channels") {
const { getRole, getChannel } = interaction.client;
- await interaction.channel.setParent(supportbot.Ticket.TicketChannelsCategory);
+ await interaction.channel.setParent(
+ supportbot.Ticket.TicketChannelsCategory,
+ );
const ticketData = db.getAllTickets();
- const ticketInfo = ticketData.find(t => t.ticket_id === interaction.channel.id);
+ const ticketInfo = ticketData.find(
+ (t) => t.ticket_id === interaction.channel.id,
+ );
if (ticketInfo) {
- const user = interaction.guild.members.cache.get(ticketInfo.user_id) || ticketInfo.user_id;
- const claimedBy = interaction.guild.members.cache.get(ticketInfo.claimed_by) || ticketInfo.claimed_by;
- const Admin = await interaction.guild.roles.fetch(supportbot.Roles.StaffMember.Admin);
- const Staff = await interaction.guild.roles.fetch(supportbot.Roles.StaffMember.Staff);
+ const user =
+ interaction.guild.members.cache.get(ticketInfo.user_id) ||
+ ticketInfo.user_id;
+ const claimedBy =
+ interaction.guild.members.cache.get(ticketInfo.claimed_by) ||
+ ticketInfo.claimed_by;
+ const Admin = await interaction.guild.roles.fetch(
+ supportbot.Roles.StaffMember.Admin,
+ );
+ const Staff = await interaction.guild.roles.fetch(
+ supportbot.Roles.StaffMember.Staff,
+ );
if (user && user.id) {
await interaction.channel.permissionOverwrites.edit(user.id, {
ViewChannel: true,
@@ -400,21 +514,27 @@ module.exports = new Event("interactionCreate", async (client, interaction) => {
}
if (supportbot.ClaimTickets === true) {
if (claimedBy && claimedBy.id) {
- await interaction.channel.permissionOverwrites.edit(claimedBy.id, {
- ViewChannel: true,
- SendMessages: true,
- ReadMessageHistory: true,
- });
- }
+ await interaction.channel.permissionOverwrites.edit(
+ claimedBy.id,
+ {
+ ViewChannel: true,
+ SendMessages: true,
+ ReadMessageHistory: true,
+ },
+ );
+ }
}
if (supportbot.ClaimTickets === false) {
if (Staff && Staff.id) {
- await interaction.channel.permissionOverwrites.edit(claimedBy.id, {
- ViewChannel: true,
- SendMessages: true,
- ReadMessageHistory: true,
- });
- }
+ await interaction.channel.permissionOverwrites.edit(
+ claimedBy.id,
+ {
+ ViewChannel: true,
+ SendMessages: true,
+ ReadMessageHistory: true,
+ },
+ );
+ }
}
if (Admin && Admin.id) {
await interaction.channel.permissionOverwrites.edit(Admin.id, {
@@ -423,11 +543,14 @@ module.exports = new Event("interactionCreate", async (client, interaction) => {
ReadMessageHistory: true,
});
}
- await interaction.channel.permissionOverwrites.edit(interaction.guild.roles.everyone.id, {
- ViewChannel: false,
- SendMessages: false,
- ReadMessageHistory: false,
- });
+ await interaction.channel.permissionOverwrites.edit(
+ interaction.guild.roles.everyone.id,
+ {
+ ViewChannel: false,
+ SendMessages: false,
+ ReadMessageHistory: false,
+ },
+ );
}
}
const unrcembed = new Discord.EmbedBuilder()
@@ -450,8 +573,8 @@ module.exports = new Event("interactionCreate", async (client, interaction) => {
.setCustomId("reasonInput")
.setLabel("Reason")
.setStyle(Discord.TextInputStyle.Paragraph)
- .setRequired(true)
- )
+ .setRequired(true),
+ ),
);
await interaction.showModal(modal);
} else {
@@ -480,8 +603,8 @@ module.exports = new Event("interactionCreate", async (client, interaction) => {
.setCustomId("reasonInput")
.setLabel("Reason")
.setStyle(Discord.TextInputStyle.Paragraph)
- .setRequired(true)
- )
+ .setRequired(true),
+ ),
);
await interaction.showModal(modal);
} else {
@@ -497,7 +620,8 @@ module.exports = new Event("interactionCreate", async (client, interaction) => {
if (interaction.customId.startsWith("claimticket-")) {
const ticketChannelId = interaction.customId.split("-")[1];
- const ticketChannel = interaction.guild.channels.cache.get(ticketChannelId);
+ const ticketChannel =
+ interaction.guild.channels.cache.get(ticketChannelId);
if (!ticketChannel) {
return interaction.reply({
content: "Ticket channel not found.",
@@ -506,13 +630,19 @@ module.exports = new Event("interactionCreate", async (client, interaction) => {
}
const { getRole } = interaction.client;
- const Staff = await getRole(supportbot.Roles.StaffMember.Staff, interaction.guild);
- const Admin = await getRole(supportbot.Roles.StaffMember.Admin, interaction.guild);
+ const Staff = await getRole(
+ supportbot.Roles.StaffMember.Staff,
+ interaction.guild,
+ );
+ const Admin = await getRole(
+ supportbot.Roles.StaffMember.Admin,
+ interaction.guild,
+ );
const RolePerms = new Discord.EmbedBuilder()
.setTitle("Invalid Permissions!")
.setDescription(
- `${msgconfig.Error.IncorrectPerms}\n\nRole Required: \`${supportbot.Roles.StaffMember.Staff}\` or \`${supportbot.Roles.StaffMember.Admin}\``
+ `${msgconfig.Error.IncorrectPerms}\n\nRole Required: \`${supportbot.Roles.StaffMember.Staff}\` or \`${supportbot.Roles.StaffMember.Admin}\``,
)
.setColor(supportbot.Embed.Colours.Warn);
@@ -523,11 +653,17 @@ module.exports = new Event("interactionCreate", async (client, interaction) => {
const NoPermsClaim = new Discord.EmbedBuilder()
.setTitle(msgconfig.Ticket.ClaimTickets.NoPermsToClaimTitle)
.setDescription(
- msgconfig.Ticket.ClaimTickets.NoPermsToClaim.replace("%channel%", ticketChannel.name)
+ msgconfig.Ticket.ClaimTickets.NoPermsToClaim.replace(
+ "%channel%",
+ ticketChannel.name,
+ ),
)
.setColor(supportbot.Embed.Colours.Error);
- if (!interaction.member.roles.cache.has(Staff.id) && !interaction.member.roles.cache.has(Admin.id)) {
+ if (
+ !interaction.member.roles.cache.has(Staff.id) &&
+ !interaction.member.roles.cache.has(Admin.id)
+ ) {
return interaction.reply({ embeds: [NoPermsClaim], ephemeral: true });
}
@@ -544,12 +680,17 @@ module.exports = new Event("interactionCreate", async (client, interaction) => {
SendMessages: true,
ReadMessageHistory: true,
});
- await ticketChannel.permissionOverwrites.create(interaction.guild.roles.everyone, {
- ViewChannel: false,
- });
+ await ticketChannel.permissionOverwrites.create(
+ interaction.guild.roles.everyone,
+ {
+ ViewChannel: false,
+ },
+ );
}
- const pingclaimedstaff = await ticketChannel.send(`<@${interaction.user.id}>`);
+ const pingclaimedstaff = await ticketChannel.send(
+ `<@${interaction.user.id}>`,
+ );
setTimeout(() => pingclaimedstaff.delete(), 2000);
const ticketInfo = db.getTicket(ticketChannelId);
@@ -562,21 +703,28 @@ module.exports = new Event("interactionCreate", async (client, interaction) => {
const claimedEmbed = new Discord.EmbedBuilder(claimMessage.embeds[0].data)
.setTitle(msgconfig.Ticket.ClaimTickets.ClaimedTitle)
.setDescription(
- msgconfig.Ticket.ClaimTickets.ClaimMessage_Edit
- .replace("%user%", `${interaction.user.id}`)
- .replace("%channel%", `${ticketChannel.id}`)
+ msgconfig.Ticket.ClaimTickets.ClaimMessage_Edit.replace(
+ "%user%",
+ `${interaction.user.id}`,
+ ).replace("%channel%", `${ticketChannel.id}`),
)
.setColor(supportbot.Embed.Colours.Success);
const successfullyClaimed = new Discord.EmbedBuilder()
.setTitle(msgconfig.Ticket.ClaimTickets.ClaimedTitle)
- .setDescription(msgconfig.Ticket.ClaimTickets.Claimed
- .replace("%channel%", `${ticketChannel.id}`)
+ .setDescription(
+ msgconfig.Ticket.ClaimTickets.Claimed.replace(
+ "%channel%",
+ `${ticketChannel.id}`,
+ ),
)
.setColor(supportbot.Embed.Colours.Success);
await claimMessage.edit({ embeds: [claimedEmbed], components: [] });
- await interaction.reply({ embeds: [successfullyClaimed], ephemeral: true });
+ await interaction.reply({
+ embeds: [successfullyClaimed],
+ ephemeral: true,
+ });
}
// SUGGESTION INTERACTIONS [START]
@@ -588,7 +736,10 @@ module.exports = new Event("interactionCreate", async (client, interaction) => {
) {
const suggestion = suggestions[interaction.message.id];
if (!suggestion)
- return interaction.reply({ content: "Suggestion not found.", ephemeral: true });
+ return interaction.reply({
+ content: "Suggestion not found.",
+ ephemeral: true,
+ });
const userId = interaction.user.id;
const hasUpvoted = suggestion.upvotes.includes(userId);
@@ -600,29 +751,42 @@ module.exports = new Event("interactionCreate", async (client, interaction) => {
} else {
suggestion.upvotes.push(userId);
if (hasDownvoted) {
- suggestion.downvotes = suggestion.downvotes.filter((id) => id !== userId);
+ suggestion.downvotes = suggestion.downvotes.filter(
+ (id) => id !== userId,
+ );
}
}
} else if (interaction.customId === "downvote") {
if (hasDownvoted) {
- suggestion.downvotes = suggestion.downvotes.filter((id) => id !== userId);
+ suggestion.downvotes = suggestion.downvotes.filter(
+ (id) => id !== userId,
+ );
} else {
suggestion.downvotes.push(userId);
if (hasUpvoted) {
- suggestion.upvotes = suggestion.upvotes.filter((id) => id !== userId);
+ suggestion.upvotes = suggestion.upvotes.filter(
+ (id) => id !== userId,
+ );
}
}
} else if (interaction.customId === "removevote") {
suggestion.upvotes = suggestion.upvotes.filter((id) => id !== userId);
- suggestion.downvotes = suggestion.downvotes.filter((id) => id !== userId);
+ suggestion.downvotes = suggestion.downvotes.filter(
+ (id) => id !== userId,
+ );
}
- fs.writeFileSync("./Data/SuggestionData.json", JSON.stringify(suggestions, null, 2));
+ fs.writeFileSync(
+ "./Data/SuggestionData.json",
+ JSON.stringify(suggestions, null, 2),
+ );
const upvoteCount = suggestion.upvotes.length;
const downvoteCount = suggestion.downvotes.length;
- const updatedEmbed = new Discord.EmbedBuilder(interaction.message.embeds[0].data).setFields([
+ const updatedEmbed = new Discord.EmbedBuilder(
+ interaction.message.embeds[0].data,
+ ).setFields([
{ name: "Suggestion", value: suggestion.suggestion, inline: true },
{ name: "From", value: `<@${suggestion.author}>` },
{
@@ -639,25 +803,30 @@ module.exports = new Event("interactionCreate", async (client, interaction) => {
await interaction.update({ embeds: [updatedEmbed] });
}
-
}
async function handleInvites(interaction, enable) {
if (supportbot.Ticket.Invites.StaffOnly) {
const { getRole, getChannel } = interaction.client;
- const SupportStaff = await getRole(supportbot.Roles.StaffMember.Staff, interaction.guild);
- const Admin = await getRole(supportbot.Roles.StaffMember.Admin, interaction.guild);
+ const SupportStaff = await getRole(
+ supportbot.Roles.StaffMember.Staff,
+ interaction.guild,
+ );
+ const Admin = await getRole(
+ supportbot.Roles.StaffMember.Admin,
+ interaction.guild,
+ );
if (!SupportStaff || !Admin) {
return interaction.reply(
- "Some roles seem to be missing!\nPlease check for errors when starting the bot."
+ "Some roles seem to be missing!\nPlease check for errors when starting the bot.",
);
}
const NoPerms = new Discord.EmbedBuilder()
.setTitle("Invalid Permissions!")
.setDescription(
- `${msgconfig.Error.IncorrectPerms}\n\nRole Required: \`${supportbot.Roles.StaffMember.Staff}\` or \`${supportbot.Roles.StaffMember.Admin}\``
+ `${msgconfig.Error.IncorrectPerms}\n\nRole Required: \`${supportbot.Roles.StaffMember.Staff}\` or \`${supportbot.Roles.StaffMember.Admin}\``,
)
.setColor(supportbot.Embed.Colours.Warn);
@@ -671,8 +840,12 @@ module.exports = new Event("interactionCreate", async (client, interaction) => {
await interaction.channel.setInvitable(enable);
- const message = enable ? msgconfig.Ticket.EnableInvites : msgconfig.Ticket.DisableInvites;
- const embed = new Discord.EmbedBuilder().setDescription(message).setColor(supportbot.Embed.Colours.General);
+ const message = enable
+ ? msgconfig.Ticket.EnableInvites
+ : msgconfig.Ticket.DisableInvites;
+ const embed = new Discord.EmbedBuilder()
+ .setDescription(message)
+ .setColor(supportbot.Embed.Colours.General);
await interaction.reply({
embeds: [embed],
@@ -680,7 +853,10 @@ module.exports = new Event("interactionCreate", async (client, interaction) => {
});
}
- if (interaction.type === Discord.InteractionType.ModalSubmit && interaction.customId === "editProfileModal") {
+ if (
+ interaction.type === Discord.InteractionType.ModalSubmit &&
+ interaction.customId === "editProfileModal"
+ ) {
const userId = interaction.user.id;
const profilePath = `./Data/Profiles/${userId}.json`;
@@ -710,7 +886,11 @@ module.exports = new Event("interactionCreate", async (client, interaction) => {
.setThumbnail(interaction.user.displayAvatarURL())
.addFields(
{ name: "Bio", value: newBio || "No bio set.", inline: false },
- { name: "Timezone", value: newTimezone || "No timezone set.", inline: true }
+ {
+ name: "Timezone",
+ value: newTimezone || "No timezone set.",
+ inline: true,
+ },
);
if (profileData.clockedIn !== undefined) {
@@ -721,11 +901,15 @@ module.exports = new Event("interactionCreate", async (client, interaction) => {
});
}
- await interaction.reply({ embeds: [updatedProfileEmbed], ephemeral: true });
+ await interaction.reply({
+ embeds: [updatedProfileEmbed],
+ ephemeral: true,
+ });
} catch (error) {
console.error("Failed to update profile data: ", error);
await interaction.reply({
- content: "There was an error while updating your profile. Please try again later.",
+ content:
+ "There was an error while updating your profile. Please try again later.",
flags: Discord.MessageFlags.Ephemeral,
});
}
diff --git a/Events/ready.js b/Events/ready.js
index 6921a17..ccbb15c 100644
--- a/Events/ready.js
+++ b/Events/ready.js
@@ -2,14 +2,9 @@ const fs = require("fs");
const Discord = require("discord.js");
const yaml = require("js-yaml");
const Event = require("../Structures/Event.js");
-const gradient = require("gradient-string");
-const figlet = require("figlet");
// Load configs
const supportbot = yaml.load(fs.readFileSync("./Configs/supportbot.yml", "utf8"));
-const panel = yaml.load(fs.readFileSync("./Configs/ticket-panel.yml", "utf8"));
-const cmdconfig = yaml.load(fs.readFileSync("./Configs/commands.yml", "utf8"));
-const msgconfig = yaml.load(fs.readFileSync("./Configs/messages.yml", "utf8"));
const ACTIVITY_TYPES = {
PLAYING: Discord.ActivityType.Playing,
@@ -20,65 +15,76 @@ const ACTIVITY_TYPES = {
const setActivity = (client, type, status) => {
const activityType = ACTIVITY_TYPES[type?.toUpperCase()];
- if (activityType) {
- client.user.setPresence({
- activities: [{ name: status || "Online", type: activityType }],
- status: type?.toLowerCase() || "online"
- });
- }
+ if (!activityType) return;
+
+ client.user.setPresence({
+ activities: [{ name: status || "Online", type: activityType }],
+ status: "online"
+ });
};
-const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
+// simple colors
+const c = {
+ reset: "\x1b[0m",
+ gray: "\x1b[90m",
+ green: "\x1b[32m",
+ yellow: "\x1b[33m",
+ cyan: "\x1b[36m",
+ bold: "\x1b[1m"
+};
+
+function line() {
+ console.log(`${c.gray}────────────────────────────────────────────────────────${c.reset}`);
+}
+
+function section(title) {
+ console.log(`\n${c.green}${title}${c.reset}`);
+ console.log(`${c.gray}${"-".repeat(title.length)}${c.reset}`);
+}
+
+function info(label, value) {
+ console.log(`${c.gray}${label.padEnd(14)}${c.reset} ${value}`);
+}
+
+function status(label, ok, details = "") {
+ const icon = ok ? `${c.green}✓${c.reset}` : `${c.yellow}!${c.reset}`;
+ console.log(` ${icon} ${label}${details ? ` (${details})` : ""}`);
+}
const logStartup = async (client) => {
console.clear();
-
- // Animated loading
- const frames = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
- for (let i = 0; i < frames.length * 2; i++) {
- process.stdout.write(`\r${frames[i % frames.length]} Initializing SupportBot...`);
- await sleep(100);
- }
- console.clear();
- // Main title
- console.log("\n" + gradient.pastel.multiline(figlet.textSync("SupportBot", { font: "ANSI Shadow", horizontalLayout: "fitted" })));
-
- // Extract bot details safely
const botName = supportbot?.General?.Name || "SupportBot";
- const botVersion = supportbot.SupportBot_Version;
+ const botVersion = supportbot?.SupportBot_Version || "Unknown";
- console.log("\n╔═══════════════════════════════════════════════════════════════╗");
- console.log("║ System Information ║");
- console.log("╠═══════════════════════════════════════════════════════════════╣");
- console.log(`║ Bot Name : ${botName.padEnd(45)} ║`);
- console.log(`║ Version : ${botVersion.padEnd(45)} ║`);
- console.log(`║ Node.js : ${process.version.padEnd(45)} ║`);
- console.log(`║ Discord.js : v${Discord.version.padEnd(44)} ║`);
- console.log("╚═══════════════════════════════════════════════════════════════╝\n");
-
- // Links section
- console.log(gradient.summer("┏━━━━━━━━━━━━━━━━━━━ Important Links ━━━━━━━━━━━━━━━━━━━┓"));
- console.log(gradient.summer("┃ ┃"));
- console.log(gradient.summer("┃ ") + "📚 Docs : " + gradient.cristal("https://emerald-services.gitbook.io/ ┃"));
- console.log(gradient.summer("┃ ") + "🤝 Discord : " + gradient.cristal("https://dsc.gg/emerald-dev ┃"));
- console.log(gradient.summer("┃ ┃"));
- console.log(gradient.summer("┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛\n"));
-
- // Bot Invite
const inviteUrl = `https://discord.com/api/oauth2/authorize?client_id=${client.user.id}&permissions=8&scope=bot%20applications.commands`;
- console.log(gradient.teen("╔════════════════════════ Bot Invite ════════════════════════╗"));
- console.log(gradient.teen("║ ║"));
- console.log(gradient.teen("║ ") + gradient.pastel(inviteUrl.padEnd(56)) + gradient.teen("║"));
- console.log(gradient.teen("║ ║"));
- console.log(gradient.teen("╚════════════════════════════════════════════════════════════╝\n"));
+
+ line();
+ console.log(`${c.bold}${c.green}SupportBot${c.reset}`);
+ console.log(`${c.gray}Emerald Services${c.reset}`);
+ line();
+
+ section("System");
+ info("Bot Name", botName);
+ info("Version", botVersion);
+ info("Node.js", process.version);
+ info("Discord.js", `v${Discord.version}`);
+ info("Logged in as", client.user.tag);
+ info("Guilds", `${client.guilds.cache.size}`);
+
+ section("Links");
+ info("Docs", "https://emerald-services.gitbook.io/");
+ info("Discord", "https://dsc.gg/emerald-dev");
+ info("Invite", inviteUrl);
+
+ console.log("");
};
const checkConfigs = async (client) => {
const guild = client.guilds.cache.first();
if (!guild) return;
- console.log(gradient.morning("┏━━━━━━━━━━━━━━━━━━━━ Config Check ━━━━━━━━━━━━━━━━━━━━━┓"));
+ section("Config Check");
const roles = [
supportbot?.Roles?.StaffMember?.Admin,
@@ -87,38 +93,30 @@ const checkConfigs = async (client) => {
].filter(Boolean);
const channels = [
- supportbot?.Suggestions?.Channel,
- supportbot?.Ticket?.Log?.TicketLog,
- supportbot?.Ticket?.Log?.TranscriptLog,
- supportbot?.Ticket?.TicketHome,
- supportbot?.Welcome?.Channel,
- supportbot?.Leave?.Channel,
- supportbot?.Translate?.Log
+ supportbot?.Ticket?.TicketHome
].filter(Boolean);
- const [missingRoles, missingChannels] = await Promise.all([
+ const [roleResults, channelResults] = await Promise.all([
Promise.all(roles.map((role) => client.getRole(role, guild))),
Promise.all(channels.map((channel) => client.getChannel(channel, guild)))
]);
- const missing = {
- roles: roles.filter((_, i) => !missingRoles[i]),
- channels: channels.filter((_, i) => !missingChannels[i])
- };
+ const missingRoles = roles.filter((_, i) => !roleResults[i]);
+ const missingChannels = channels.filter((_, i) => !channelResults[i]);
- if (missing.roles.length || missing.channels.length) {
- console.log(gradient.morning("┃ ⚠️ - Configuration Issues Found: ┃"));
- if (missing.roles.length) {
- console.log(gradient.morning("┃ • Missing Roles: ") + gradient.fruit(missing.roles.join(", ")));
- }
- if (missing.channels.length) {
- console.log(gradient.morning("┃ • Missing Channels: ") + gradient.fruit(missing.channels.join(", ")));
- }
+ if (!missingRoles.length && !missingChannels.length) {
+ status("All configurations validated successfully", true);
} else {
- console.log(gradient.morning("┃ ✅ - All configurations validated successfully ┃"));
+ if (missingRoles.length) {
+ status("Missing roles", false, missingRoles.join(", "));
+ }
+
+ if (missingChannels.length) {
+ status("Missing channels", false, missingChannels.join(", "));
+ }
}
- console.log(gradient.morning("┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛\n"));
+ console.log("");
};
module.exports = new Event("clientReady", async (client) => {
diff --git a/Structures/Client.js b/Structures/Client.js
index e3ac602..a5e7b81 100644
--- a/Structures/Client.js
+++ b/Structures/Client.js
@@ -1,13 +1,32 @@
const fs = require("fs");
const path = require("path");
const Discord = require("discord.js");
-const { GatewayIntentBits, Partials } = require('discord.js');
+const { GatewayIntentBits, Partials } = require("discord.js");
const yaml = require("js-yaml");
-const { Command, Event } = require('./Addon.js');
+const { Command, Event } = require("./Addon.js");
const supportbot = yaml.load(fs.readFileSync("./Configs/supportbot.yml", "utf8"));
const cmdconfig = yaml.load(fs.readFileSync("./Configs/commands.yml", "utf8"));
+const c = {
+ reset: "\x1b[0m",
+ gray: "\x1b[90m",
+ green: "\x1b[32m",
+ yellow: "\x1b[33m",
+ cyan: "\x1b[36m",
+ red: "\x1b[31m",
+ white: "\x1b[37m",
+ bold: "\x1b[1m",
+};
+
+function line() {
+ console.log(`${c.gray}────────────────────────────────────────────────────────${c.reset}`);
+}
+
+function section(title) {
+ console.log(`\n${c.green}${title}${c.reset}`);
+ console.log(`${c.gray}${"-".repeat(title.length)}${c.reset}`);
+}
function walk(dir, ext = ".js", fileList = []) {
const files = fs.readdirSync(dir);
@@ -30,9 +49,9 @@ class Client extends Discord.Client {
GatewayIntentBits.Guilds,
GatewayIntentBits.GuildMembers,
GatewayIntentBits.GuildMessages,
- GatewayIntentBits.MessageContent
+ GatewayIntentBits.MessageContent,
],
- partials: [Partials.Message, Partials.Channel]
+ partials: [Partials.Message, Partials.Channel],
});
this.commands = new Discord.Collection();
@@ -43,14 +62,14 @@ class Client extends Discord.Client {
return guild.channels.cache.find(
(c) =>
(c.type === Discord.ChannelType.GuildText || c.type === Discord.ChannelType.GuildNews) &&
- (c.id === channel || (c.name && c.name.toLowerCase() === channel.toLowerCase()))
+ (c.id === channel || (c.name && c.name.toLowerCase() === channel.toLowerCase())),
);
}
async getRole(role, guild) {
if (!role) return null;
return guild.roles.cache.find(
- (r) => r.id === role || (r.name && r.name.toLowerCase() === role.toLowerCase())
+ (r) => r.id === role || (r.name && r.name.toLowerCase() === role.toLowerCase()),
);
}
@@ -59,88 +78,42 @@ class Client extends Discord.Client {
return guild.channels.cache.find(
(c) =>
c.type === Discord.ChannelType.GuildCategory &&
- (c.id === category || (c.name && c.name.toLowerCase() === category.toLowerCase()))
+ (c.id === category || (c.name && c.name.toLowerCase() === category.toLowerCase())),
);
}
async start(token) {
- let tempCommandFiles = walk("./Commands");
+ const commandFiles = walk("./Commands");
- if (cmdconfig.Suggestion.Enabled === false) {
- tempCommandFiles = tempCommandFiles.filter(item => path.basename(item) !== "suggest.js");
- }
+ const allCommands = commandFiles.map((file) => ({
+ file,
+ command: require(`../${file}`),
+ }));
- if (cmdconfig.Suggestion.Enabled === false) {
- tempCommandFiles = tempCommandFiles.filter(item => path.basename(item) !== "suggestadmin.js");
- }
+ const enabledCommands = allCommands.filter(({ command }) => {
+ const matchingConfig = Object.values(cmdconfig).find(
+ (entry) =>
+ entry &&
+ typeof entry === "object" &&
+ entry.Command &&
+ String(entry.Command).toLowerCase() === String(command.name).toLowerCase(),
+ );
- if (cmdconfig.Help.Enabled === false) {
- tempCommandFiles = tempCommandFiles.filter(item => path.basename(item) !== "help.js");
- }
+ if (!matchingConfig) {
+ return true;
+ }
- if (cmdconfig.Info.Enabled === false) {
- tempCommandFiles = tempCommandFiles.filter(item => path.basename(item) !== "info.js");
- }
-
- if (cmdconfig.OpenTicket.Enabled === false) {
- tempCommandFiles = tempCommandFiles.filter(item => path.basename(item) !== "ticket.js");
- }
-
- if (cmdconfig.CloseTicket.Enabled === false) {
- tempCommandFiles = tempCommandFiles.filter(item => path.basename(item) !== "close.js");
- }
-
- if (cmdconfig.Embed.Enabled === false) {
- tempCommandFiles = tempCommandFiles.filter(item => path.basename(item) !== "embed.js");
- }
-
- if (cmdconfig.Translate.Enabled === false) {
- tempCommandFiles = tempCommandFiles.filter(item => path.basename(item) !== "translate.js");
- }
-
- if (cmdconfig.UserInfo.Enabled === false) {
- tempCommandFiles = tempCommandFiles.filter(item => path.basename(item) !== "userinfo.js");
- }
-
- if (cmdconfig.Ping.Enabled === false) {
- tempCommandFiles = tempCommandFiles.filter(item => path.basename(item) !== "Ping.js");
- }
-
- if (cmdconfig.AddUser.Enabled === false) {
- tempCommandFiles = tempCommandFiles.filter(item => path.basename(item) !== "addUser.js");
- }
-
- if (cmdconfig.RemoveUser.Enabled === false) {
- tempCommandFiles = tempCommandFiles.filter(item => path.basename(item) !== "removeUser.js");
- }
-
- if (cmdconfig.ForceAddUser.Enabled === false) {
- tempCommandFiles = tempCommandFiles.filter(item => path.basename(item) !== "forceaddUser.js");
- }
-
- if (cmdconfig.Profile.Enabled === false) {
- tempCommandFiles = tempCommandFiles.filter(item => path.basename(item) !== "profile.js");
- }
-
- if (cmdconfig.Settings.Enabled === false) {
- tempCommandFiles = tempCommandFiles.filter(item => path.basename(item) !== "settings.js");
- }
-
- if (cmdconfig.Mod.Enabled === false) {
- tempCommandFiles = tempCommandFiles.filter(item => path.basename(item) !== "mod.js");
- }
-
- const commandFiles = tempCommandFiles;
- const commands = commandFiles.map((file) => require(`../${file}`));
-
- console.log(`\u001b[33m`, "――――――――――――――――――――――――――――――――――――――――――――");
-
- commands.forEach((cmd) => {
- console.log(`\u001b[32m`, `[CMD]`, `\u001b[37;1m`, `${cmd.name}`, `\u001b[32;1m`, "Loaded");
- this.commands.set(cmd.name, cmd);
+ return matchingConfig.Enabled !== false;
});
- console.log(`\u001b[33m`, "▬▬▬▬▬▬▬ Commands ▬▬▬▬▬▬▬");
+ section("Commands");
+
+ enabledCommands.forEach(({ command, file }) => {
+ console.log(
+ `${c.green}✓${c.reset} ${c.white}${command.name}${c.reset} ${c.gray}(${path.basename(file)})${c.reset}`,
+ );
+ this.commands.set(command.name, command);
+ });
if (supportbot.General.Addons.Enabled) {
const addonFiles = fs.readdirSync("./Addons").filter((file) => file.endsWith(".js"));
@@ -151,18 +124,10 @@ class Client extends Discord.Client {
return addon;
});
- console.log(" ");
- console.log(`\u001b[33m`, "▬▬▬▬▬▬▬ Addons ▬▬▬▬▬▬▬");
+ section("Addons");
addons.forEach((addon) => {
- console.log(
- `\u001b[32m`,
- `[ADDON]`,
- `\u001b[37;1m`,
- `${addon.name}`,
- `\u001b[32;1m`,
- "Loaded"
- );
+ console.log(`${c.green}✓${c.reset} ${c.white}${addon.name}${c.reset}`);
if (addon instanceof Command) {
this.commands.set(addon.name, addon);
@@ -176,37 +141,34 @@ class Client extends Discord.Client {
});
}
});
-
- console.log(`\u001b[33m`, "▬▬▬▬▬▬▬ Addons ▬▬▬▬▬▬▬");
}
this.once("clientReady", async () => {
await this.guilds.cache.first()?.commands.set(this.commands);
+
console.log(
- "\u001b[32m",
- `[SUCCESS]`,
- `\u001b[37;1m`,
- `Slash Commands Registered for ${this.guilds.cache.first().name}`
+ `\n${c.green}✓${c.reset} ${c.white}Slash Commands Registered${c.reset} ${c.gray}for ${this.guilds.cache.first().name}${c.reset}`,
);
});
- console.log(" ");
- console.log(`\u001b[33m`, "▬▬▬▬▬▬▬ Events ▬▬▬▬▬▬▬");
+ section("Events");
- fs.readdirSync("./Events").filter((file) => file.endsWith(".js")).forEach((file) => {
- const event = require(`../Events/${file}`);
- console.log(`\u001b[32m`, `[EVENT]`, `\u001b[37;1m`, `${event.event}`, `\u001b[32;1m`, "Loaded");
- this.on(event.event, (...args) => event.run(this, ...args));
- });
+ fs.readdirSync("./Events")
+ .filter((file) => file.endsWith(".js"))
+ .forEach((file) => {
+ const event = require(`../Events/${file}`);
+ console.log(
+ `${c.green}✓${c.reset} ${c.white}${event.event}${c.reset} ${c.gray}(${file})${c.reset}`,
+ );
+ this.on(event.event, (...args) => event.run(this, ...args));
+ });
+
+ console.log("");
- console.log(`\u001b[33m`, "▬▬▬▬▬▬▬ Events ▬▬▬▬▬▬▬");
if (process.argv[2] !== "test") {
await this.login(token);
} else {
- console.log(
- `\u001b[31;1m`,
- "RUNNING IN TEST MODE, IF NOT INTENDED, PLEASE USE npm start"
- );
+ console.log(`${c.red}RUNNING IN TEST MODE, IF NOT INTENDED, PLEASE USE npm start${c.reset}`);
}
}
}
diff --git a/Structures/Database.js b/Structures/Database.js
index 9b10848..657d228 100644
--- a/Structures/Database.js
+++ b/Structures/Database.js
@@ -1,166 +1,205 @@
// SupportBot | Emerald Services
-// Database Structure
-
const Database = require("better-sqlite3");
const path = require("path");
const db = new Database(path.join(__dirname, "../Data/supportbot.db"));
-db.prepare(`
+db.prepare(
+ `
CREATE TABLE IF NOT EXISTS suggestions (
thread_id TEXT PRIMARY KEY,
author_id TEXT,
- text TEXT,
- status TEXT DEFAULT 'Created'
+ text TEXT,
+ status TEXT DEFAULT 'Created'
)
-`).run();
+`,
+).run();
-db.prepare(`
+db.prepare(
+ `
CREATE TABLE IF NOT EXISTS profiles (
- user_id TEXT PRIMARY KEY,
- bio TEXT,
- timezone TEXT,
+ user_id TEXT PRIMARY KEY,
+ bio TEXT,
+ timezone TEXT,
clockedIn INTEGER DEFAULT 0
)
-`).run();
+`,
+).run();
-db.prepare(`
+db.prepare(
+ `
CREATE TABLE IF NOT EXISTS tickets (
- ticket_id TEXT PRIMARY KEY,
- user_id TEXT,
- status TEXT DEFAULT 'open',
- created_at INTEGER,
- updated_at INTEGER,
- subject TEXT,
- description TEXT
+ ticket_id TEXT PRIMARY KEY,
+ user_id TEXT,
+ status TEXT DEFAULT 'open',
+ created_at INTEGER,
+ updated_at INTEGER,
+ subject TEXT,
+ description TEXT
)
-`).run();
+`,
+).run();
-try {
- db.prepare("ALTER TABLE tickets ADD COLUMN voiceChannelId TEXT").run();
-} catch (e) {
- if (!String(e).includes("duplicate column name")) {
- throw e;
+// Safe column upgrades
+const safeAlter = (sql) => {
+ try {
+ db.prepare(sql).run();
+ } catch (e) {
+ if (!String(e).includes("duplicate column name")) throw e;
}
-}
+};
-db.prepare(`
+safeAlter(`ALTER TABLE tickets ADD COLUMN voiceChannelId TEXT`);
+safeAlter(`ALTER TABLE tickets ADD COLUMN department TEXT DEFAULT 'general'`);
+safeAlter(`ALTER TABLE tickets ADD COLUMN priority TEXT DEFAULT 'medium'`);
+
+db.prepare(
+ `
CREATE TABLE IF NOT EXISTS ticket_panel (
id INTEGER PRIMARY KEY AUTOINCREMENT,
message_id TEXT,
channel_id TEXT,
created_at INTEGER
)
-`).run();
+`,
+).run();
-db.prepare(`
+db.prepare(
+ `
CREATE TABLE IF NOT EXISTS ticket_users (
ticket_id TEXT,
- user_id TEXT,
+ user_id TEXT,
PRIMARY KEY(ticket_id, user_id)
)
-`).run();
+`,
+).run();
-db.prepare(`
+db.prepare(
+ `
CREATE TABLE IF NOT EXISTS settings (
- id INTEGER PRIMARY KEY,
+ id INTEGER PRIMARY KEY,
data TEXT
)
-`).run();
+`,
+).run();
-module.exports = {
- addSuggestion(threadId, authorId, text) {
- db.prepare(`
- INSERT INTO suggestions (thread_id, author_id, text)
- VALUES (@threadId, @authorId, @text)
- ON CONFLICT(thread_id) DO NOTHING
- `).run({ threadId, authorId, text });
- },
+function ensureProfile(userId) {
+ const existing = db.prepare(`
+ SELECT user_id FROM profiles WHERE user_id = ?
+ `).get(userId);
- setSuggestionStatus(threadId, status) {
- db.prepare(`
- UPDATE suggestions SET status=@status WHERE thread_id=@threadId
- `).run({ threadId, status });
- },
-
- getSuggestion(threadId) {
- return db.prepare(`
- SELECT * FROM suggestions WHERE thread_id=@threadId
- `).get({ threadId });
- },
-
- // ----- Profiles -----
- getProfile(userId) {
- const row = db.prepare(`SELECT * FROM profiles WHERE user_id=?`).get(userId);
- if (!row) {
- db.prepare(`
- INSERT INTO profiles (user_id, bio, timezone, clockedIn)
- VALUES (?, '', '', 0)
- `).run(userId);
- return { user_id: userId, bio: "", timezone: "", clockedIn: 0 };
- }
- return row;
- },
-
- updateProfile(userId, data) {
- const { bio = "", timezone = "", clockedIn = 0 } = data;
+ if (!existing) {
db.prepare(`
INSERT INTO profiles (user_id, bio, timezone, clockedIn)
- VALUES (@userId, @bio, @timezone, @clockedIn)
- ON CONFLICT(user_id) DO UPDATE SET
- bio = excluded.bio,
- timezone = excluded.timezone,
- clockedIn = excluded.clockedIn
- `).run({ userId, bio, timezone, clockedIn });
- },
-
- setClockedIn(userId, value) {
- db.prepare(`UPDATE profiles SET clockedIn=? WHERE user_id=?`)
- .run(value ? 1 : 0, userId);
- },
+ VALUES (?, '', '', 0)
+ `).run(userId);
+ }
+}
+module.exports = {
addTicket(ticket) {
const now = Date.now();
- db.prepare(`
- INSERT INTO tickets (ticket_id, user_id, subject, description, created_at, updated_at)
- VALUES (@ticket_id, @user_id, @subject, @description, @created_at, @updated_at)
+
+ db.prepare(
+ `
+ INSERT INTO tickets (
+ ticket_id,
+ user_id,
+ subject,
+ description,
+ created_at,
+ updated_at,
+ department,
+ priority
+ )
+ VALUES (
+ @ticket_id,
+ @user_id,
+ @subject,
+ @description,
+ @created_at,
+ @updated_at,
+ @department,
+ @priority
+ )
ON CONFLICT(ticket_id) DO NOTHING
- `).run({
+ `,
+ ).run({
ticket_id: ticket.id,
user_id: ticket.user,
subject: ticket.reason || "",
description: ticket.description || "",
created_at: now,
- updated_at: now
+ updated_at: now,
+ department: ticket.department || "general",
+ priority: ticket.priority || "medium",
});
},
updateTicketVoice(ticketId, voiceChannelId) {
- db.prepare(`
- UPDATE tickets SET voiceChannelId=@voiceChannelId, updated_at=@updatedAt WHERE ticket_id=@ticketId
- `).run({ ticketId, voiceChannelId, updatedAt: Date.now() });
- },
-
- getTicketVoice(ticketId) {
- return db.prepare(`SELECT voiceChannelId FROM tickets WHERE ticket_id=?`).get(ticketId);
- },
-
- deleteTicketVoice(ticketId) {
- db.prepare(`
- UPDATE tickets SET voiceChannelId=NULL, updated_at=@updatedAt WHERE ticket_id=@ticketId
- `).run({ ticketId, updatedAt: Date.now() });
+ db.prepare(
+ `
+ UPDATE tickets
+ SET voiceChannelId=@voiceChannelId, updated_at=@updatedAt
+ WHERE ticket_id=@ticketId
+ `,
+ ).run({
+ ticketId,
+ voiceChannelId,
+ updatedAt: Date.now(),
+ });
},
updateTicketStatus(ticketId, status) {
- db.prepare(`
- UPDATE tickets SET status=@status, updated_at=@updatedAt WHERE ticket_id=@ticketId
- `).run({ status, updatedAt: Date.now(), ticketId });
+ db.prepare(
+ `
+ UPDATE tickets
+ SET status=@status, updated_at=@updatedAt
+ WHERE ticket_id=@ticketId
+ `,
+ ).run({
+ status,
+ updatedAt: Date.now(),
+ ticketId,
+ });
+ },
+
+ updateTicketDepartment(ticketId, department) {
+ db.prepare(
+ `
+ UPDATE tickets
+ SET department=@department, updated_at=@updatedAt
+ WHERE ticket_id=@ticketId
+ `,
+ ).run({
+ ticketId,
+ department,
+ updatedAt: Date.now(),
+ });
+ },
+
+ updateTicketPriority(ticketId, priority) {
+ db.prepare(
+ `
+ UPDATE tickets
+ SET priority=@priority, updated_at=@updatedAt
+ WHERE ticket_id=@ticketId
+ `,
+ ).run({
+ ticketId,
+ priority,
+ updatedAt: Date.now(),
+ });
},
getTicket(ticketId) {
- return db.prepare(`
+ return db
+ .prepare(
+ `
SELECT * FROM tickets WHERE ticket_id=@ticketId
- `).get({ ticketId });
+ `,
+ )
+ .get({ ticketId });
},
getAllTickets() {
@@ -168,36 +207,51 @@ module.exports = {
},
saveTicketPanel(messageId, channelId) {
- db.prepare(`
+ db.prepare(
+ `
INSERT INTO ticket_panel (message_id, channel_id, created_at)
VALUES (@messageId, @channelId, @createdAt)
- `).run({ messageId, channelId, createdAt: Date.now() });
+ `,
+ ).run({ messageId, channelId, createdAt: Date.now() });
},
getTicketPanel() {
- return db.prepare(`
+ return db
+ .prepare(
+ `
SELECT * FROM ticket_panel ORDER BY id DESC LIMIT 1
- `).get();
+ `,
+ )
+ .get();
},
addUserToTicket(ticketId, userId) {
- db.prepare(`
+ db.prepare(
+ `
INSERT INTO ticket_users (ticket_id, user_id)
VALUES (?, ?)
ON CONFLICT(ticket_id, user_id) DO NOTHING
- `).run(ticketId, userId);
+ `,
+ ).run(ticketId, userId);
},
removeUserFromTicket(ticketId, userId) {
- db.prepare(`
+ db.prepare(
+ `
DELETE FROM ticket_users WHERE ticket_id=? AND user_id=?
- `).run(ticketId, userId);
+ `,
+ ).run(ticketId, userId);
},
getTicketUsers(ticketId) {
- return db.prepare(`
+ return db
+ .prepare(
+ `
SELECT user_id FROM ticket_users WHERE ticket_id=?
- `).all(ticketId).map(r => r.user_id);
+ `,
+ )
+ .all(ticketId)
+ .map((r) => r.user_id);
},
getSettings() {
@@ -210,12 +264,46 @@ module.exports = {
}
},
+ getProfile(userId) {
+ ensureProfile(userId);
+
+ const row = db.prepare(`
+ SELECT * FROM profiles WHERE user_id = ?
+ `).get(userId);
+
+ if (!row) {
+ return {
+ user_id: userId,
+ bio: "",
+ timezone: "",
+ clockedIn: false,
+ };
+ }
+
+ return {
+ ...row,
+ clockedIn: Boolean(row.clockedIn),
+ };
+ },
+
+ setClockedIn(userId, clockedIn) {
+ ensureProfile(userId);
+
+ db.prepare(`
+ UPDATE profiles
+ SET clockedIn = ?
+ WHERE user_id = ?
+ `).run(clockedIn ? 1 : 0, userId);
+ },
+
saveSettings(obj) {
const data = JSON.stringify(obj);
- db.prepare(`
+ db.prepare(
+ `
INSERT INTO settings (id, data)
VALUES (1, @data)
ON CONFLICT(id) DO UPDATE SET data = excluded.data
- `).run({ data });
- }
+ `,
+ ).run({ data });
+ },
};
diff --git a/Structures/TicketManager.js b/Structures/TicketManager.js
new file mode 100644
index 0000000..85455f4
--- /dev/null
+++ b/Structures/TicketManager.js
@@ -0,0 +1,636 @@
+const fs = require("fs");
+const path = require("path");
+const Discord = require("discord.js");
+const yaml = require("js-yaml");
+const db = require("./Database.js");
+
+const supportbot = yaml.load(fs.readFileSync("./Configs/supportbot.yml", "utf8"));
+const msgconfig = yaml.load(fs.readFileSync("./Configs/messages.yml", "utf8"));
+
+function escapeHtml(str = "") {
+ return String(str)
+ .replace(/&/g, "&")
+ .replace(//g, ">")
+ .replace(/"/g, """);
+}
+
+function parseMarkdown(content = "") {
+ let safe = escapeHtml(content);
+
+ safe = safe.replace(/```([\s\S]*?)```/g, "$1
");
+ safe = safe.replace(/`(.*?)`/g, "$1");
+ safe = safe.replace(/\*\*(.*?)\*\*/g, "$1");
+ safe = safe.replace(/\*(.*?)\*/g, "$1");
+ safe = safe.replace(/_(.*?)_/g, "$1");
+ safe = safe.replace(
+ /\[(.*?)\]\((.*?)\)/g,
+ '$1',
+ );
+
+ return safe.replace(/\n/g, "
");
+}
+
+function formatDate(iso) {
+ try {
+ return new Date(iso).toLocaleString("en-GB", {
+ dateStyle: "medium",
+ timeStyle: "short",
+ });
+ } catch {
+ return iso;
+ }
+}
+
+function getFileTypeInfo(att) {
+ const fileName = String(att.name || "attachment").toLowerCase();
+
+ if (/\.(png|jpg|jpeg|gif|webp|bmp|svg)$/i.test(fileName)) {
+ return "image";
+ }
+
+ if (/\.(mp4|webm|mov|m4v)$/i.test(fileName)) {
+ return "video";
+ }
+
+ if (/\.(mp3|wav|ogg|m4a|flac)$/i.test(fileName)) {
+ return "audio";
+ }
+
+ return "file";
+}
+
+function renderAttachment(att) {
+ const url = escapeHtml(att.url || "");
+ const name = escapeHtml(att.name || "attachment");
+ const type = getFileTypeInfo(att);
+
+ if (type === "image") {
+ return `
+
+ `;
+ }
+
+ if (type === "video") {
+ return `
+
+ `;
+ }
+
+ if (type === "audio") {
+ return `
+
+ `;
+ }
+
+ return `
+
+ `;
+}
+
+function renderEmbeds(embeds = []) {
+ return embeds
+ .map((embed) => {
+ const fields = (embed.fields || [])
+ .map(
+ (field) => `
+
+
${escapeHtml(field.name || "Field")}
+
${parseMarkdown(field.value || "")}
+
+ `,
+ )
+ .join("");
+
+ return `
+
+ ${embed.title ? `
${escapeHtml(embed.title)}
` : ""}
+ ${embed.description ? `
${parseMarkdown(embed.description)}
` : ""}
+ ${fields}
+
+ `;
+ })
+ .join("");
+}
+
+function createTranscriptHTML(ticket, reason) {
+ return `
+
+
+
+
+ Transcript - ${escapeHtml(ticket.name)}
+
+
+
+
+
+
+
+
+ ${
+ ticket.messages.length
+ ? ticket.messages
+ .map(
+ (msg) => `
+
+
+
+
+
+
${parseMarkdown(msg.content || "")}
+
+ ${renderEmbeds(msg.embeds || [])}
+ ${(msg.attachments || []).map(renderAttachment).join("")}
+
+
+ `,
+ )
+ .join("")
+ : `No messages were found in this ticket.
`
+ }
+
+
+
+`;
+}
+
+async function createTranscript(interaction, ticket, reason) {
+ const { getChannel } = interaction.client;
+
+ const transcriptChannel = await getChannel(
+ supportbot.Ticket.Log.TicketDataLog,
+ interaction.guild,
+ );
+
+ if (!transcriptChannel) {
+ throw new Error("Transcript log channel missing or inaccessible.");
+ }
+
+ let allMessages = [];
+ let lastId = null;
+
+ while (true) {
+ const options = { limit: 100 };
+ if (lastId) options.before = lastId;
+
+ const messages = await interaction.channel.messages.fetch(options);
+ if (messages.size === 0) break;
+
+ allMessages = [...allMessages, ...messages.values()];
+ lastId = messages.last().id;
+
+ if (messages.size < 100) break;
+ }
+
+ allMessages.sort((a, b) => a.createdTimestamp - b.createdTimestamp);
+
+ const transcriptData = allMessages.map((msg) => ({
+ content: msg.content || "",
+ username: msg.author.username,
+ userId: msg.author.id,
+ avatar: msg.author.displayAvatarURL({ extension: "png", size: 128 }),
+ timestamp: msg.createdAt.toISOString(),
+ attachments: Array.from(msg.attachments.values()).map((att) => ({
+ url: att.url,
+ name: att.name || "attachment",
+ contentType: att.contentType || null,
+ size: att.size || 0,
+ })),
+ embeds: msg.embeds.map((embed) => ({
+ title: embed.title || "",
+ description: embed.description || "",
+ fields: embed.fields || [],
+ })),
+ }));
+
+ if (typeof db.updateTicketStatus === "function") {
+ db.updateTicketStatus(interaction.channel.id, "closed");
+ }
+
+ const ticketUserId = ticket.user_id || ticket.user;
+ const tUser =
+ interaction.client.users.cache.get(ticketUserId) ||
+ (ticketUserId
+ ? await interaction.client.users.fetch(ticketUserId).catch(() => null)
+ : null);
+
+ const transcriptEmbed = new Discord.EmbedBuilder()
+ .setTitle(msgconfig.TicketLog.Title)
+ .setColor(msgconfig.TicketLog.Colour)
+ .setFooter({
+ text: supportbot.Embed.Footer,
+ iconURL: interaction.user.displayAvatarURL(),
+ })
+ .setDescription(
+ `> **Ticket:** ${interaction.channel.name} (\`${interaction.channel.id}\`)\n` +
+ `> **User:** ${tUser?.tag || "Unknown User"} (\`${tUser?.id || ticketUserId || "Unknown"}\`)\n` +
+ `> **Closed by:** <@${interaction.user.id}>\n` +
+ `> **Message Count:** ${transcriptData.length}`,
+ )
+ .addFields({
+ name: "Reason",
+ value: `\`\`\`${reason || "No Reason Provided."}\`\`\``,
+ inline: false,
+ });
+
+ const transcriptDir = path.join(process.cwd(), "./Data/Transcripts");
+ if (!fs.existsSync(transcriptDir)) {
+ fs.mkdirSync(transcriptDir, { recursive: true });
+ }
+
+ const htmlFileName = `${interaction.channel.id}-transcript.html`;
+ const htmlPath = path.join(transcriptDir, htmlFileName);
+
+ const html = createTranscriptHTML(
+ {
+ id: interaction.channel.id,
+ name: interaction.channel.name,
+ messages: transcriptData,
+ },
+ reason,
+ );
+
+ fs.writeFileSync(htmlPath, html, "utf8");
+
+ await transcriptChannel.send({
+ embeds: [transcriptEmbed],
+ files: [
+ new Discord.AttachmentBuilder(htmlPath, {
+ name: `SPOILER_${htmlFileName}`,
+ }),
+ ],
+ });
+
+ return { htmlPath };
+}
+
+module.exports = {
+ createTranscript,
+};
\ No newline at end of file
diff --git a/package.json b/package.json
index eea17b1..2e53aa9 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "supportbot",
- "version": "8.4",
+ "version": "26.0.0-prerelease-1",
"description": "SupportBot is an open-sourced support system for discord. It's completely free to use with high amounts of customization to match your own brand.",
"main": "index.js",
"scripts": {
@@ -18,10 +18,12 @@
},
"homepage": "https://community.emeraldsrv.dev",
"dependencies": {
+ "@vitalets/google-translate-api": "^9.2.1",
"axios": "^1.7.2",
"better-sqlite3": "^12.2.0",
"discord.js": "^14.13.0",
"figlet": "^1.9.3",
+ "google-translate-api": "^2.3.0",
"gradient-string": "^3.0.0",
"js": "^0.1.0",
"js-yaml": "^4.1.0",