Each example below was run against a test copy of Discord, deleting and editing messages the bot had seen and messages from before it started, to check what reaches the log and what does not.
Before the code
- Message Content. Without this intent a bot sees messages with empty text, so there is nothing to log. It is privileged: switch it on in the code, as below, and in the Developer Portal on the Bot page. Once the bot is in 100 servers, Discord asks you to apply for it.
- A staff-only channel for the log, where the bot can send messages and embeds. Copy its ID with Developer Mode on (User Settings, Advanced, then right-click the channel).
- Say so in the rules. A log keeps what people chose to take back. Members should know it exists.
What a bot can and cannot know
Discord tells a bot only which message was deleted, not what it said. The text comes from the bot's own memory of messages it has seen: discord.py keeps the last 1,000 across all channels, discord.js the last 200 in each channel. A message from before the bot last started, or older than that, is deleted without its text, and the log can only say so. The same goes for the old text of an edit.
The event does not say who deleted the message either. When a moderator deletes someone else's message, the server's audit log records it (reading it needs View Audit Log); when people delete their own, nothing does.
discord.py
import os
import discord
from discord.ext import commands
LOG_CHANNEL_ID = 123456789012345678 # a channel only staff can see
intents = discord.Intents.default()
intents.message_content = True # the Message Content intent: switch it on in the Developer Portal too
bot = commands.Bot(command_prefix="!", intents=intents)
def log_channel(guild_id: int | None) -> discord.TextChannel | None:
channel = bot.get_channel(LOG_CHANNEL_ID)
return channel if channel and channel.guild.id == guild_id else None
@bot.event
async def on_raw_message_delete(payload: discord.RawMessageDeleteEvent):
log = log_channel(payload.guild_id)
message = payload.cached_message
if log is None or payload.channel_id == log.id or (message and message.author.bot):
return
embed = discord.Embed(title="Message deleted", colour=discord.Colour.red())
embed.add_field(name="Channel", value=f"<#{payload.channel_id}>")
if message is None:
embed.description = "Sent before the bot started, or too long ago: its text is not known."
else:
embed.add_field(name="Author", value=message.author.mention)
embed.description = message.content[:4000] or "(no text)"
await log.send(embed=embed)
@bot.event
async def on_raw_message_edit(payload: discord.RawMessageUpdateEvent):
log = log_channel(payload.guild_id)
after, before = payload.message, payload.cached_message
if log is None or payload.channel_id == log.id or after.author.bot:
return
if after.edited_at is None:
return # a link preview loading, not an edit
if before is not None and before.content == after.content:
return
embed = discord.Embed(title="Message edited", url=after.jump_url, colour=discord.Colour.orange())
embed.add_field(name="Author", value=after.author.mention)
embed.add_field(name="Before", value=(before.content[:1024] or "(no text)") if before else "(not known)", inline=False)
embed.add_field(name="After", value=after.content[:1024] or "(no text)", inline=False)
await log.send(embed=embed)
bot.run(os.environ["BOT_TOKEN"])- The raw events.
on_message_deleteandon_message_editonly fire for messages in the bot's memory. The raw ones fire for every message, and carry the remembered copy ascached_messagewhen there is one. payload.messageon an edit is the message as it is now; it needs discord.py 2.5 or newer.
discord.js
const { Client, Colors, EmbedBuilder, Events, GatewayIntentBits, Partials } = require("discord.js");
const LOG_CHANNEL_ID = "123456789012345678"; // a channel only staff can see
const client = new Client({
// MessageContent is privileged: switch it on in the Developer Portal too.
intents: [GatewayIntentBits.Guilds, GatewayIntentBits.GuildMessages, GatewayIntentBits.MessageContent],
partials: [Partials.Message], // deletes and edits of messages the bot never had in memory
});
function logChannel(guild) {
const channel = guild?.channels.cache.get(LOG_CHANNEL_ID);
return channel?.isTextBased() ? channel : null;
}
client.on(Events.MessageDelete, async (message) => {
const log = logChannel(message.guild);
if (!log || message.channelId === log.id || message.author?.bot) return;
const embed = new EmbedBuilder().setTitle("Message deleted").setColor(Colors.Red)
.addFields({ name: "Channel", value: `${message.channel}`, inline: true });
if (message.partial) {
embed.setDescription("Sent before the bot started, or too long ago: its text is not known.");
} else {
embed.addFields({ name: "Author", value: `${message.author}`, inline: true })
.setDescription(message.content.slice(0, 4000) || "(no text)");
}
await log.send({ embeds: [embed] });
});
client.on(Events.MessageUpdate, async (before, after) => {
const log = logChannel(after.guild);
if (!log || after.channelId === log.id || after.author?.bot) return;
if (!after.editedTimestamp) return; // a link preview loading, not an edit
if (!before.partial && before.content === after.content) return;
const embed = new EmbedBuilder().setTitle("Message edited").setURL(after.url).setColor(Colors.Orange)
.addFields(
{ name: "Author", value: `${after.author}` },
{ name: "Before", value: before.partial ? "(not known)" : before.content.slice(0, 1024) || "(no text)" },
{ name: "After", value: after.content.slice(0, 1024) || "(no text)" },
);
await log.send({ embeds: [embed] });
});
client.login(process.env.BOT_TOKEN);- Why
Partials.Message. discord.js only firesMessageDeleteandMessageUpdatefor messages it has in memory. With the partial, it fires for the others too, with a message whose text is unknown (message.partialis true). Without it, the test run logged nothing at all for a message from before the bot started. - For an edit,
afterhas the new text even whenbeforeis a partial, because Discord sends the whole edited message.
Edits that are not edits
When someone posts a link, Discord loads its preview a moment later and sends the bot an update for the message, with the same text. Both examples skip an update without an edit time (edited_at, editedTimestamp), and one whose text did not change, so the log only shows real edits.
Common problems
- Every log says "(no text)": Message Content is off in the code or the portal.
- Deletions show "its text is not known": the message was older than the bot's memory, or from before it last started. That is the limit, not a bug.
- Nothing is logged for older messages in discord.js:
Partials.Messageis missing. - A purge logs nothing: deleting many messages at once arrives as one bulk event,
on_raw_bulk_message_deleteorMessageBulkDelete, which these examples do not handle. 50013 Missing Permissionswhen logging: the bot cannot send messages or embeds in the log channel.
Did this page help?
Thanks for the feedback.