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'); /** * Send the ticket embed with a "Create a Ticket" button to πŸŽ«γƒ»open-ticket channel. */ 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; } /** * Create a ticket channel for a user. */ async function createTicket(guild, user, client) { // Check if user already has an open ticket 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'); // Find the SUPPORT & TICKETS category 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, }); // Save to database await stmts.createTicket(user.id, user.tag, ticketChannel.id); // Send welcome embed with action buttons 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; } /** * Generate a transcript of a ticket channel. */ async function generateTranscript(channel) { const messages = []; let lastId; // Fetch all messages (paginated) 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)'; } /** * Handle ticket button interactions. */ async function handleTicketButton(interaction, client) { const { customId, channel, guild, member } = interaction; // Handle "Create a Ticket" button 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; } // Permission check: only staff, owner, or ticket creator 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); // Save transcript to ticket-logs 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 }); // Delete the channel after a short delay 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 };