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?