Un bot ticket Discord permet aux utilisateurs d'ouvrir, d'un simple clic sur un bouton, un salon privé visible uniquement par eux-mêmes et l'équipe de support. Le chat général reste propre, chaque demande se déroule dans son propre salon isolé et votre équipe traite les tickets sereinement. Dans ce guide, nous allons construire de zéro un système de support piloté par bouton avec discord.js v14, étape par étape.
Comment fonctionne le système ?
La logique repose sur trois parties :
- Le message du panneau : un embed contenant un bouton « Ouvrir un ticket », envoyé dans un salon fixe.
- L'écouteur de bouton : le code qui capte l'interaction au clic et crée le salon privé.
- La fermeture : un second bouton qui ferme le salon une fois la demande résolue.
L'astuce clé réside dans les surcharges de permissions (permission overwrites) : à la création du salon, nous retirons la permission Voir le salon au rôle @everyone et ne l'accordons qu'à l'utilisateur demandeur et au rôle du staff.
Intents et permissions nécessaires
Nous n'avons pas besoin d'accéder au contenu des messages pour les boutons et la création de salons ; l'intent Guilds suffit. Dans les paramètres du serveur, donnez au rôle du bot ces permissions : Gérer les salons (Manage Channels), Voir les salons et Gérer les rôles (Manage Roles) pour qu'il puisse définir les surcharges de permissions.
const { Client, GatewayIntentBits } = require('discord.js');
const client = new Client({
intents: [GatewayIntentBits.Guilds],
});
client.once('ready', () => console.log(`Connecté en tant que ${client.user.tag}`));
client.login(process.env.TOKEN);
Envoyer le message du panneau
Construisons d'abord le panneau que verront les utilisateurs. Vous pouvez l'envoyer via une commande simple ou une seule fois au démarrage. Le customId du bouton est crucial : c'est lui qui nous permettra d'identifier l'interaction.
const {
ActionRowBuilder, ButtonBuilder, ButtonStyle, EmbedBuilder,
} = require('discord.js');
const embed = new EmbedBuilder()
.setTitle('🎫 Support')
.setDescription("Besoin d'aide ? Cliquez sur le bouton ci-dessous pour ouvrir votre propre salon de support privé.")
.setColor(0x5865f2);
const row = new ActionRowBuilder().addComponents(
new ButtonBuilder()
.setCustomId('ticket_create')
.setLabel('Ouvrir un ticket')
.setEmoji('🎫')
.setStyle(ButtonStyle.Primary),
);
await panelChannel.send({ embeds: [embed], components: [row] });
Ouvrir un salon privé au clic
C'est le cœur du système. Dans l'événement interactionCreate, nous captons le bouton, vérifions si l'utilisateur a déjà un ticket ouvert et créons un salon privé avec les surcharges de permissions. Passer un ID de catégorie à parent est une bonne idée pour regrouper tous les tickets sous une même catégorie.
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;
// Cet utilisateur a-t-il déjà un ticket ouvert ?
const existing = guild.channels.cache.find(
(c) => c.name === `ticket-${user.id}`,
);
if (existing) {
return interaction.reply({
content: `Tu as déjà un ticket ouvert : ${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('Fermer le ticket')
.setEmoji('🔒')
.setStyle(ButtonStyle.Danger),
);
await channel.send({
content: `${user} bonjour ! L'équipe <@&${STAFF_ROLE_ID}> te répondra sous peu. Décris ton problème en détail.`,
components: [closeRow],
});
await interaction.reply({
content: `Ton salon de support est prêt : ${channel}`,
ephemeral: true,
});
});
Utiliser user.id plutôt que le pseudo dans le nom du salon est important : les pseudos peuvent changer, contenir des caractères spéciaux et rendre les contrôles de doublons peu fiables. L'ID, lui, est unique et stable.
Fermer un ticket
Pour le bouton de fermeture, nous ajoutons une seconde condition dans le même gestionnaire interactionCreate. Nous prévenons brièvement l'utilisateur puis supprimons le salon quelques secondes plus tard.
if (interaction.isButton() && interaction.customId === 'ticket_close') {
await interaction.reply({
content: 'Fermeture du ticket dans 5 secondes...',
});
setTimeout(() => {
interaction.channel.delete().catch(() => null);
}, 5000);
}
Dans des systèmes plus aboutis, vous pourriez déplacer le salon vers une catégorie d'archives au lieu de le supprimer, envoyer une transcription dans un salon de logs, ou exiger une confirmation du staff avant la fermeture. La suppression suffit pour démarrer.
Pièges fréquents
- Rôle du bot trop bas : le rôle du bot doit être assez haut dans la liste des rôles pour gérer les permissions qu'il accorde, sinon la création du salon échoue avec
Missing Permissions. - Délai d'interaction : Discord attend une réponse en moins de 3 secondes. Si la création du salon est lente, appelez d'abord
interaction.deferReply({ ephemeral: true })puis terminez avecinteraction.editReply(...). - Limite de salons par catégorie : une catégorie contient au plus 50 salons. Sur les serveurs actifs, nettoyez régulièrement les tickets fermés.
Questions fréquentes
Dois-je activer l'intent Message Content ?
Non. L'intent Guilds suffit pour les interactions de bouton et la création de salons. N'activez l'intent privilégié MessageContent que si vous devez lire le texte des messages dans le salon.
Comment empêcher un utilisateur d'ouvrir deux tickets à la fois ?
Nommez les salons ticket-${user.id} et vérifiez dans le cache l'existence d'un salon portant ce nom avant d'en créer un nouveau. S'il existe, redirigez l'utilisateur vers son salon actuel.
Comment enregistrer une transcription ?
Avant la fermeture, récupérez les messages du salon avec channel.messages.fetch() et écrivez-les dans un fichier texte/HTML, ou utilisez un paquet comme discord-html-transcripts et envoyez le fichier dans un salon de logs.
Vous voulez un système de tickets professionnel pour votre serveur ? Je peux développer une solution sur mesure avec transcriptions, tickets par catégorie, statistiques du staff et interface multilingue — contactez-moi.