TicketsSupportButtons7 min read

Ticket and support bot

Ticket system with buttons, transcript generation, categories, staff roles and auto-close inactive tickets.


Ticket system architecture

A ticket system creates temporary private channels where users can talk to staff. It includes: button panel, channel creation, transcript and automatic closing.

Step 1: Ticket panel with buttons

javascript
const { ActionRowBuilder, ButtonBuilder, ButtonStyle, EmbedBuilder } = require('discord.js');

// Command to create the panel
async function createTicketPanel(channel) {
  const embed = new EmbedBuilder()
    .setTitle('Support Center')
    .setDescription('Click the appropriate button to open a ticket.')
    .setColor(0x5865f2)
    .addFields(
      { name: 'General Inquiry', value: 'Questions about the service', inline: true },
      { name: 'Bug Report', value: 'Report a technical issue', inline: true },
      { name: 'Billing', value: 'Payment inquiries', inline: true }
    );

  const row = new ActionRowBuilder().addComponents(
    new ButtonBuilder()
      .setCustomId('ticket_general')
      .setLabel('General Inquiry')
      .setEmoji('πŸ’¬')
      .setStyle(ButtonStyle.Primary),
    new ButtonBuilder()
      .setCustomId('ticket_bug')
      .setLabel('Bug Report')
      .setEmoji('πŸ›')
      .setStyle(ButtonStyle.Danger),
    new ButtonBuilder()
      .setCustomId('ticket_billing')
      .setLabel('Billing')
      .setEmoji('πŸ’°')
      .setStyle(ButtonStyle.Success)
  );

  await channel.send({ embeds: [embed], components: [row] });
}

Step 2: Create the ticket channel

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;

  // Check if user already has an open ticket
  const existing = guild.channels.cache.find(
    c => c.topic === `ticket-${interaction.user.id}`
  );
  if (existing) {
    return interaction.reply({
      content: `You already have an open ticket: ${existing}`,
      ephemeral: true
    });
  }

  // Create channel
  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(`Hello ${interaction.user}, a staff member will assist you shortly.\nPlease describe your issue in as much detail as possible.`)
    .setColor(0x5865f2)
    .setTimestamp();

  const closeRow = new ActionRowBuilder().addComponents(
    new ButtonBuilder()
      .setCustomId('ticket_close')
      .setLabel('Close Ticket')
      .setEmoji('πŸ”’')
      .setStyle(ButtonStyle.Danger)
  );

  await ticketChannel.send({ embeds: [welcomeEmbed], components: [closeRow] });
  await interaction.reply({ content: `Ticket created: ${ticketChannel}`, ephemeral: true });
});

Step 3: Close ticket and generate transcript

javascript
async function closeTicket(channel, closedBy) {
  // Generate 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');

  // Save transcript
  const fs = require('fs');
  const filename = `transcript-${channel.name}-${Date.now()}.txt`;
  fs.writeFileSync(`./transcripts/${filename}`, transcript);

  // Send to log channel
  const logChannel = channel.guild.channels.cache.find(c => c.name === 'ticket-logs');
  if (logChannel) {
    await logChannel.send({
      content: `Ticket closed by ${closedBy.tag}`,
      files: [`./transcripts/${filename}`]
    });
  }

  // Delete the channel after 5 seconds
  await channel.send('Ticket closed. This channel will be deleted in 5 seconds.');
  setTimeout(() => channel.delete(), 5000);
}

Step 4: Auto-close inactive tickets

javascript
// Check every hour
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('This ticket will be closed due to inactivity in 1 hour. Send a message to keep it open.');
      // Schedule close in 1 hour if no response
    }
  }
}, 3600000);

Recommendations

  • Limit to 1-2 open tickets per user
  • Store transcripts in a database for later searching
  • Add a rating system when closing tickets
  • Use threads instead of channels if you prefer less clutter
  • Notify staff with a ping when a new ticket is opened

Was this guide helpful?