Only show guides for your app
Mirror deleted and edited messages, plus member joins and leaves, into a moderation log channel — with the two privileged intents it needs and an honest note about what survives a restart.
A logging bot keeps a paper trail: when a message is deleted or edited, or a member joins or leaves, it posts a tidy embed to a log channel your moderators watch. This recipe builds that in discord.js, covers the two privileged intents it needs, and is honest about the one thing that resets when the bot restarts.
| At a glance | |
|---|---|
| You need | A working bot from Host a discord.js bot |
| Plan | Free or premium — no premium features required |
| Time | About twenty minutes |
New here? Start with Host a discord.js bot; this recipe builds on that index.js.
Logging reads message text and watches members, so it needs two privileged intents. On the Discord Developer Portal → your app → Bot, switch on:
⚠️ Heads up: If your code requests a privileged intent that isn't switched on in the portal, the bot won't log in — it crashes with
Used disallowed intents. Turn them on before you start the bot.
Here's the complete index.js. Members set the log channel with /setlog, and the bot mirrors four kinds of events into it.
require('dotenv').config();
const { Client, GatewayIntentBits, Events, Partials, EmbedBuilder } = require('discord.js');
const client = new Client({
intents: [
GatewayIntentBits.Guilds,
GatewayIntentBits.GuildMessages,
GatewayIntentBits.MessageContent,
GatewayIntentBits.GuildMembers,
],
partials: [Partials.Message, Partials.Channel],
});
// Per-guild log channel, set with /setlog. In memory — a restart clears it.
const logChannels = new Map();
function logChannelFor(guild) {
const id = guild && logChannels.get(guild.id);
return id ? guild.channels.cache.get(id) : null;
}
client.once(Events.ClientReady, async (c) => {
await c.application.commands.set([
{ name: 'setlog', description: 'Send event logs to this channel' },
]);
console.log(`Listening as ${c.user.tag}`);
});
client.on(Events.InteractionCreate, async (interaction) => {
if (interaction.isChatInputCommand() && interaction.commandName === 'setlog') {
logChannels.set(interaction.guildId, interaction.channelId);
await interaction.reply({ content: 'Event logs will go to this channel.', ephemeral: true });
}
});
client.on(Events.MessageDelete, async (message) => {
if (message.author?.bot) return;
const channel = logChannelFor(message.guild);
if (!channel) return;
const embed = new EmbedBuilder()
.setColor(0xed4245)
.setAuthor({ name: message.author?.tag ?? 'Unknown', iconURL: message.author?.displayAvatarURL() })
.setDescription(`🗑️ **Message deleted in <#${message.channelId}>**\n${message.content || '*no text*'}`)
.setTimestamp();
channel.send({ embeds: [embed] });
});
client.on(Events.MessageUpdate, async (oldMessage, newMessage) => {
if (oldMessage.author?.bot || oldMessage.content === newMessage.content) return;
const channel = logChannelFor(newMessage.guild);
if (!channel) return;
const embed = new EmbedBuilder()
.setColor(0xfee75c)
.setAuthor({ name: newMessage.author?.tag ?? 'Unknown', iconURL: newMessage.author?.displayAvatarURL() })
.setDescription(`✏️ **Message edited in <#${newMessage.channelId}>**`)
.addFields(
{ name: 'Before', value: (oldMessage.content || '*empty*').slice(0, 1024) },
{ name: 'After', value: (newMessage.content || '*empty*').slice(0, 1024) },
)
.setTimestamp();
channel.send({ embeds: [embed] });
});
client.on(Events.GuildMemberAdd, (member) => {
const channel = logChannelFor(member.guild);
if (channel) channel.send({ embeds: [new EmbedBuilder().setColor(0x57f287).setDescription(`📥 **${member.user.tag}** joined`).setTimestamp()] });
});
client.on(Events.GuildMemberRemove, (member) => {
const channel = logChannelFor(member.guild);
if (channel) channel.send({ embeds: [new EmbedBuilder().setColor(0xed4245).setDescription(`📤 **${member.user.tag}** left`).setTimestamp()] });
});
client.login(process.env.DISCORD_TOKEN);
Restart, run /setlog in the channel you want to use, and edit or delete a test message.
MessageDelete, MessageUpdate, GuildMemberAdd, GuildMemberRemove. Each looks up the guild's log channel and posts a color-coded embed.Partials.Message and Partials.Channel lets the bot receive those events for older messages too — though for a message deleted before the bot ever saw it, message.content will be empty (that's the *no text* case).author?.bot checks, so your bot doesn't log itself.🎯 Good to know: Want to know who deleted someone else's message? Discord doesn't put that in the delete event — you'd check the server's audit log (
guild.fetchAuditLogs), which is a best-effort match, not a guarantee. Keep that honest with your mods.
The log channel is remembered in a Map in memory, so a restart forgets it and you'll need to run /setlog again. That's fine for a small server, but if you want it to stick, save the guild-to-channel mapping to a file or SQLite — see Storing data for your bot. One settings table keyed by guild ID is all it takes.
After /setlog, delete one of your own messages and confirm a red "Message deleted" embed appears in the log channel, showing the old text. Edit a message and confirm a yellow "before / after" embed. Have someone join or leave to see the green/red member embeds.
| Symptom | Fix |
|---|---|
Used disallowed intents on start |
Turn on Message Content and Server Members in the Developer Portal Bot tab. |
Deletes always show *no text* |
The message wasn't cached (it's old) or Message Content is off. Partials let the event fire; content only shows for messages the bot could see. |
| Joins/leaves never log | The Server Members intent isn't enabled, or the bot can't see the log channel. |
| Nothing logs at all | You haven't run /setlog since the last restart — the channel is remembered only in memory. |