Build a logging bot

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.

Turn on the intents first

Logging reads message text and watches members, so it needs two privileged intents. On the Discord Developer Portal → your app → Bot, switch on:

  • Message Content Intent — so the bot can see what a deleted or edited message said.
  • Server Members Intent — so it hears joins and leaves.

⚠️ 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.

The whole 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.

How it works

  • Four event listeners do the work: MessageDelete, MessageUpdate, GuildMemberAdd, GuildMemberRemove. Each looks up the guild's log channel and posts a color-coded embed.
  • Partials matter. Discord only tells your bot about deletes and edits of messages it has cached. Adding 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).
  • Bot messages are skipped with the 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.

What resets on a restart

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.

Verify it works

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.

Troubleshooting

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.

Next steps

Was this guide helpful?