Un bot de logs Discord est l'un des outils les plus précieux à ajouter à un serveur : il enregistre discrètement ce qui s'y passe et offre à votre équipe de modération une véritable piste d'audit. Qui a supprimé quel message, qui a quitté le serveur, quand un rôle a été attribué à un membre — voir tout cela dans un salon permanent fait passer les conflits du « ce n'est pas moi » à une modération fondée sur des preuves. Dans ce guide, nous allons construire un bot pratique avec discord.js v14 qui journalise les suppressions de messages, les arrivées et départs de membres, ainsi que les changements de rôle.
À quoi sert un bot de logs Discord ?
Le journal d'audit intégré de Discord ne conserve que certaines actions administratives pendant une durée limitée et n'affiche jamais le contenu des messages. En écrivant votre propre bot de logs, vous décidez quoi enregistrer, comment et où. Les cas d'usage typiques sont :
- Audit des messages : conserver le contenu des messages supprimés et modifiés.
- Activité des membres : suivre les arrivées et départs avec l'âge du compte (utile pour détecter les raids).
- Suivi des permissions : voir les attributions et retraits de rôles, et qui les a effectués.
Nous enverrons tous ces événements vers un seul salon #logs sous forme d'embeds lisibles.
Intents requis et configuration des partials
La journalisation nécessite des intents privilégiés. GuildMembers est requis pour les événements de membres et MessageContent pour lire le contenu des messages. Vous devez aussi activer ces deux options dans les paramètres de votre application sur le Discord Developer Portal. Pour capter les événements de messages anciens absents du cache, il faut déclarer les partials :
// index.js
const { Client, GatewayIntentBits, Partials } = require('discord.js');
const client = new Client({
intents: [
GatewayIntentBits.Guilds,
GatewayIntentBits.GuildMembers,
GatewayIntentBits.GuildMessages,
GatewayIntentBits.MessageContent,
],
partials: [Partials.Message, Partials.Channel, Partials.GuildMember],
});
const LOG_CHANNEL_ID = '123456789012345678';
client.login(process.env.TOKEN);
Pour éviter les répétitions, définissons une petite fonction utilitaire réutilisée par chaque événement :
const { EmbedBuilder } = require('discord.js');
function log(guild, embed) {
const channel = guild.channels.cache.get(LOG_CHANNEL_ID);
if (channel) channel.send({ embeds: [embed] }).catch(() => {});
}
Journaliser les suppressions et modifications de messages
On capte les suppressions avec l'événement Events.MessageDelete. Il existe une limitation importante : si un message envoyé avant le démarrage du bot — et absent du cache — est supprimé, l'événement n'arrive qu'en partial et vous ne pouvez pas récupérer son contenu, car le message n'existe plus sur Discord. On ignore donc proprement les messages partials :
const { Events } = require('discord.js');
client.on(Events.MessageDelete, async (message) => {
if (message.partial) return; // contenu indisponible si non mis en cache
if (message.author?.bot) return;
log(message.guild, new EmbedBuilder()
.setColor(0xED4245)
.setAuthor({ name: message.author.tag, iconURL: message.author.displayAvatarURL() })
.setDescription(
`🗑️ **Message supprimé** — <#${message.channel.id}>\n` +
(message.content || '*Aucun contenu*'),
)
.setTimestamp());
});
On écoute les modifications avec Events.MessageUpdate. Cet événement se déclenche aussi quand un aperçu de lien (embed) se charge sur un message ; pour éviter le bruit, on sort tôt si le contenu n'a pas réellement changé :
client.on(Events.MessageUpdate, async (oldMsg, newMsg) => {
if (oldMsg.partial || newMsg.author?.bot) return;
if (oldMsg.content === newMsg.content) return; // seul un embed s'est chargé
log(newMsg.guild, new EmbedBuilder()
.setColor(0xFEE75C)
.setAuthor({ name: newMsg.author.tag, iconURL: newMsg.author.displayAvatarURL() })
.setDescription(`✏️ **Message modifié** — [Aller au message](${newMsg.url})`)
.addFields(
{ name: 'Avant', value: (oldMsg.content || '—').slice(0, 1024) },
{ name: 'Après', value: (newMsg.content || '—').slice(0, 1024) },
)
.setTimestamp());
});
Comme un champ d'embed accepte au plus 1024 caractères, on tronque avec slice(0, 1024) ; sinon les longs messages feraient échouer l'envoi.
Journaliser les arrivées et départs de membres
Les arrivées passent par Events.GuildMemberAdd et les départs par Events.GuildMemberRemove. Afficher la date de création du compte d'un nouvel arrivant sous forme d'horodatage relatif avec le format <t:...:R> permet de repérer facilement une vague de raid composée de comptes tout neufs :
client.on(Events.GuildMemberAdd, (member) => {
const created = Math.floor(member.user.createdTimestamp / 1000);
log(member.guild, new EmbedBuilder()
.setColor(0x57F287)
.setAuthor({ name: member.user.tag, iconURL: member.user.displayAvatarURL() })
.setDescription(`📥 **${member} a rejoint le serveur**`)
.addFields({ name: 'Âge du compte', value: `<t:${created}:R>` })
.setTimestamp());
});
client.on(Events.GuildMemberRemove, (member) => {
log(member.guild, new EmbedBuilder()
.setColor(0xED4245)
.setAuthor({ name: member.user.tag, iconURL: member.user.displayAvatarURL() })
.setDescription(`📤 **${member.user.tag} a quitté le serveur**`)
.setTimestamp());
});
Journaliser les changements de rôle
Les changements de rôle ne sont pas un événement distinct ; ils font partie de la mise à jour du membre (Events.GuildMemberUpdate). Pour trouver les rôles ajoutés et retirés, on compare les collections de rôles de l'ancien et du nouveau membre :
client.on(Events.GuildMemberUpdate, (oldMember, newMember) => {
const oldRoles = oldMember.roles.cache;
const newRoles = newMember.roles.cache;
const added = newRoles.filter((r) => !oldRoles.has(r.id));
const removed = oldRoles.filter((r) => !newRoles.has(r.id));
if (added.size === 0 && removed.size === 0) return; // ex. seul le pseudo a changé
const lines = [];
if (added.size) lines.push(`➕ Ajouté : ${added.map((r) => r).join(', ')}`);
if (removed.size) lines.push(`➖ Retiré : ${removed.map((r) => r).join(', ')}`);
log(newMember.guild, new EmbedBuilder()
.setColor(0x5865F2)
.setAuthor({ name: newMember.user.tag, iconURL: newMember.user.displayAvatarURL() })
.setDescription(`🎭 **Changement de rôle** — ${newMember}\n${lines.join('\n')}`)
.setTimestamp());
});
Si vous voulez aussi savoir qui a effectué l'action, vous pouvez interroger le journal d'audit. Pour cela, le bot a besoin de la permission ViewAuditLog :
const { AuditLogEvent } = require('discord.js');
const audit = await newMember.guild.fetchAuditLogs({
type: AuditLogEvent.MemberRoleUpdate, limit: 1,
});
const entry = audit.entries.first();
const executor = entry?.target.id === newMember.id ? entry.executor : null;
Questions fréquentes
Pourquoi le contenu de certains messages supprimés est-il vide ?
Parce que ce message n'était pas dans le cache du bot. discord.js ne met en cache que les messages qu'il voit en fonctionnement ; si un ancien message envoyé avant le démarrage du bot est supprimé, son contenu ne peut pas être récupéré. C'est une limite de l'API Discord, pas un défaut du code. Pour un historique plus complet, il faut stocker les messages dans votre propre base de données.
Puis-je journaliser les événements survenus pendant que le bot est hors ligne ?
Non. Les événements de la gateway ne sont diffusés en direct que lorsque le bot est connecté ; tout ce qui se passe pendant une indisponibilité est perdu. C'est pourquoi il est important de maintenir le bot de logs en service 24/7 avec un outil comme PM2 et de capter les événements error.
Puis-je répartir les types d'événements dans des salons distincts ?
Oui. Au lieu d'un seul LOG_CHANNEL_ID, définissez des identifiants de salon différents par type d'événement et passez le salon cible en paramètre à la fonction log() ; cela permet de séparer les logs de messages des logs de membres.
Vous voulez une infrastructure de journalisation fiable pour votre serveur ? Je peux concevoir et développer un bot Discord adapté de bout en bout à vos besoins de modération. Contactez-moi pour parler de votre projet.