NapNodes

← Discord bot guides

Discord bots · updated 30 October 2026

Discord Logging Bot: Deleted and Edited Messages, Joins and Leaves

Discord doesn't keep deleted messages: when someone deletes one, it's gone and nobody on the server can see it, not even the admins. The only thing that works is a bot that copies it while it's still there. Here it is, complete, in Python (discord.py) and in JavaScript (discord.js): it records in a private channel the deleted messages, the edited ones with before and after, and who joins and who leaves.

What a bot can log (and what it can't)

When a message is deleted, Discord tells bots it was deleted, but it doesn't send them the text again: only the message number and the channel. That's why the bot keeps the latest messages it sees arrive in memory and, when one is deleted, looks it up there. Anything written before the bot was on the server, or before its last restart, is logged as deleted but without the text.

Edits work the same way: the bot knows what the message says now, and the “before” only if it had it in memory.

There is no way to see messages deleted before you added the bot. The programs that promise it modify the Discord app, which its terms don't allow, and they only see what went past your own screen.

If what you want to know is who kicked, banned or changed a role, Discord already keeps that without any bot, in “Server Settings” → “Audit Log”.

Rather not set it up yourself? Discord bot hosting from €1.50 a month, no contract and a full refund within 72 hours. Or create a free Discord bot, no card needed.

Before the code: the bot and the channel

  1. Create the bot

    If you don't have one yet: at discord.com/developers/applications, “New Application”, and in the “Bot” tab, “Reset Token” to get the token. Step by step in the Python guide or the JavaScript one.

  2. Turn on two privileged intents

    In the same “Bot” tab, under “Privileged Gateway Intents”, turn on “Message Content Intent” (to read the text of messages) and “Server Members Intent” (to know who joins and who leaves), and save. If the code asks for them and they're off here, the bot won't start: discord.py says PrivilegedIntentsRequired and discord.js, “Used disallowed intents”.

  3. Make a channel just for moderators

    Create a text channel, for example #logs, and in “Edit Channel” → “Permissions” take “View Channel” away from @everyone and give it to your moderator role and to the bot. Messages people thought were deleted will show up there: don't let the whole server see it.

  4. Invite it with these permissions

    In “OAuth2” → “URL Generator”, tick “bot” and the permissions “View Channels”, “Send Messages” and “Embed Links” (without it the logs come out empty, because they're embeds). The bot permissions calculator gives you the link ready made. The bot only hears about what happens in the channels it can see: from a private channel without it, it won't log anything.

  5. Copy the channel ID

    In “User Settings” → “Advanced”, turn on “Developer Mode”. Then right-click #logs → “Copy Channel ID”. That number goes in the .env file, next to the token.

The bot in Python (discord.py)

Next to .env, requirements.txt and app.py. You need Python 3.10 or newer.

on_raw_message_delete fires for every deletion, including messages the bot doesn't remember; payload.cached_message is the message if it had it in memory. on_message_edit, on the other hand, only fires for messages it remembers, which are the ones with a “before”. max_messages=5000 is how many messages it keeps across all channels (1000 by default).

.env
DISCORD_TOKEN=paste-your-token-here
CANAL_REGISTRO=123456789012345678
requirements.txt
discord.py
python-dotenv
app.py
import os

import discord
from dotenv import load_dotenv

load_dotenv()  # reads the token and the channel from the .env file
CANAL_REGISTRO = int(os.environ["CANAL_REGISTRO"])  # the ID of the log channel

intents = discord.Intents.default()
intents.message_content = True  # read the text of messages
intents.members = True  # know who joins and who leaves
# the latest messages the bot remembers; for older ones it won't know the text
client = discord.Client(intents=intents, max_messages=5000)


def recortar(texto: str | None) -> str:
    texto = texto or "(no text)"
    return texto if len(texto) <= 1000 else texto[:1000] + "…"


async def registrar(guild: discord.Guild | None, embed: discord.Embed):
    # only to the log channel of that same server
    canal = client.get_channel(CANAL_REGISTRO)
    if canal and guild and canal.guild == guild:
        await canal.send(embed=embed)


@client.event
async def on_raw_message_delete(payload: discord.RawMessageDeleteEvent):
    if payload.channel_id == CANAL_REGISTRO or payload.guild_id is None:
        return
    msg = payload.cached_message  # only if the bot saw it arrive
    if msg and msg.author.bot:
        return
    embed = discord.Embed(title="Message deleted", color=0xE74C3C, timestamp=discord.utils.utcnow())
    embed.add_field(name="Channel", value=f"<#{payload.channel_id}>")
    if msg:
        embed.description = recortar(msg.content)
        embed.add_field(name="Author", value=msg.author.mention)
        if msg.attachments:
            nombres = ", ".join(a.filename for a in msg.attachments)
            embed.add_field(name="Attachments", value=recortar(nombres), inline=False)
    else:
        embed.description = "It was from before the bot saw it: Discord doesn't give its text."
    await registrar(client.get_guild(payload.guild_id), embed)


@client.event
async def on_message_edit(before: discord.Message, after: discord.Message):
    # it also fires when Discord adds a link preview: no text change, nothing to log
    if after.guild is None or after.author.bot or before.content == after.content:
        return
    embed = discord.Embed(title="Message edited", url=after.jump_url, color=0xF1C40F, timestamp=discord.utils.utcnow())
    embed.add_field(name="Author", value=after.author.mention)
    embed.add_field(name="Channel", value=after.channel.mention)
    embed.add_field(name="Before", value=recortar(before.content), inline=False)
    embed.add_field(name="After", value=recortar(after.content), inline=False)
    await registrar(after.guild, embed)


@client.event
async def on_member_join(member: discord.Member):
    embed = discord.Embed(title="Joined", description=f"{member.mention} ({member})", color=0x2ECC71, timestamp=discord.utils.utcnow())
    embed.add_field(name="Account created", value=discord.utils.format_dt(member.created_at, "R"))
    await registrar(member.guild, embed)


@client.event
async def on_member_remove(member: discord.Member):
    embed = discord.Embed(title="Left", description=f"{member.mention} ({member})", color=0x95A5A6, timestamp=discord.utils.utcnow())
    await registrar(member.guild, embed)


@client.event
async def on_ready():
    print(f"Logged in as {client.user}")


client.run(os.environ["DISCORD_TOKEN"])

The bot in JavaScript (discord.js)

Next to .env, package.json and index.js; install with npm install.

The partials make discord.js also report messages it doesn't remember (they arrive half-filled, with partial set to true and no text) and people who leave without being in its memory. discord.js memory is per channel: 200 messages in each by default; here, 1000.

.env
DISCORD_TOKEN=paste-your-token-here
CANAL_REGISTRO=123456789012345678
package.json
{
  "name": "bot-registros",
  "version": "1.0.0",
  "main": "index.js",
  "dependencies": {
    "discord.js": "^14.27.0",
    "dotenv": "^16.4.0"
  }
}
index.js
require("dotenv").config(); // reads the token and the channel from the .env file
const { Client, EmbedBuilder, Events, GatewayIntentBits, Options, Partials } = require("discord.js");

const CANAL_REGISTRO = process.env.CANAL_REGISTRO; // the ID of the log channel

const client = new Client({
  intents: [
    GatewayIntentBits.Guilds,
    GatewayIntentBits.GuildMessages,
    GatewayIntentBits.MessageContent, // read the text of messages
    GatewayIntentBits.GuildMembers, // know who joins and who leaves
  ],
  // without this, a message the bot doesn't remember doesn't even trigger the event
  partials: [Partials.Message, Partials.GuildMember],
  // the messages it remembers per channel (200 by default)
  makeCache: Options.cacheWithLimits({ ...Options.DefaultMakeCacheSettings, MessageManager: 1000 }),
});

const recortar = (texto) => (!texto ? "(no text)" : texto.length <= 1000 ? texto : texto.slice(0, 1000) + "…");

async function registrar(guild, embed) {
  // only to the log channel of that same server
  const canal = await client.channels.fetch(CANAL_REGISTRO).catch(() => null);
  if (canal && guild && canal.guildId === guild.id) await canal.send({ embeds: [embed] });
}

client.on(Events.MessageDelete, async (msg) => {
  if (!msg.guild || msg.channelId === CANAL_REGISTRO || msg.author?.bot) return;
  const embed = new EmbedBuilder()
    .setTitle("Message deleted")
    .setColor("#e74c3c")
    .setTimestamp()
    .addFields({ name: "Channel", value: `<#${msg.channelId}>`, inline: true });
  if (msg.partial) {
    embed.setDescription("It was from before the bot saw it: Discord doesn't give its text."); // the bot didn't see it arrive
  } else {
    embed.setDescription(recortar(msg.content)).addFields({ name: "Author", value: `${msg.author}`, inline: true });
    if (msg.attachments.size) {
      embed.addFields({ name: "Attachments", value: recortar(msg.attachments.map((a) => a.name).join(", ")) });
    }
  }
  await registrar(msg.guild, embed);
});

client.on(Events.MessageUpdate, async (antes, despues) => {
  if (!despues.guild || !despues.author || despues.author.bot) return;
  // it also fires when Discord adds a link preview: no text change, nothing to log
  if (!antes.partial && antes.content === despues.content) return;
  const embed = new EmbedBuilder()
    .setTitle("Message edited")
    .setURL(despues.url)
    .setColor("#f1c40f")
    .setTimestamp()
    .addFields(
      { name: "Author", value: `${despues.author}`, inline: true },
      { name: "Channel", value: `<#${despues.channelId}>`, inline: true },
      { name: "Before", value: antes.partial ? "(the bot didn't remember it)" : recortar(antes.content) },
      { name: "After", value: recortar(despues.content) },
    );
  await registrar(despues.guild, embed);
});

client.on(Events.GuildMemberAdd, async (m) => {
  const embed = new EmbedBuilder()
    .setTitle("Joined")
    .setDescription(`${m.user} (${m.user.tag})`)
    .setColor("#2ecc71")
    .setTimestamp()
    .addFields({ name: "Account created", value: `<t:${Math.floor(m.user.createdTimestamp / 1000)}:R>` });
  await registrar(m.guild, embed);
});

client.on(Events.GuildMemberRemove, async (m) => {
  const embed = new EmbedBuilder().setTitle("Left").setDescription(`${m.user} (${m.user.tag})`).setColor("#95a5a6").setTimestamp();
  await registrar(m.guild, embed);
});

client.once(Events.ClientReady, (c) => console.log(`Logged in as ${c.user.tag}`));

client.login(process.env.DISCORD_TOKEN);

What shows up in the log channel

Each event is an embed with its own colour: red, a deleted message (channel, author and text); yellow, an edited one (author, channel, before and after, and the title links to the message); green, someone joining, with the age of their account (an account made a few minutes ago is usually a throwaway); grey, someone leaving.

The text is cut at 1000 characters so it doesn't go over the 1024 limit of an embed field (all the limits, in the embeds guide). Attachments are logged by name only: copying them would make the bot download and re-upload every file that's sent.

It doesn't log messages from other bots (or its own), nor bulk deletions, like the clear command of a moderation bot, which arrive in a different event: on_raw_bulk_message_delete in discord.py and MessageBulkDelete in discord.js.

And who deleted it?

The deletion event doesn't say who deleted it. If it was the author, Discord doesn't record it anywhere. If a moderator deleted someone else's message, it shows up in the “Audit Log”, and the bot can read it with the “View Audit Log” permission: guild.audit_logs(action=discord.AuditLogAction.message_delete) in discord.py and guild.fetchAuditLogs({ type: AuditLogEvent.MessageDelete }) in discord.js.

Don't trust it completely: Discord groups several deletions in a row by the same moderator into a single entry with a counter, so it doesn't always match one specific message.

Say so in the rules

A logging bot keeps what people write even if they delete it later. Say so in the server rules (for example, “deleted and edited messages are logged for moderation”), keep the log channel for moderators only and don't use it for anything else.

If you also want warnings, mutes and bans with commands, the moderation bot records every action in a log channel, which can be the same one.

Keeping it online all the time

A logging bot only records what happens while it's running, and when it restarts it forgets what it had in memory: anything deleted while the bot is off is lost without a trace. That's why this one, more than any other, has to stay on day and night, not on your computer. Free and paid options, compared, in where to host your Discord bot.

NapNodes has a free 256 MB bot that isn't switched off for being empty; you renew it every 7 days with “Start” (its downsides, in this guide). Create it, choose Node.js or Python in “Settings” → “Language” and upload your files in “Files” (or set your repository in “Settings” → “GitHub repository”); whatever the bot prints shows up in “Console”.

Frequently asked questions

Can you see deleted messages on Discord?

No: Discord doesn't keep deleted messages or let anyone see them, admins included. Only a bot that was already on the server can have copied them before they were deleted.

Why does my bot log the deletion but without the text?

Because the message was from before the bot saw it, or from before its last restart. Discord doesn't send the text on deletion: the bot can only get it from its memory.

Which intents does a logging bot need?

Message Content, to read the text, and Server Members, for joins and leaves. Both are turned on in the “Bot” tab of the developer portal and requested in the code.

Can the bot tell who deleted a message?

Only if a moderator deleted it, and through the “Audit Log” (with the “View Audit Log” permission). If the author deletes it, Discord doesn't record it.

Shall we set it up for you?

Discord bot hosting from €1.50 a month, never oversold: your RAM is yours alone. And if you'd rather start without paying, there's a free Discord bot, no card needed.

Free tool: Discord bot permissions calculator. Tick your bot's permissions and get the number and the invite link; or paste a number and see which permissions it grants.

Free tool: Discord intents calculator. Tick what your bot does or search the event you're not getting: the intents, the discord.js or discord.py code and what to enable in the portal.

Keep reading

All Discord bot guides, sorted →