Ein discord log bot gehört zu den wertvollsten Werkzeugen, die du einem Server hinzufügen kannst: Er zeichnet still auf, was passiert, und liefert deinem Moderationsteam ein solides Prüfprotokoll. Wer hat welche Nachricht gelöscht, wer hat den Server verlassen, wann wurde jemandem welche Rolle gegeben — all das in einem dauerhaften Kanal zu sehen, hebt Streitigkeiten von „Das war ich nicht" auf eine beweisbasierte Moderation. In dieser Anleitung bauen wir mit discord.js v14 einen praktischen Bot, der gelöschte Nachrichten, Beitritte und Austritte von Mitgliedern sowie Rollenänderungen protokolliert.
Was macht ein discord log bot?
Das eingebaute Audit-Log von Discord bewahrt nur bestimmte administrative Aktionen für begrenzte Zeit auf und zeigt nie den Inhalt von Nachrichten. Wenn du deinen eigenen Log-Bot schreibst, entscheidest du, was du aufzeichnest, wie und wo. Typische Anwendungsfälle sind:
- Nachrichtenprüfung: den Inhalt gelöschter und bearbeiteter Nachrichten bewahren.
- Mitgliederaktivität: Beitritte und Austritte zusammen mit dem Kontoalter verfolgen (wertvoll für die Raid-Erkennung).
- Rechteverfolgung: das Vergeben und Entziehen von Rollen sehen und wer es getan hat.
Wir senden all diese Ereignisse als lesbare Embeds in einen einzigen #logs-Kanal.
Erforderliche Intents und Partial-Konfiguration
Das Protokollieren benötigt privilegierte (privileged) Intents. GuildMembers ist für Mitgliederereignisse erforderlich und MessageContent, um den Inhalt von Nachrichten zu lesen. Du musst diese beiden zusätzlich in den Einstellungen deiner Anwendung im Discord Developer Portal aktivieren. Um Ereignisse für ältere, nicht im Cache befindliche Nachrichten zu erfassen, musst du partials deklarieren:
// 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);
Um Wiederholungen zu vermeiden, definieren wir eine kleine Hilfsfunktion, die wir bei jedem Ereignis wiederverwenden:
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(() => {});
}
Gelöschte und bearbeitete Nachrichten protokollieren
Löschungen erfassen wir mit dem Ereignis Events.MessageDelete. Es gibt eine wichtige Einschränkung: Wird eine Nachricht gelöscht, die vor dem Start des Bots gesendet wurde — und nicht im Cache liegt —, kommt das Ereignis nur als Partial an und du kannst ihren Inhalt nicht wiederherstellen, weil die Nachricht auf Discord nicht mehr existiert. Daher überspringen wir Partial-Nachrichten sauber:
const { Events } = require('discord.js');
client.on(Events.MessageDelete, async (message) => {
if (message.partial) return; // Inhalt nicht verfügbar, wenn nicht gecacht
if (message.author?.bot) return;
log(message.guild, new EmbedBuilder()
.setColor(0xED4245)
.setAuthor({ name: message.author.tag, iconURL: message.author.displayAvatarURL() })
.setDescription(
`🗑️ **Nachricht gelöscht** — <#${message.channel.id}>\n` +
(message.content || '*Kein Inhalt*'),
)
.setTimestamp());
});
Bearbeitungen hören wir mit Events.MessageUpdate ab. Dieses Ereignis wird auch ausgelöst, wenn eine Linkvorschau (Embed) auf einer Nachricht lädt; um Rauschen zu vermeiden, brechen wir früh ab, wenn sich der Inhalt nicht wirklich geändert hat:
client.on(Events.MessageUpdate, async (oldMsg, newMsg) => {
if (oldMsg.partial || newMsg.author?.bot) return;
if (oldMsg.content === newMsg.content) return; // nur ein Embed wurde geladen
log(newMsg.guild, new EmbedBuilder()
.setColor(0xFEE75C)
.setAuthor({ name: newMsg.author.tag, iconURL: newMsg.author.displayAvatarURL() })
.setDescription(`✏️ **Nachricht bearbeitet** — [Zur Nachricht](${newMsg.url})`)
.addFields(
{ name: 'Vorher', value: (oldMsg.content || '—').slice(0, 1024) },
{ name: 'Nachher', value: (newMsg.content || '—').slice(0, 1024) },
)
.setTimestamp());
});
Da ein Embed-Feld höchstens 1024 Zeichen fasst, kürzen wir mit slice(0, 1024); sonst würden lange Nachrichten den Versand scheitern lassen.
Beitritte und Austritte von Mitgliedern protokollieren
Beitritte kommen über Events.GuildMemberAdd und Austritte über Events.GuildMemberRemove. Anzuzeigen, wann das Konto eines Neulings erstellt wurde — als relativer Zeitstempel im Format <t:...:R> —, macht es leicht, eine Raid-Welle aus brandneuen Konten zu erkennen:
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} ist dem Server beigetreten**`)
.addFields({ name: 'Kontoalter', 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} hat den Server verlassen**`)
.setTimestamp());
});
Rollenänderungen protokollieren
Rollenänderungen sind kein eigenes Ereignis; sie sind Teil der Mitglieder-Aktualisierung (Events.GuildMemberUpdate). Um die hinzugefügten und entfernten Rollen zu finden, vergleichen wir die Rollensammlungen des alten und des neuen Mitglieds:
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; // z. B. nur Spitzname geändert
const lines = [];
if (added.size) lines.push(`➕ Hinzugefügt: ${added.map((r) => r).join(', ')}`);
if (removed.size) lines.push(`➖ Entfernt: ${removed.map((r) => r).join(', ')}`);
log(newMember.guild, new EmbedBuilder()
.setColor(0x5865F2)
.setAuthor({ name: newMember.user.tag, iconURL: newMember.user.displayAvatarURL() })
.setDescription(`🎭 **Rollenänderung** — ${newMember}\n${lines.join('\n')}`)
.setTimestamp());
});
Willst du auch wissen, wer die Aktion ausgeführt hat, kannst du das Audit-Log abfragen. Dafür braucht der Bot die Berechtigung 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;
Häufige Fragen
Warum ist der Inhalt mancher gelöschter Nachrichten leer?
Weil diese Nachricht nicht im Cache des Bots war. discord.js cacht nur Nachrichten, die es im laufenden Betrieb sieht; wird eine ältere, vor dem Start des Bots gesendete Nachricht gelöscht, lässt sich ihr Inhalt nicht wiederherstellen. Das ist eine Einschränkung der Discord-API, kein schlechter Code. Für einen vollständigeren Verlauf musst du Nachrichten in deiner eigenen Datenbank speichern.
Kann ich Ereignisse protokollieren, die passieren, während der Bot offline ist?
Nein. Gateway-Ereignisse werden nur live übertragen, solange der Bot verbunden ist; alles, was während einer Ausfallzeit geschieht, wird verpasst. Deshalb ist es wichtig, den Log-Bot mit einem Werkzeug wie PM2 rund um die Uhr laufen zu lassen und error-Ereignisse abzufangen.
Kann ich verschiedene Ereignistypen auf getrennte Kanäle aufteilen?
Ja. Statt einer einzigen LOG_CHANNEL_ID definierst du pro Ereignistyp unterschiedliche Kanal-IDs und übergibst den Zielkanal als Parameter an die log()-Funktion; so hältst du Nachrichten-Logs von Mitglieder-Logs getrennt.
Möchtest du eine zuverlässige Protokollierung für deinen Server? Ich kann einen Discord-Bot entwerfen und bauen, der von Anfang bis Ende auf deine Moderationsbedürfnisse zugeschnitten ist. Nimm Kontakt auf, um über dein Projekt zu sprechen.