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 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.CloseTicket.Command, description: cmdconfig.CloseTicket.Description, type: Discord.ApplicationCommandType.ChatInput, options: [ { name: "reason", description: "Ticket Close Reason", type: Discord.ApplicationCommandOptionType.String, }, ], permissions: cmdconfig.CloseTicket.Permission, async run(interaction) { const { getRole, getChannel } = 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); if (!SupportStaff || !Admin) return interaction.reply("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}\``) .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] }); } // Check if it's a thread or channel based on config if ( (supportbot.Ticket.TicketType === "threads" && interaction.channel.type !== Discord.ChannelType.PrivateThread) || (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 }); } await interaction.deferReply(); let tickets = JSON.parse(fs.readFileSync("./Data/TicketData.json", "utf8")); let TicketData = tickets.tickets.findIndex((t) => t.id === interaction.channel.id); let ticket = tickets.tickets[TicketData]; if (TicketData === -1) { const Exists = new Discord.EmbedBuilder() .setTitle("No Ticket Found!") .setDescription(msgconfig.Error.NoValidTicket) .setColor(supportbot.Embed.Colours.Warn); return interaction.followUp({ embeds: [Exists] }); } let reason = interaction.options?.getString("reason") || "No Reason Provided."; 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 row = new Discord.ActionRowBuilder().addComponents(CloseButton); const CloseTicketRequest = new Discord.EmbedBuilder() .setTitle(`**${supportbot.Ticket.Close.Title}**`) .setDescription(`${msgconfig.Ticket.ConfirmClose}`) .setColor(supportbot.Embed.Colours.General); await interaction.followUp({ embeds: [CloseTicketRequest], components: [row], }); const filter = (btnInteraction) => btnInteraction.customId === "confirmCloseTicket" && btnInteraction.user.id === interaction.user.id; interaction.channel.awaitMessageComponent({ filter, time: 20000 }) .then(async (btnInteraction) => { await handleCloseTicket(interaction, reason, ticket, TicketData); await btnInteraction.reply({ content: "Ticket successfully closed.", ephemeral: true }); }) .catch((err) => { interaction.followUp(`${msgconfig.Ticket.CloseTimeout}`); console.error(err); }); }, }); async function handleCloseTicket(interaction, reason, ticket, TicketData) { const { getChannel } = interaction.client; let tickets = JSON.parse(fs.readFileSync("./Data/TicketData.json", "utf8")); let tUser = interaction.client.users.cache.get(ticket.user); let transcriptChannel = await getChannel(supportbot.Ticket.Log.TicketDataLog, interaction.guild); if (!transcriptChannel) return interaction.followUp("Some Channels seem to be missing!"); try { tickets.tickets[TicketData].open = false; fs.writeFileSync( "./Data/TicketData.json", JSON.stringify(tickets, null, 4), (err) => { if (err) console.error(err); } ); const transcriptEmbed = new Discord.EmbedBuilder() .setTitle(msgconfig.TicketLog.Title) .setColor(msgconfig.TicketLog.Colour) .setFooter({ text: supportbot.Embed.Footer, iconURL: interaction.user.displayAvatarURL(), }) .addFields( { name: "Ticket", value: `${interaction.channel.name} (${interaction.channel.id})` }, { name: "User", value: `${tUser?.username || "N/A"}#${tUser?.discriminator || "N/A"} (${tUser?.id || ticket.user})` }, { name: "Closed By", value: interaction.user.tag }, { name: "Reason", value: reason } ); let msgs = await interaction.channel.messages.fetch({ limit: 100 }); let html = `

Ticket Transcript

Server Name: ${interaction.guild.name}

Ticket: ${interaction.channel.name}

Messages: ${msgs.size} Messages

`; msgs = msgs.sort((a, b) => a.createdTimestamp - b.createdTimestamp); msgs.forEach((msg) => { if (msg.content) { html += `
Avatar

${msg.author.tag}

${msg.createdAt.toLocaleString()}

${convertMarkdownToHTML(msg.content)}

`; } }); const fileName = `${interaction.channel.name}.html`; html += `
Download Transcript
`; let file = new Discord.AttachmentBuilder( Buffer.from(html), { name: fileName } ); await transcriptChannel .send({ embeds: [transcriptEmbed], files: [file] }) .catch((err) => { console.error(err); }); if (supportbot.Ticket.DMTranscripts) { await tUser ?.send({ embeds: [transcriptEmbed], files: [file] }) .catch((err) => { console.error(err); }); if (ticket.subUsers) { ticket.subUsers.forEach(async (subUser) => { await interaction.client.users.cache .get(subUser) ?.send({ embeds: [transcriptEmbed], files: [file] }); }); } } await interaction.channel.delete().catch((error) => { console.error(error); if (error.code !== Discord.Constants.APIErrors.UNKNOWN_CHANNEL) { console.error("Failed to delete the interaction:", error); } }); } catch (error) { console.error(error); } } function convertMarkdownToHTML(text) { // Convert **bold** to bold text = text.replace(/\*\*(.*?)\*\*/g, '$1'); // Convert *italic* to italic text = text.replace(/\*(.*?)\*/g, '$1'); // Convert __underline__ to underline text = text.replace(/__(.*?)__/g, '$1'); // Convert ~~strikethrough~~ to strikethrough text = text.replace(/~~(.*?)~~/g, '$1'); // Convert inline code `code` to code text = text.replace(/`(.*?)`/g, '$1'); // Convert blockquotes > quote to
quote
text = text.replace(/^> (.*?$)/gm, '
$1
'); // Convert multiline code blocks ```code``` to
code
text = text.replace(/```(.*?)```/gs, '
$1
'); // Convert line breaks to
text = text.replace(/\n/g, '
'); return text; }