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 => ` -
-
- Avatar - ${msg.username} - ${new Date(msg.timestamp).toLocaleString()} -
-
- ${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 ` +
+ 📎 ${name} +
+ ${name} +
+
+ `; + } + + if (type === "video") { + return ` +
+ 📎 ${name} +
+ +
+
+ `; + } + + if (type === "audio") { + return ` +
+ 📎 ${name} +
+ +
+
+ `; + } + + return ` +
+ 📎 ${name} +
+ `; +} + +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 Transcript

+

Exported conversation log for support ticket ${escapeHtml(ticket.name)}

+
+
Closed Transcript
+
+ +
+
+
Channel
+
${escapeHtml(ticket.name)}
+
+
+
Ticket ID
+
${escapeHtml(ticket.id)}
+
+
+
Message Count
+
${ticket.messages.length}
+
+
+
Close Reason
+
${escapeHtml(reason || "No Reason Provided.")}
+
+
+
+ +
+
Messages
+ ${ + ticket.messages.length + ? ticket.messages + .map( + (msg) => ` +
+ Avatar +
+
+ ${escapeHtml(msg.username || "Unknown User")} + ${escapeHtml(msg.userId || "Unknown ID")} + ${formatDate(msg.timestamp)} +
+ +
${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",