wsb-bot / src /systems /tickets.js
APRK01
Premium Redesign: System Modules and Surgical Layout
5fb7488
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 };