Met een discord ticketbot kunnen gebruikers met één klik op een knop een privékanaal openen dat alleen voor henzelf en het supportteam zichtbaar is. De algemene chat blijft overzichtelijk, elk verzoek loopt in een eigen geïsoleerd kanaal en je team werkt rustig de tickets af. In deze gids bouwen we stap voor stap een knopgestuurd supportsysteem vanaf nul met discord.js v14.
Hoe werkt het systeem?
De logica bestaat uit drie delen:
- Het paneelbericht: een embed met een knop "Ticket openen", geplaatst in een vast kanaal.
- De knopluisteraar: code die de interactie opvangt zodra een gebruiker klikt en het privékanaal aanmaakt.
- Sluiten: een tweede knop die het kanaal sluit zodra het verzoek is opgelost.
De sleuteltruc zijn permissie-overrides (permission overwrites): bij het aanmaken van het kanaal ontzeggen we de rol @everyone de rechten om het kanaal te zien en geven we die alleen aan de aanvragende gebruiker en de staffrol.
Vereiste intents en rechten
Voor knoppen en het aanmaken van kanalen hebben we geen toegang tot berichtinhoud nodig; de Guilds-intent alleen volstaat. Geef de rol van de bot in de serverinstellingen deze rechten: Kanalen beheren (Manage Channels), Kanalen bekijken en Rollen beheren (Manage Roles) zodat hij permissie-overrides kan instellen.
const { Client, GatewayIntentBits } = require('discord.js');
const client = new Client({
intents: [GatewayIntentBits.Guilds],
});
client.once('ready', () => console.log(`Ingelogd als ${client.user.tag}`));
client.login(process.env.TOKEN);
Het paneelbericht versturen
Laten we eerst het paneel bouwen dat gebruikers te zien krijgen. Je kunt het versturen met een eenvoudig commando of eenmalig bij het opstarten. De customId van de knop is cruciaal: daarmee herkennen we de interactie.
const {
ActionRowBuilder, ButtonBuilder, ButtonStyle, EmbedBuilder,
} = require('discord.js');
const embed = new EmbedBuilder()
.setTitle('🎫 Support')
.setDescription('Hulp nodig? Klik op de knop hieronder om je eigen privé-supportkanaal te openen.')
.setColor(0x5865f2);
const row = new ActionRowBuilder().addComponents(
new ButtonBuilder()
.setCustomId('ticket_create')
.setLabel('Ticket openen')
.setEmoji('🎫')
.setStyle(ButtonStyle.Primary),
);
await panelChannel.send({ embeds: [embed], components: [row] });
Een privékanaal openen bij een klik
Dit is de kern. In het interactionCreate-event vangen we de knop op, controleren of de gebruiker al een open ticket heeft en maken een privékanaal met permissie-overrides. Een categorie-ID meegeven aan parent is een goed idee, zodat alle tickets onder één categorie vallen.
const { Events, ChannelType, PermissionFlagsBits } = require('discord.js');
const STAFF_ROLE_ID = '123456789012345678';
const TICKET_CATEGORY_ID = '987654321098765432';
client.on(Events.InteractionCreate, async (interaction) => {
if (!interaction.isButton()) return;
if (interaction.customId !== 'ticket_create') return;
const { guild, user } = interaction;
// Heeft deze gebruiker al een open ticket?
const existing = guild.channels.cache.find(
(c) => c.name === `ticket-${user.id}`,
);
if (existing) {
return interaction.reply({
content: `Je hebt al een open ticket: ${existing}`,
ephemeral: true,
});
}
const channel = await guild.channels.create({
name: `ticket-${user.id}`,
type: ChannelType.GuildText,
parent: TICKET_CATEGORY_ID,
permissionOverwrites: [
{ id: guild.id, deny: [PermissionFlagsBits.ViewChannel] },
{
id: user.id,
allow: [
PermissionFlagsBits.ViewChannel,
PermissionFlagsBits.SendMessages,
PermissionFlagsBits.AttachFiles,
],
},
{
id: STAFF_ROLE_ID,
allow: [
PermissionFlagsBits.ViewChannel,
PermissionFlagsBits.SendMessages,
],
},
],
});
const closeRow = new ActionRowBuilder().addComponents(
new ButtonBuilder()
.setCustomId('ticket_close')
.setLabel('Ticket sluiten')
.setEmoji('🔒')
.setStyle(ButtonStyle.Danger),
);
await channel.send({
content: `${user} hoi! Het <@&${STAFF_ROLE_ID}>-team helpt je zo snel mogelijk. Beschrijf je probleem zo gedetailleerd mogelijk.`,
components: [closeRow],
});
await interaction.reply({
content: `Je supportkanaal is klaar: ${channel}`,
ephemeral: true,
});
});
user.id gebruiken in plaats van de gebruikersnaam in de kanaalnaam is belangrijk: gebruikersnamen kunnen veranderen, bevatten soms speciale tekens en maken duplicaatcontroles onbetrouwbaar. De ID is uniek en stabiel.
Een ticket sluiten
Voor de sluitknop voegen we een tweede voorwaarde toe in dezelfde interactionCreate-handler. We geven de gebruiker een korte melding en verwijderen het kanaal een paar seconden later.
if (interaction.isButton() && interaction.customId === 'ticket_close') {
await interaction.reply({
content: 'Ticket wordt over 5 seconden gesloten...',
});
setTimeout(() => {
interaction.channel.delete().catch(() => null);
}, 5000);
}
In volwassener systemen verplaats je het kanaal misschien naar een archiefcategorie in plaats van het te verwijderen, stuur je een transcript naar een logkanaal, of vereis je een bevestiging van de staff voor het sluiten. Verwijderen volstaat om te beginnen.
Veelgemaakte fouten
- Botrol te laag: de rol van de bot moet hoog genoeg in de rollenlijst staan om de rechten te beheren die hij toekent, anders mislukt het aanmaken van het kanaal met
Missing Permissions. - Interactie-timeout: Discord verwacht binnen 3 seconden een antwoord. Is het aanmaken van het kanaal traag, roep dan eerst
interaction.deferReply({ ephemeral: true })aan en sluit af metinteraction.editReply(...). - Kanaallimiet per categorie: een categorie bevat maximaal 50 kanalen. Ruim op drukke servers gesloten tickets regelmatig op.
Veelgestelde vragen
Moet ik de Message Content-intent inschakelen?
Nee. De Guilds-intent volstaat voor knopinteracties en het aanmaken van kanalen. Schakel de geprivilegieerde MessageContent-intent alleen in als je de tekst van berichten in het kanaal moet lezen.
Hoe voorkom ik dat een gebruiker twee tickets tegelijk opent?
Geef kanalen de naam ticket-${user.id} en controleer de cache op een bestaand kanaal met die naam voordat je een nieuw kanaal aanmaakt. Bestaat het al, verwijs de gebruiker dan naar zijn huidige kanaal.
Hoe sla ik een transcript op?
Haal voor het sluiten de berichten van het kanaal op met channel.messages.fetch() en schrijf ze naar een tekst-/HTML-bestand, of gebruik een pakket als discord-html-transcripts en stuur het bestand naar een logkanaal.
Wil je een professioneel ticketsysteem voor je server? Ik bouw een maatwerkoplossing met transcripts, categoriegebaseerde tickets, staffstatistieken en een meertalige interface — neem contact op.