Encontranos en redes
TicketsSoporteBotones7 min de lectura
Bot de tickets y soporte
Sistema de tickets con botones, generación de transcripts, categorías, roles de staff y auto-cierre.
Arquitectura del sistema de tickets
Un sistema de tickets crea canales privados temporales donde los usuarios pueden hablar con el staff. Incluye: panel con botones, creación de canal, transcript y cierre automático.
Paso 1: Panel de tickets con botones
javascript
const { ActionRowBuilder, ButtonBuilder, ButtonStyle, EmbedBuilder } = require('discord.js');
// Comando para crear el panel
async function createTicketPanel(channel) {
const embed = new EmbedBuilder()
.setTitle('🎫 Centro de Soporte')
.setDescription('Hacé clic en el botón correspondiente para abrir un ticket.')
.setColor(0x5865f2)
.addFields(
{ name: '💬 Consulta General', value: 'Preguntas sobre el servicio', inline: true },
{ name: '🐛 Reporte de Bug', value: 'Reportar un problema técnico', inline: true },
{ name: '💰 Facturación', value: 'Consultas sobre pagos', inline: true }
);
const row = new ActionRowBuilder().addComponents(
new ButtonBuilder()
.setCustomId('ticket_general')
.setLabel('Consulta General')
.setEmoji('💬')
.setStyle(ButtonStyle.Primary),
new ButtonBuilder()
.setCustomId('ticket_bug')
.setLabel('Reporte de Bug')
.setEmoji('🐛')
.setStyle(ButtonStyle.Danger),
new ButtonBuilder()
.setCustomId('ticket_billing')
.setLabel('Facturación')
.setEmoji('💰')
.setStyle(ButtonStyle.Success)
);
await channel.send({ embeds: [embed], components: [row] });
}Paso 2: Crear el canal del ticket
javascript
client.on('interactionCreate', async interaction => {
if (!interaction.isButton()) return;
if (!interaction.customId.startsWith('ticket_')) return;
const category = interaction.customId.replace('ticket_', '');
const guild = interaction.guild;
// Verificar si ya tiene un ticket abierto
const existing = guild.channels.cache.find(
c => c.topic === `ticket-${interaction.user.id}`
);
if (existing) {
return interaction.reply({
content: `Ya tenés un ticket abierto: ${existing}`,
ephemeral: true
});
}
// Crear canal
const ticketChannel = await guild.channels.create({
name: `ticket-${interaction.user.username}`,
topic: `ticket-${interaction.user.id}`,
parent: process.env.TICKETS_CATEGORY_ID,
permissionOverwrites: [
{ id: guild.id, deny: ['ViewChannel'] },
{ id: interaction.user.id, allow: ['ViewChannel', 'SendMessages'] },
{ id: process.env.STAFF_ROLE_ID, allow: ['ViewChannel', 'SendMessages'] }
]
});
const welcomeEmbed = new EmbedBuilder()
.setTitle(`Ticket - ${category}`)
.setDescription(`Hola ${interaction.user}, un miembro del staff te va a atender pronto.\nDescribí tu consulta con el mayor detalle posible.`)
.setColor(0x5865f2)
.setTimestamp();
const closeRow = new ActionRowBuilder().addComponents(
new ButtonBuilder()
.setCustomId('ticket_close')
.setLabel('Cerrar Ticket')
.setEmoji('🔒')
.setStyle(ButtonStyle.Danger)
);
await ticketChannel.send({ embeds: [welcomeEmbed], components: [closeRow] });
await interaction.reply({ content: `Ticket creado: ${ticketChannel}`, ephemeral: true });
});Paso 3: Cerrar ticket y generar transcript
javascript
async function closeTicket(channel, closedBy) {
// Generar transcript
const messages = await channel.messages.fetch({ limit: 100 });
const transcript = messages.reverse().map(m =>
`[${m.createdAt.toISOString()}] ${m.author.tag}: ${m.content}`
).join('\n');
// Guardar transcript
const fs = require('fs');
const filename = `transcript-${channel.name}-${Date.now()}.txt`;
fs.writeFileSync(`./transcripts/${filename}`, transcript);
// Enviar al canal de logs
const logChannel = channel.guild.channels.cache.find(c => c.name === 'ticket-logs');
if (logChannel) {
await logChannel.send({
content: `📋 Ticket cerrado por ${closedBy.tag}`,
files: [`./transcripts/${filename}`]
});
}
// Eliminar el canal después de 5 segundos
await channel.send('🔒 Ticket cerrado. Este canal se eliminará en 5 segundos.');
setTimeout(() => channel.delete(), 5000);
}Paso 4: Auto-cierre de tickets inactivos
javascript
// Revisar cada hora
setInterval(async () => {
const ticketCategory = client.channels.cache.get(process.env.TICKETS_CATEGORY_ID);
if (!ticketCategory) return;
for (const [, channel] of ticketCategory.children.cache) {
const lastMessage = (await channel.messages.fetch({ limit: 1 })).first();
if (!lastMessage) continue;
const hoursSinceLastMessage = (Date.now() - lastMessage.createdTimestamp) / 3600000;
if (hoursSinceLastMessage > 48) {
await channel.send('⏰ Este ticket se cerrará por inactividad en 1 hora. Enviá un mensaje para mantenerlo abierto.');
// Programar cierre en 1 hora si no hay respuesta
}
}
}, 3600000);Recomendaciones
- Limitá a 1-2 tickets abiertos por usuario
- Guardá transcripts en base de datos para búsqueda posterior
- Agregá un sistema de calificación al cerrar el ticket
- Usá threads en vez de canales si preferís menos desorden
- Notificá al staff con un ping cuando se abre un ticket nuevo
¿Te resultó útil esta guía?