| const { |
| ChannelType, |
| PermissionFlagsBits, |
| ActionRowBuilder, |
| ButtonBuilder, |
| ButtonStyle, |
| } = require('discord.js'); |
| const { createEmbed } = require('../utils/embeds'); |
| const { Colors } = require('../config'); |
| const { stmts } = require('../database'); |
| const { logTicket } = require('./logger'); |
|
|
| |
| |
| |
| async function sendTicketEmbed(client) { |
| const row = await stmts.getState('channel_π«γ»open-ticket'); |
| if (!row) throw new Error('Ticket channel not found in bot state.'); |
|
|
| const channel = await client.channels.fetch(row); |
| if (!channel) throw new Error('Could not fetch ticket channel.'); |
|
|
| const embed = createEmbed({ |
| title: 'π« Support Tickets', |
| description: [ |
| '> Need help? Open a support ticket!', |
| '', |
| 'Click the button below to create a private ticket.', |
| '', |
| '```', |
| 'β’ A private channel will be created for you', |
| 'β’ Staff will assist you as soon as possible', |
| 'β’ Only you and staff can see the ticket', |
| '```', |
| ].join('\n'), |
| color: Colors.PRIMARY, |
| }); |
|
|
| const actionRow = new ActionRowBuilder().addComponents( |
| new ButtonBuilder() |
| .setCustomId('ticket_create') |
| .setLabel('Create a Ticket') |
| .setEmoji('π«') |
| .setStyle(ButtonStyle.Primary), |
| ); |
|
|
| const msg = await channel.send({ embeds: [embed], components: [actionRow] }); |
|
|
| await stmts.setState('ticket_message_id', msg.id); |
| await stmts.setState('ticket_channel_id', channel.id); |
|
|
| return msg; |
| } |
|
|
| |
| |
| |
| async function createTicket(guild, user, client) { |
| |
| const existing = await stmts.getUserTicket(user.id, 'open'); |
| if (existing) return null; |
|
|
| const staffRole = guild.roles.cache.find(r => r.name === '@@ Staff'); |
| const ownerRole = guild.roles.cache.find(r => r.name === '@@ Owner'); |
|
|
| |
| const category = guild.channels.cache.find( |
| c => c.type === ChannelType.GuildCategory && c.name.includes('SUPPORT') |
| ); |
|
|
| const channelName = `ticket-${user.username.toLowerCase().replace(/[^a-z0-9]/g, '')}`; |
|
|
| const overwrites = [ |
| { id: guild.id, deny: [PermissionFlagsBits.ViewChannel] }, |
| { id: user.id, allow: [PermissionFlagsBits.ViewChannel, PermissionFlagsBits.SendMessages, PermissionFlagsBits.ReadMessageHistory] }, |
| ]; |
| if (staffRole) overwrites.push({ id: staffRole.id, allow: [PermissionFlagsBits.ViewChannel, PermissionFlagsBits.SendMessages, PermissionFlagsBits.ReadMessageHistory, PermissionFlagsBits.ManageMessages] }); |
| if (ownerRole) overwrites.push({ id: ownerRole.id, allow: [PermissionFlagsBits.ViewChannel, PermissionFlagsBits.SendMessages, PermissionFlagsBits.ReadMessageHistory, PermissionFlagsBits.ManageMessages] }); |
|
|
| const ticketChannel = await guild.channels.create({ |
| name: channelName, |
| type: ChannelType.GuildText, |
| parent: category?.id, |
| permissionOverwrites: overwrites, |
| }); |
|
|
| |
| await stmts.createTicket(user.id, user.tag, ticketChannel.id); |
|
|
| |
| const embed = createEmbed({ |
| title: 'π« Ticket Opened', |
| description: [ |
| `Welcome <@${user.id}>!`, |
| '', |
| 'A staff member will be with you shortly.', |
| 'Please describe your issue below.', |
| '', |
| '> Use the buttons to manage this ticket.', |
| ].join('\n'), |
| color: Colors.PRIMARY, |
| }); |
|
|
| const row = new ActionRowBuilder().addComponents( |
| new ButtonBuilder() |
| .setCustomId('ticket_close') |
| .setLabel('Close Ticket') |
| .setEmoji('π') |
| .setStyle(ButtonStyle.Secondary), |
| new ButtonBuilder() |
| .setCustomId('ticket_transcript') |
| .setLabel('Transcript') |
| .setEmoji('π') |
| .setStyle(ButtonStyle.Primary), |
| new ButtonBuilder() |
| .setCustomId('ticket_delete') |
| .setLabel('Delete Ticket') |
| .setEmoji('ποΈ') |
| .setStyle(ButtonStyle.Danger), |
| ); |
|
|
| await ticketChannel.send({ embeds: [embed], components: [row] }); |
| await logTicket(client, { user, action: 'opened', channelName }); |
|
|
| return ticketChannel; |
| } |
|
|
| |
| |
| |
| async function generateTranscript(channel) { |
| const messages = []; |
| let lastId; |
|
|
| |
| while (true) { |
| const batch = await channel.messages.fetch({ limit: 100, ...(lastId ? { before: lastId } : {}) }); |
| if (batch.size === 0) break; |
| messages.push(...batch.values()); |
| lastId = batch.last().id; |
| } |
|
|
| messages.reverse(); |
|
|
| const lines = messages.map(m => { |
| const time = m.createdAt.toISOString().replace('T', ' ').slice(0, 19); |
| return `[${time}] ${m.author.tag}: ${m.content || '(embed/attachment)'}`; |
| }); |
|
|
| return lines.join('\n') || '(no messages)'; |
| } |
|
|
| |
| |
| |
| async function handleTicketButton(interaction, client) { |
| const { customId, channel, guild, member } = interaction; |
|
|
| |
| if (customId === 'ticket_create') { |
| await interaction.deferReply({ ephemeral: true }); |
| const user = interaction.user; |
| const ticketChannel = await createTicket(guild, user, client); |
|
|
| if (!ticketChannel) { |
| await interaction.editReply({ content: 'β You already have an open ticket.' }); |
| } else { |
| await interaction.editReply({ content: `β
Ticket created: <#${ticketChannel.id}>` }); |
| } |
| return true; |
| } |
|
|
| if (!['ticket_close', 'ticket_delete', 'ticket_transcript'].includes(customId)) return false; |
|
|
| const ticket = await stmts.getTicket(channel.id); |
| if (!ticket) { |
| await interaction.reply({ content: 'β This is not a ticket channel.', ephemeral: true }); |
| return true; |
| } |
|
|
| |
| const isStaff = member.roles.cache.some(r => ['@@ Staff', '@@ Owner', '@@ Co-Owner'].includes(r.name)); |
| const isCreator = ticket.user_id === member.id; |
| if (!isStaff && !isCreator) { |
| await interaction.reply({ content: 'β You do not have permission.', ephemeral: true }); |
| return true; |
| } |
|
|
| if (customId === 'ticket_transcript') { |
| await interaction.deferReply({ ephemeral: true }); |
| const transcript = await generateTranscript(channel); |
| const buffer = Buffer.from(transcript, 'utf-8'); |
| await interaction.editReply({ |
| content: 'π Transcript generated.', |
| files: [{ attachment: buffer, name: `transcript-${channel.name}.txt` }], |
| }); |
| return true; |
| } |
|
|
| if (customId === 'ticket_close') { |
| await stmts.closeTicket('closed', channel.id); |
|
|
| |
| const transcript = await generateTranscript(channel); |
| const logsRow = await stmts.getState('channel_πγ»ticket-logs'); |
| if (logsRow) { |
| const logsChannel = await client.channels.fetch(logsRow).catch(() => null); |
| if (logsChannel) { |
| const embed = createEmbed({ |
| title: 'π Ticket Closed', |
| description: `**Ticket:** ${channel.name}\n**User:** <@${ticket.user_id}>\n**Closed by:** <@${member.id}>`, |
| color: Colors.WARNING, |
| }); |
| const buffer = Buffer.from(transcript, 'utf-8'); |
| await logsChannel.send({ |
| embeds: [embed], |
| files: [{ attachment: buffer, name: `transcript-${channel.name}.txt` }], |
| }); |
| } |
| } |
|
|
| await logTicket(client, { user: { tag: ticket.username, id: ticket.user_id }, action: 'closed', channelName: channel.name }); |
|
|
| |
| const closeEmbed = createEmbed({ |
| title: 'π Ticket Closed', |
| description: 'This ticket has been closed. The channel will be deleted in 5 seconds.', |
| color: Colors.WARNING, |
| }); |
| await interaction.reply({ embeds: [closeEmbed] }); |
| setTimeout(() => channel.delete().catch(() => { }), 5000); |
| return true; |
| } |
|
|
| if (customId === 'ticket_delete') { |
| await stmts.closeTicket('deleted', channel.id); |
| await logTicket(client, { user: { tag: ticket.username, id: ticket.user_id }, action: 'deleted', channelName: channel.name }); |
| await interaction.reply({ content: 'ποΈ Deleting ticket...' }); |
| setTimeout(() => channel.delete().catch(() => { }), 1000); |
| return true; |
| } |
|
|
| return false; |
| } |
|
|
| module.exports = { sendTicketEmbed, createTicket, handleTicketButton, generateTranscript }; |
|
|