Ein discord ticket bot ermöglicht es Nutzern, mit einem einzigen Button-Klick einen privaten Kanal zu öffnen, der nur für sie selbst und das Support-Team sichtbar ist. Der allgemeine Chat bleibt aufgeräumt, jede Anfrage läuft in einem eigenen, isolierten Kanal, und dein Team kann die Tickets in Ruhe abarbeiten. In dieser Anleitung bauen wir mit discord.js v14 Schritt für Schritt ein buttongesteuertes Support-System von Grund auf.
So funktioniert das System
Die Logik besteht aus drei Teilen:
- Die Panel-Nachricht: ein Embed mit einem "Ticket öffnen"-Button, das in einen festen Kanal gepostet wird.
- Der Button-Listener: Code, der die Interaktion abfängt, wenn ein Nutzer klickt, und den privaten Kanal erstellt.
- Das Schließen: ein zweiter Button, der den Kanal schließt, sobald die Anfrage erledigt ist.
Der entscheidende Trick sind Berechtigungs-Overrides (permission overwrites): Beim Erstellen des Kanals entziehen wir der Rolle @everyone die Berechtigung "Kanal ansehen" und gewähren sie nur dem anfragenden Nutzer und der Support-Rolle.
Benötigte Intents und Berechtigungen
Für Buttons und das Erstellen von Kanälen brauchen wir keinen Zugriff auf Nachrichteninhalte; der Guilds-Intent allein genügt. Gib der Rolle des Bots in den Servereinstellungen folgende Berechtigungen: Kanäle verwalten (Manage Channels), Kanäle ansehen und Rollen verwalten (Manage Roles), damit er Berechtigungs-Overrides setzen kann.
const { Client, GatewayIntentBits } = require('discord.js');
const client = new Client({
intents: [GatewayIntentBits.Guilds],
});
client.once('ready', () => console.log(`Angemeldet als ${client.user.tag}`));
client.login(process.env.TOKEN);
Die Panel-Nachricht senden
Bauen wir zuerst das Panel, das die Nutzer sehen. Du kannst es mit einem einfachen Befehl oder einmalig beim Start senden. Die customId des Buttons ist entscheidend, denn darüber identifizieren wir die Interaktion.
const {
ActionRowBuilder, ButtonBuilder, ButtonStyle, EmbedBuilder,
} = require('discord.js');
const embed = new EmbedBuilder()
.setTitle('🎫 Support')
.setDescription('Brauchst du Hilfe? Klicke auf den Button unten, um deinen eigenen privaten Support-Kanal zu öffnen.')
.setColor(0x5865f2);
const row = new ActionRowBuilder().addComponents(
new ButtonBuilder()
.setCustomId('ticket_create')
.setLabel('Ticket öffnen')
.setEmoji('🎫')
.setStyle(ButtonStyle.Primary),
);
await panelChannel.send({ embeds: [embed], components: [row] });
Beim Klick einen privaten Kanal öffnen
Das ist der Kern. Im interactionCreate-Event fangen wir den Button ab, prüfen, ob der Nutzer bereits ein offenes Ticket hat, und erstellen einen privaten Kanal mit Berechtigungs-Overrides. Eine Kategorie-ID an parent zu übergeben ist sinnvoll, damit alle Tickets unter einer Kategorie liegen.
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;
// Hat dieser Nutzer bereits ein offenes Ticket?
const existing = guild.channels.cache.find(
(c) => c.name === `ticket-${user.id}`,
);
if (existing) {
return interaction.reply({
content: `Du hast bereits ein offenes 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 schließen')
.setEmoji('🔒')
.setStyle(ButtonStyle.Danger),
);
await channel.send({
content: `${user} hallo! Das Team <@&${STAFF_ROLE_ID}> kümmert sich gleich um dich. Bitte beschreibe dein Anliegen ausführlich.`,
components: [closeRow],
});
await interaction.reply({
content: `Dein Support-Kanal ist bereit: ${channel}`,
ephemeral: true,
});
});
Im Kanalnamen user.id statt des Benutzernamens zu verwenden ist wichtig: Benutzernamen können sich ändern, Sonderzeichen enthalten und machen die Duplikatprüfung unzuverlässig. Die ID ist eindeutig und stabil.
Ein Ticket schließen
Für den Schließen-Button fügen wir eine zweite Bedingung in denselben interactionCreate-Handler ein. Wir geben dem Nutzer einen kurzen Hinweis und löschen den Kanal wenige Sekunden später.
if (interaction.isButton() && interaction.customId === 'ticket_close') {
await interaction.reply({
content: 'Dieses Ticket wird in 5 Sekunden geschlossen...',
});
setTimeout(() => {
interaction.channel.delete().catch(() => null);
}, 5000);
}
In ausgereifteren Systemen kannst du den Kanal statt ihn zu löschen in eine Archiv-Kategorie verschieben, ein Transkript an einen Log-Kanal senden oder vor dem Schließen eine Bestätigung durch das Team verlangen. Zum Einstieg genügt das Löschen.
Häufige Stolperfallen
- Bot-Rolle zu niedrig: Die Rolle des Bots muss in der Rollenliste hoch genug stehen, um die Berechtigungen verwalten zu können, die er vergibt, sonst schlägt die Kanalerstellung mit
Missing Permissionsfehl. - Interaktions-Timeout: Discord erwartet eine Antwort innerhalb von 3 Sekunden. Wenn die Kanalerstellung langsam ist, rufe zuerst
interaction.deferReply({ ephemeral: true })auf und schließe mitinteraction.editReply(...)ab. - Kanallimit pro Kategorie: Eine Kategorie fasst höchstens 50 Kanäle. Räume auf stark frequentierten Servern geschlossene Tickets regelmäßig auf.
Häufig gestellte Fragen
Muss ich den Message-Content-Intent aktivieren?
Nein. Der Guilds-Intent reicht für Button-Interaktionen und Kanalerstellung aus. Aktiviere den privilegierten MessageContent-Intent nur, wenn du tatsächlich den Text der Nachrichten im Kanal lesen musst.
Wie verhindere ich, dass ein Nutzer gleichzeitig zwei Tickets öffnet?
Benenne Kanäle nach dem Schema ticket-${user.id} und prüfe im Cache, ob bereits ein Kanal mit diesem Namen existiert, bevor du einen neuen erstellst. Falls ja, verweise den Nutzer auf seinen aktuellen Kanal.
Wie speichere ich ein Transkript?
Hole vor dem Schließen die Nachrichten des Kanals mit channel.messages.fetch() und schreibe sie in eine Text-/HTML-Datei, oder nutze ein Paket wie discord-html-transcripts und sende die Datei an einen Log-Kanal.
Möchtest du ein professionelles Ticket-System für deinen Server? Ich entwickle eine maßgeschneiderte Lösung mit Transkripten, kategoriebasierten Tickets, Team-Statistiken und mehrsprachiger Oberfläche — nimm Kontakt auf.