const fs = require("fs"); const { EmbedBuilder, ApplicationCommandOptionType, ApplicationCommandType, ChannelType, ActionRowBuilder, ButtonBuilder, StringSelectMenuBuilder, StringSelectMenuOptionBuilder, ButtonStyle, MessageCollector, MessageFlags, SeparatorBuilder, ContainerBuilder, TextDisplayBuilder } = require("discord.js"); const yaml = require("js-yaml"); const db = require("../../Structures/Database.js"); const panelconfig = yaml.load(fs.readFileSync("./Configs/ticket-panel.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 TicketNumberID = require("../../Structures/TicketID.js"); async function isBlacklisted(userId) { let blacklistData; try { blacklistData = JSON.parse(fs.readFileSync("./Data/BlacklistedUsers.json", "utf8")); } catch (error) { console.error("Error reading BlacklistedUsers.json:", error); 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); for (const id of rows) { const profile = db.getProfile(id); if (profile && profile.clockedIn) { clockedInUsers.add(id); } } return clockedInUsers; } async function askTicketQuestions(ticketChannel, interaction, questions) { const answers = []; let questionIndex = 0; const sendNextQuestion = async () => { if (questionIndex < questions.length) { const questionsEmbed = new EmbedBuilder() .setDescription(`**${questionIndex + 1}.** ${questions[questionIndex]}`) .setColor(supportbot.Embed.Colours.General); const questionMessage = await ticketChannel.send({ embeds: [questionsEmbed] }); const collector = new MessageCollector(ticketChannel, { filter: (m) => m.author.id === interaction.user.id, max: 1, time: 60000, }); collector.on("collect", async (message) => { answers.push({ question: questions[questionIndex], answer: message.content }); questionIndex++; await message.delete(); await questionMessage.delete(); sendNextQuestion(); }); } else { const summaryEmbed = new EmbedBuilder() .setTitle(msgconfig.Ticket.TicketQuestions.Details.Title) .setDescription( msgconfig.Ticket.TicketQuestions.Details.Description.replace("%user%", interaction.user.id) ) .setColor(supportbot.Embed.Colours.General) .addFields( answers.map((a) => ({ name: `Q: ${a.question}`, value: `A: ${a.answer}`, inline: false, })) ) .setTimestamp(); await ticketChannel.send({ embeds: [summaryEmbed] }); } }; sendNextQuestion(); } async function assignTicketToUser(ticketChannel, Staff, Admin, interaction, 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); 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; } } const clockedInArray = Array.from(clockedInUsers); const assignedUserId = clockedInArray[Math.floor(Math.random() * clockedInArray.length)]; const assignedUser = await interaction.guild.members.fetch(assignedUserId); const claimEmbed = new EmbedBuilder() .setTitle(msgconfig.Ticket.ClaimTickets.ClaimTitle) .setDescription( 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") .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"); return null; } const claimMessage = await claimChannel.send({ content: `${assignedUser}`, embeds: [claimEmbed], components: [row], }); const filter = (i) => i.customId === `claimticket-${ticketChannel.id}` && i.user.id === assignedUserId; const collector = claimMessage.createMessageComponentCollector({ filter, time: 120000 }); collector.on("collect", async (i) => { await ticketChannel .send({ content: `<@${assignedUserId}>` }) .then((msg) => setTimeout(() => msg.delete(), 5000)); }); collector.on("end", async (collected) => { if (collected.size === 0) { await claimMessage.delete(); if (attempts >= 1) { await claimChannel.send({ content: `No one claimed the ticket. Tagging roles: <@&${Staff.id}> <@&${Admin.id}>`, embeds: [ new EmbedBuilder() .setDescription(`Ticket is still unclaimed: <#${ticketChannel.id}>`) .setColor(supportbot.Embed.Colours.Warn), ], }); 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({ 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) => { console.log(`Ticket ${ticketChannel.id} claimed by user ${i.user.id}`); await ticketChannel.member.add(i.user.id); }); collector.on("end", async (collected) => { if (collected.size === 0) { console.log( `No one claimed the ticket (${ticketChannel.id}), notifying all staff` ); await claimMessage.delete(); await claimChannel.send({ content: `No one claimed the ticket. Tagging roles: <@&${Staff.id}> <@&${Admin.id}>`, }); } }); } else { const ReassigningEmbed = new EmbedBuilder() .setTitle(msgconfig.Ticket.ClaimTickets.ReassigningTitle) .setDescription( 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); } } }); return assignedUserId; } catch (error) { console.error("Error in assignTicketToUser function:", error); return null; } } module.exports = new Command({ name: cmdconfig.OpenTicket.Command, description: cmdconfig.OpenTicket.Description, type: ApplicationCommandType.ChatInput, options: [ { name: "reason", description: "Ticket Reason", type: ApplicationCommandOptionType.String, }, ], permissions: cmdconfig.OpenTicket.Permission, async run(interaction) { try { if (await isBlacklisted(interaction.user.id)) { return interaction.reply({ content: msgconfig.Ticket.Blacklisted, flags: MessageFlags.Ephemeral }); } let TicketReason = null; if (supportbot.Ticket.TicketReason) { TicketReason = 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 ) { return interaction.reply({ embeds: [ { title: "Too Many Tickets!", description: `You can't have more than ${supportbot.Ticket.TicketsPerUser} open tickets!`, color: supportbot.Embed.Colours.Warn, }, ], flags: MessageFlags.Ephemeral }); } let ticketNumberID = await TicketNumberID.pad(); const TicketSubject = TicketReason || msgconfig.Ticket.InvalidSubject; const TicketExists = new EmbedBuilder() .setTitle("Ticket Exists!") .setDescription(msgconfig.Ticket.TicketExists); if ( await interaction.guild.channels.cache.find( (ticketChannel) => ticketChannel.name === `${supportbot.Ticket.Channel}${ticketNumberID}` ) ) { return await interaction.reply({ embeds: [TicketExists], 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) return interaction.reply({ content: "Some roles seem to be missing!\nPlease check for errors when starting the bot.", flags: MessageFlags.Ephemeral }); let ticketChannel; if (supportbot.Ticket.TicketType === "threads") { const TicketHome = interaction.guild.channels.cache.find( (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}`, type: ChannelType.PrivateThread, parent: TicketHome, autoArchiveDuration: 60, reason: 'support ticket', }); } else if (supportbot.Ticket.TicketType === "channels") { const category = interaction.guild.channels.cache.find( (c) => c.name === supportbot.Ticket.TicketChannelsCategory || c.id === supportbot.Ticket.TicketChannelsCategory ); if (!category) { return interaction.reply({ content: "The ticket category does not exist!", flags: MessageFlags.Ephemeral }); } ticketChannel = await interaction.guild.channels.create({ name: `${supportbot.Ticket.Channel}${ticketNumberID}`, type: ChannelType.GuildText, parent: category.id, reason: 'support ticket', }); if (category.children.size >= 50) { const secondaryCategory = interaction.guild.channels.cache.find( (c) => c.name === supportbot.Ticket.TicketChannelsCategory2 || c.id === supportbot.Ticket.TicketChannelsCategory2 ); if (secondaryCategory) { await ticketChannel.setParent(secondaryCategory.id); } else { return interaction.reply({ content: "The secondary ticket category does not exist!", 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); } } if (supportbot.Ticket.TicketType === "threads") { 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, { ViewChannel: false, }); } db.addTicket({ id: ticketChannel.id, name: ticketChannel.name, user: interaction.user.id, number: ticketNumberID, reason: TicketSubject, open: true, subUsers: [], createdAt: new Date().toISOString(), claimedAt: null, }); TicketData.tickets = db.getAllTickets(); const CreatedTicket = new EmbedBuilder() .setDescription( msgconfig.Ticket.TicketCreatedAlert.replace(/%ticketauthor%/g, interaction.user.id) .replace(/%ticketid%/g, ticketChannel.id) .replace(/%ticketusername%/g, interaction.user.username) .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() if (supportbot.Embed.Colours.General) { ticketMsgContainer.setAccentColor( parseInt(supportbot.Embed.Colours.General.replace("#", ""), 16) ) } const ticketAuthor = new TextDisplayBuilder().setContent( msgconfig.Ticket.TicketAuthorTitle.replace(/%ticketauthor%/g, interaction.user.id) .replace(/%ticketid%/g, ticketChannel.id) .replace(/%ticketusername%/g, interaction.user.username), ); 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) ); ticketMsgContainer.addTextDisplayComponents(ticketTitle) 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) ); ticketMsgContainer.addTextDisplayComponents(ticketDescription) const seperator3 = new SeparatorBuilder() .setDivider(true) ticketMsgContainer.addSeparatorComponents(seperator3) const ticketReason = new TextDisplayBuilder().setContent( `**Reason:**\n ${TicketReason}` ); 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" ? [ 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"), ); } selectMenuRow.addComponents(SelectMenus) ticketMsgContainer.addActionRowComponents(selectMenuRow) if (supportbot.Ticket.TicketReason && TicketReason) { ticketMsgContainer.addTextDisplayComponents(ticketReason) } 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); } } } catch (error) { console.error("Error in run method:", error); } }, });