Categoría: Bots de Discord

Crear un bot de Discord#32¡Crea un canal de registros y auditoría (Audit Log) del servidor! Registra automáticamente mensajes borrados y editados, entradas, salidas y expulsiones (ejemplo en Python) 🕵️

Esta entrada es una traducción del original en coreano.

De un vistazo

El registro de auditoría de Discord solo dice 'alguien borró N mensajes', sin mostrar qué se borró. Combinamos eventos raw con on_audit_log_entry_create para crear un bot que reúne en un canal, como embeds, el contenido borrado, quién lo borró, entradas y salidas, cambios de rol y expulsiones.

  • Python 3.10.12 · discord.py 2.7.1 · python-dotenv 1.2.3 · ejecutado y comprobado el 2026-09-25
  • El contenido borrado solo se ve si el mensaje está en la caché del bot → sube max_messages y activa los intents Message Content y Server Members
  • Quién lo borró se busca en guild.audit_logs() por target, canal y tiempo (15 s); si el autor borra su propio mensaje, no queda registro
  • Las expulsiones y baneos llegan en tiempo real, con moderador y motivo, vía on_audit_log_entry_create (2.2+, Intents.moderation)
Índice13 secciones

¡Hola! Soy Mojangcam 😎

En el #31 creamos un bot de canales de voz temporales (Temp Voice) que abre un canal automáticamente cuando entras a un lobby y lo borra cuando se queda vacío. Esta entrega trata de una función que todo administrador de servidor acaba buscando tarde o temprano: "¿Quién borró esto?"

Esto es lo que hará el bot de registros de hoy.

  • Cuando alguien borra un mensaje, el contenido de antes del borrado queda tal cual en el canal de registros
  • Si un administrador borró el mensaje de otra persona, también se anota quién lo borró
  • Cuando entra un miembro se guarda la fecha de creación de su cuenta (¡para detectar cuentas secundarias y bots!), y cuando se va, los roles que tenía
  • Las expulsiones y los baneos se registran en tiempo real, con el administrador que los hizo y el motivo

¿Por qué hace falta un bot? 🧠 Discord tiene el menú Server Settings (서버 설정) → Audit Log (감사 로그), pero no muestra el contenido de los mensajes borrados. Solo te dice "fulano borró tantos mensajes". Si surge un conflicto y quieres demostrar que "esa persona insultó y luego lo borró", la única forma es que un bot lo haya guardado antes.

Canal #server-log con embeds de mensaje borrado (rojo) y entrada de miembro (verde) acumulados, y leyenda de colores por evento

El bot de registros de esta entrega, junto con el #21 filtro de palabras prohibidas y el #25 manejo de errores, hará de módulo de transparencia de moderación del "bot de gestión integral de la comunidad".


📌 Lo que necesitas

  • Python 3.9 o superior (se recomienda 3.10+) — usamos la notación dict[str, int], así que funciona desde la 3.9
  • discord.py 2.7.1 (la más reciente en PyPI a 2026-09-25) — on_audit_log_entry_create existe desde la 2.2, así que si tienes una versión antigua, actualízala sí o sí
  • python-dotenv 1.x
  • Un bot creado en el Portal para desarrolladores de Discord e invitado a tu servidor (permisos: Ver registro de auditoría (View Audit Log), Enviar mensajes (Send Messages), Insertar enlaces (Embed Links))
Terminal
pip install -U discord.py python-dotenv

Crea un archivo .env en la carpeta del proyecto y pon ahí tu token. Si escribes el token directamente en el código, es muy fácil filtrarlo al subirlo a GitHub 😱

Texto
token=여기에_봇_토큰

💡 No olvides añadir .env a tu .gitignore. (echo .env >> .gitignore)

⚠️ Esta entrega necesita dos intents privilegiados (Privileged Intents). En el Portal para desarrolladores → pestaña Bot, activa Message Content Intent (obligatorio para ver el contenido de los mensajes borrados) y Server Members Intent (para detectar entradas, salidas y cambios de rol). Si no los activas, los registros de borrado saldrán con el contenido vacío o los eventos de entrada y salida ni siquiera se dispararán.

🧩 Qué va a registrar el bot

Hoy vamos a hacer 6 tipos de registro. Los diferenciaremos por color para que, con solo hacer scroll, veas qué pasó.

Evento Qué se registra Color del embed
Mensaje borrado Autor, canal, contenido antes de borrarse, nombres de adjuntos, quién lo borró Rojo
Mensaje editado Contenido antes y después, enlace directo al mensaje Naranja
Entrada / salida Fecha de creación de la cuenta (detectar cuentas nuevas), fecha de entrada, roles que tenía Verde / gris
Cambio de apodo / rol Antes y después, roles añadidos y quitados Azul
Expulsión / baneo / desbaneo Administrador que lo hizo, afectado, motivo Rojo oscuro
Canal creado / borrado Nombre y tipo del canal Verde / rojo

📌 Paso 1 - Configuración básica y 3 intents

Python
# discord.py 2.7.1 / Python 3.9+ / python-dotenv 1.x
import os
import json
import asyncio

import discord
from discord import app_commands
from discord.ext import commands
from dotenv import load_dotenv

load_dotenv()
token = os.getenv("token")

CONFIG_FILE = "log_config.json"   # {ID del servidor (str): ID del canal de registros (int)}
MAX_CONTENT = 1000                # Campo de embed máx. 1024 caracteres → recortamos con margen

intents = discord.Intents.default()
intents.message_content = True   # Obligatorio para ver el 'contenido' de mensajes borrados (intent privilegiado)
intents.members = True           # Detecta entradas, salidas, cambios de apodo y rol (intent privilegiado)
intents.moderation = True        # Expulsiones, baneos + eventos del registro de auditoría

bot = commands.Bot(command_prefix="!", intents=intents, max_messages=5000)

log_channels: dict[str, int] = {}
lock = asyncio.Lock()

Vamos a desmenuzarlo 💡

  • intents.message_content : permite que el bot vea el contenido de los mensajes que escriben otras personas. Si está desactivado, en el registro de borrado content llega como cadena vacía. Es un intent privilegiado que también hay que activar en el portal.
  • intents.members : on_member_join / on_member_remove / on_member_update dependen de este intent. También es privilegiado.
  • intents.moderation : aquí pertenecen on_member_ban / on_member_unban y el protagonista de hoy, on_audit_log_entry_create. No es privilegiado, así que basta con activarlo en el código. (Es lo mismo que el antiguo intents.bans.)
  • max_messages=5000 : la cantidad de mensajes que el bot recuerda. El valor por defecto es 1000, pero como el contenido de un mensaje borrado solo se puede ver si está en esta caché, lo subimos con holgura.

📌 Paso 2 - Guardar el canal de registros y funciones comunes para embeds

Cada servidor tiene su propio canal de registros, así que guardamos {서버ID: 채널ID} en un JSON. Es el mismo patrón que en el #31.

Python
def save():
    with open(CONFIG_FILE, "w", encoding="utf-8") as f:
        json.dump(log_channels, f, ensure_ascii=False, indent=2)


def load():
    global log_channels
    if os.path.exists(CONFIG_FILE):
        with open(CONFIG_FILE, encoding="utf-8") as f:
            log_channels = json.load(f)


def get_log_channel(guild: discord.Guild):
    cid = log_channels.get(str(guild.id))
    return guild.get_channel(cid) if cid else None


def make_embed(title: str, color: discord.Color, user=None) -> discord.Embed:
    embed = discord.Embed(title=title, color=color, timestamp=discord.utils.utcnow())
    if user is not None:
        embed.set_author(name=f"{user} ({user.id})", icon_url=user.display_avatar.url)
    return embed


async def send_log(guild: discord.Guild, embed: discord.Embed):
    channel = get_log_channel(guild)
    if channel is None:
        return
    try:
        await channel.send(embed=embed)
    except discord.Forbidden:
        print(f"⚠️ {guild.name}: no tengo permiso para enviar mensajes al canal de registros.")


def cut(text: str) -> str:
    text = text or "(sin contenido — solo embed/adjunto o no está en caché)"
    return text if len(text) <= MAX_CONTENT else text[:MAX_CONTENT] + " …(recortado)"
  • timestamp=discord.utils.utcnow() : debajo del embed aparece automáticamente una hora como "hoy a las 15:12", en la zona horaria de quien lo mira. ¡En un registro, lo más importante es la hora!
  • set_author(name, icon_url) : fija la foto de perfil + 이름 (ID) (nombre e ID) en la parte superior del embed. Guardamos también el ID porque, aunque luego cambie de apodo o salga del servidor, el ID no cambia.
  • try/except de send_log : si alguien cambia los permisos del canal de registros, el bot podría morir con Forbidden. Como aprendimos en el #25, lo capturamos, lo mostramos solo en la consola y seguimos.
  • cut() : el valor de un campo de embed tiene un límite de 1024 caracteres. Si lo superas, obtienes 400 Bad Request, así que lo recortamos antes.

📌 Paso 3 - ¡Lo esencial! Capturar mensajes borrados y editados

Aquí usamos eventos raw. on_message_delete solo avisa de los mensajes que están en la caché, pero on_raw_message_delete te dice al menos "se borró tal ID en tal canal" aunque no esté en caché.

Flujo del bot al borrarse un mensaje - 3 pasos (borrado, consulta de caché, consulta del registro de auditoría) y el valor que aparece en el campo 'Borrado por' según cada caso

Python
async def find_deleter(guild: discord.Guild, author_id: int, channel_id: int):
    """Busca en el registro de auditoría al 'administrador que acaba de borrar un mensaje de esta persona en este canal'."""
    try:
        async for entry in guild.audit_logs(limit=5, action=discord.AuditLogAction.message_delete):
            same_target = entry.target is not None and entry.target.id == author_id
            same_channel = entry.extra.channel.id == channel_id
            recent = (discord.utils.utcnow() - entry.created_at).total_seconds() < 15
            if same_target and same_channel and recent:
                return entry.user
    except discord.Forbidden:
        return None
    return None


@bot.event
async def on_raw_message_delete(payload: discord.RawMessageDeleteEvent):
    if payload.guild_id is None:      # Ignorar los MD
        return
    guild = bot.get_guild(payload.guild_id)
    msg = payload.cached_message

    embed = make_embed("🗑️ Mensaje borrado", discord.Color.red(), msg.author if msg else None)
    embed.add_field(name="Canal", value=f"<#{payload.channel_id}>", inline=True)

    if msg is None:
        embed.add_field(name="Contenido", value="Mensaje que no está en caché (anterior al inicio del bot)", inline=False)
        embed.set_footer(text=f"ID del mensaje: {payload.message_id}")
    else:
        if msg.author.bot:
            return
        deleter = await find_deleter(guild, msg.author.id, payload.channel_id)
        who = deleter.mention if deleter else "El propio autor (o no se pudo confirmar)"
        embed.add_field(name="Borrado por", value=who, inline=True)
        embed.add_field(name="Contenido", value=cut(msg.content), inline=False)
        if msg.attachments:
            files = "\n".join(a.filename for a in msg.attachments)
            embed.add_field(name="Adjuntos", value=files, inline=False)
        embed.set_footer(text=f"ID del mensaje: {msg.id}")

    await send_log(guild, embed)


@bot.event
async def on_raw_message_edit(payload: discord.RawMessageUpdateEvent):
    if payload.guild_id is None:
        return
    before = payload.cached_message
    after = payload.message
    if before is None or after.author.bot:
        return
    if before.content == after.content:      # Filtra 'ediciones falsas', como la generación de vistas previas
        return

    guild = bot.get_guild(payload.guild_id)
    embed = make_embed("✏️ Mensaje editado", discord.Color.orange(), after.author)
    embed.add_field(name="Canal", value=after.channel.mention, inline=True)
    embed.add_field(name="Enlace", value=f"[Ir al mensaje]({after.jump_url})", inline=True)
    embed.add_field(name="Antes", value=cut(before.content), inline=False)
    embed.add_field(name="Después", value=cut(after.content), inline=False)
    await send_log(guild, embed)


@bot.event
async def on_raw_bulk_message_delete(payload: discord.RawBulkMessageDeleteEvent):
    if payload.guild_id is None:
        return
    guild = bot.get_guild(payload.guild_id)
    embed = make_embed("🧹 Borrado masivo de mensajes", discord.Color.dark_red())
    embed.add_field(name="Canal", value=f"<#{payload.channel_id}>", inline=True)
    embed.add_field(name="Cantidad", value=f"{len(payload.message_ids)} mensajes", inline=True)
    await send_log(guild, embed)

Vamos a desmenuzarlo 💡

  • payload.cached_message : si el mensaje borrado estaba en la caché del bot, es un objeto Message; si no, None. Los mensajes publicados antes de encender el bot no están en caché, así que se registran como "contenido no disponible", pero se guardan el canal y el ID del mensaje. Aun así, ayuda a investigar.
  • guild.audit_logs(limit=5, action=...) : es un iterador asíncrono que recorre el registro de auditoría del servidor del más reciente al más antiguo. Hay que recorrerlo con async for, no con for. Si el bot no tiene el permiso Ver registro de auditoría, lanza Forbidden, por eso lo envolvimos en un try.
  • entry.target / entry.extra.channel / entry.created_at : en una entrada message_delete, target es quien escribió el mensaje, extra.channel es el canal donde se borró y extra.count es cuántos se borraron. Si esos tres datos coinciden y la entrada se creó en los últimos 15 segundos, su entry.user (quien ejecutó la acción) es quien lo borró.
  • "El propio autor (o no se pudo confirmar)" : cuando alguien borra su propio mensaje, no queda ningún registro en el registro de auditoría. Por eso, si no lo encontramos ahí, lo tratamos como borrado por el propio autor. Eso sí, si el mismo administrador borra varios mensajes seguidos de la misma persona, Discord solo sube el count de la entrada existente sin crear una nueva, y a veces se nos escapa por la condición de 15 segundos. Solo recuerda que no es 100 % exacto.
  • payload.message (evento de edición) : es el mensaje después de editarse (añadido en la 2.5). El de antes se toma de cached_message. Cuando pegas un enlace, Discord crea un embed de vista previa y envía otro evento de edición; si content es igual, lo saltamos para que el registro no se llene de ruido.
  • on_raw_bulk_message_delete : cuando un administrador hace un borrado masivo (purge) como "borrar 100 mensajes", en lugar de eventos de borrado individuales llega este una sola vez. Si guardáramos todo el contenido, el registro explotaría, así que solo anotamos la cantidad.

📌 Paso 4 - Entradas, salidas, apodos y cambios de rol

Python
@bot.event
async def on_member_join(member: discord.Member):
    embed = make_embed("📥 Entró un miembro", discord.Color.green(), member)
    embed.add_field(name="Miembro", value=member.mention, inline=True)
    embed.add_field(name="Cuenta creada", value=discord.utils.format_dt(member.created_at, "R"), inline=True)
    embed.add_field(name="Miembros actuales", value=f"{member.guild.member_count}", inline=True)
    await send_log(member.guild, embed)


@bot.event
async def on_member_remove(member: discord.Member):
    embed = make_embed("📤 Salió un miembro", discord.Color.light_grey(), member)
    embed.add_field(name="Miembro", value=f"{member} ({member.mention})", inline=True)
    if member.joined_at:
        embed.add_field(name="Fecha de entrada", value=discord.utils.format_dt(member.joined_at, "R"), inline=True)
    roles = [r.mention for r in member.roles if r != member.guild.default_role]
    if roles:
        embed.add_field(name="Roles que tenía", value=" ".join(roles), inline=False)
    await send_log(member.guild, embed)


@bot.event
async def on_member_update(before: discord.Member, after: discord.Member):
    if before.nick != after.nick:
        embed = make_embed("🏷️ Cambio de apodo", discord.Color.blue(), after)
        embed.add_field(name="Antes", value=before.nick or "(ninguno)", inline=True)
        embed.add_field(name="Después", value=after.nick or "(ninguno)", inline=True)
        await send_log(after.guild, embed)

    added = [r for r in after.roles if r not in before.roles]
    removed = [r for r in before.roles if r not in after.roles]
    if added or removed:
        embed = make_embed("🎭 Cambio de roles", discord.Color.blurple(), after)
        if added:
            embed.add_field(name="➕ Añadidos", value=" ".join(r.mention for r in added), inline=False)
        if removed:
            embed.add_field(name="➖ Quitados", value=" ".join(r.mention for r in removed), inline=False)
        await send_log(after.guild, embed)
  • format_dt(momento, "R") : lo convierte al formato <t:1790291186:R> y Discord lo muestra como tiempo relativo, por ejemplo "hace 3 días" o "hace 2 años". ¿Que en el registro de entrada la cuenta se creó "hace 12 minutos"? Lo más probable es que sea un bot o una cuenta secundaria. Es el campo favorito de los moderadores 👀
  • on_member_remove : tanto si alguien se va por su cuenta como si lo expulsan o lo banean, todo llega por este evento. Por eso el "por qué se fue" lo registramos aparte con el evento del registro de auditoría del siguiente paso.
  • Comparar before.roles / after.roles : on_member_update se dispara cuando cambia cualquier cosa: apodo, roles, aislamiento temporal (timeout), foto de perfil del servidor, etc. Por eso dentro tienes que comparar tú mismo "qué cambió". Distinguimos los roles añadidos y quitados con la diferencia entre las listas de roles.

📌 Paso 5 - Expulsiones y baneos con el evento del registro de auditoría (quién y por qué)

Python
MOD_ACTIONS = {
    discord.AuditLogAction.kick: ("👢 Expulsión (Kick)", discord.Color.dark_orange()),
    discord.AuditLogAction.ban: ("🔨 Baneo (Ban)", discord.Color.dark_red()),
    discord.AuditLogAction.unban: ("🕊️ Desbaneo (Unban)", discord.Color.teal()),
}


@bot.event
async def on_audit_log_entry_create(entry: discord.AuditLogEntry):
    if entry.action not in MOD_ACTIONS:
        return
    title, color = MOD_ACTIONS[entry.action]
    embed = make_embed(title, color, entry.user)
    target = entry.target
    embed.add_field(name="Afectado", value=f"{target} (<@{target.id}>)" if target else "(desconocido)", inline=True)
    embed.add_field(name="Ejecutado por", value=entry.user.mention if entry.user else "(desconocido)", inline=True)
    embed.add_field(name="Motivo", value=entry.reason or "(sin motivo)", inline=False)
    await send_log(entry.guild, embed)
  • on_audit_log_entry_create : se llama en tiempo real en el momento en que se crea una nueva entrada en el registro de auditoría del servidor (discord.py 2.2+, requiere Intents.moderation y que el bot tenga el permiso Ver registro de auditoría). No es buscar a posteriori como en el paso 3: es Discord quien te avisa primero, así que es mucho más preciso.
  • entry.action : es un enumerado AuditLogAction que indica qué acción fue. Hoy solo elegimos expulsión, baneo y desbaneo, pero hay decenas más, como channel_create, role_update o member_role_update. Añadiendo una sola línea al diccionario, amplías los tipos de registro.
  • entry.user / entry.target / entry.reason : son, respectivamente, el administrador que ejecutó la acción, la persona afectada y el motivo que escribió el administrador al expulsar. Si no se escribe motivo, es None, así que lo cambiamos por "(sin motivo)".

💡 ¿Te sale None como ejecutor? Puede pasar si el usuario no está en la caché. entry.user_id (2.2+) siempre viene relleno, así que también puedes mostrar solo la mención con f"<@{entry.user_id}>".

📌 Paso 6 - Creación y borrado de canales + comandos para configurar el canal de registros

Python
@bot.event
async def on_guild_channel_create(channel: discord.abc.GuildChannel):
    embed = make_embed("📁 Canal creado", discord.Color.green())
    embed.add_field(name="Canal", value=f"{channel.mention} (`{channel.name}`)", inline=True)
    embed.add_field(name="Tipo", value=str(channel.type), inline=True)
    await send_log(channel.guild, embed)


@bot.event
async def on_guild_channel_delete(channel: discord.abc.GuildChannel):
    embed = make_embed("🗂️ Canal borrado", discord.Color.red())
    embed.add_field(name="Canal", value=f"`{channel.name}`", inline=True)
    embed.add_field(name="Tipo", value=str(channel.type), inline=True)
    await send_log(channel.guild, embed)


@bot.tree.command(name="로그설정", description="[Admin] Elige el canal al que se enviarán los registros del servidor")
@app_commands.describe(채널="Canal de texto donde se acumularán los registros")
@app_commands.checks.has_permissions(administrator=True)
async def set_log(interaction: discord.Interaction, 채널: discord.TextChannel):
    me = interaction.guild.me
    perms = 채널.permissions_for(me)
    if not (perms.send_messages and perms.embed_links):
        return await interaction.response.send_message(
            f"❌ No tengo permisos de **Enviar mensajes e Insertar enlaces** en {채널.mention}.", ephemeral=True)
    async with lock:
        log_channels[str(interaction.guild.id)] = 채널.id
        save()
    await interaction.response.send_message(f"✅ ¡A partir de ahora registraré los eventos del servidor en {채널.mention}!", ephemeral=True)
    await 채널.send(embed=make_embed("🟢 Configurado como canal de registros", discord.Color.green(), interaction.user))


@bot.tree.command(name="로그해제", description="[Admin] Deja de registrar los eventos del servidor")
@app_commands.checks.has_permissions(administrator=True)
async def unset_log(interaction: discord.Interaction):
    async with lock:
        log_channels.pop(str(interaction.guild.id), None)
        save()
    await interaction.response.send_message("⏹️ He desactivado el registro.", ephemeral=True)


@bot.tree.error
async def on_app_command_error(interaction: discord.Interaction, error: app_commands.AppCommandError):
    if isinstance(error, app_commands.MissingPermissions):
        await interaction.response.send_message("❌ Este comando es solo para administradores.", ephemeral=True)
    else:
        raise error


@bot.event
async def on_ready():
    load()
    await bot.tree.sync()
    print(f"✅ Sesión iniciada: {bot.user} / servidores con registro: {len(log_channels)}")


bot.run(token)

Respuesta visible solo para ti tras ejecutar el comando /로그설정 (configurar registros), el embed de configuración completada que llega al canal de registros y la lista de lo que el bot comprueba antes

  • 채널: discord.TextChannel : si le das tipo de canal a un argumento de comando de barra, Discord muestra una interfaz para elegir canal. Es la misma función que vimos en el post #15 de grupos de comandos.
  • 채널.permissions_for(me) : comprueba en el momento de configurarlo si el bot realmente puede escribir en el canal de registros. Avisar ahora es mucho más amable que fallar en silencio más tarde, cuando salte un evento.
  • bot.tree.error : si alguien que no es administrador usa el comando, se produce MissingPermissions, y si no lo capturas, solo aparece el poco útil mensaje "This interaction failed" (La interacción falló). Es el mismo patrón que usamos en el #25.

📎 Si pegas en orden los bloques de código de los pasos 1 a 6, tienes el bot completo. Guárdalo como logbot.py y ejecútalo directamente.

📌 Resultado

  1. Ejecuta python logbot.py → en la consola aparece ✅ 로그인: 봇이름 / 로그 설정된 서버 0개 (con el código en español: "✅ Sesión iniciada: NombreDelBot / servidores con registro: 0")
  2. Un administrador crea un canal privado como #서버-로그 (server-log) y ejecuta /로그설정 채널:#서버-로그 (el comando para configurar registros) → llega un embed verde "🟢 Configurado como canal de registros".
  3. Escribe un mensaje en cualquier canal y bórralo → en un embed rojo 🗑️ queda el contenido borrado tal cual. Si un administrador borra el mensaje de otra persona, en "Borrado por" aparece la mención del administrador.
  4. Si editas un mensaje, en un embed naranja ✏️ se ven el antes y el después uno junto al otro, y al pulsar el enlace saltas a ese mensaje.
  5. Si entras y sales del servidor con una cuenta secundaria, se acumulan 📥 entrada (¡cuenta creada "hace N minutos"!) → 📤 salida, y si expulsas a alguien, después del 📤 de salida aparece un embed 👢 de expulsión con ejecutor y motivo.

💡 Bloquea el canal de registros para que solo lo vean los administradores. Como guarda tal cual el contenido de los mensajes borrados, si los miembros normales lo ven recibirás quejas del tipo "lo borré, ¿por qué sigue ahí?". Y no olvides limpiar los registros antiguos más o menos una vez al mes.

💡 Extra - Más tipos de registro

Solo con añadir entradas al diccionario MOD_ACTIONS amplías los tipos de registro de auditoría. Estos son los más usados.

Python
discord.AuditLogAction.member_update: ("⏳ Aislamiento temporal / cambio de apodo", discord.Color.gold()),
discord.AuditLogAction.role_create: ("🎨 Rol creado", discord.Color.green()),
discord.AuditLogAction.role_delete: ("🎨 Rol borrado", discord.Color.red()),
discord.AuditLogAction.message_bulk_delete: ("🧹 Borrado masivo (administrador)", discord.Color.dark_red()),

Si imprimes entry.changes.before / entry.changes.after, puedes sacar hasta los detalles del cambio, como "el nombre del rol pasó de A a B" o "aislamiento temporal hasta cuándo". Y el registro de entradas y salidas de canales de voz es tan simple como añadir una línea de embed al on_voice_state_update que usamos en el #31.

📌 Errores frecuentes

  1. El registro de borrado solo muestra "(sin contenido)" → causa: no activaste Message Content Intent en el Portal para desarrolladores o falta intents.message_content = True en el código → solución: activa ambos y reinicia el bot. Los mensajes publicados antes de encender el bot no están en caché, así que es normal que no se vean.
  2. on_member_join / on_member_remove no se disparan → causa: falta configurar Server Members Intent → solución: actívalo en la pestaña Bot del portal y comprueba intents.members = True. Si no lo activas, no se disparan en silencio y sin ningún error, y eso confunde.
  3. discord.errors.Forbidden: 403 Forbidden (error code: 50013): Missing Permissions → causa: el rol del bot no tiene el permiso Ver registro de auditoría → solución: Server Settings (서버 설정) → Roles (역할) → marca "Ver registro de auditoría" (View Audit Log) en el rol del bot. Sin este permiso, on_audit_log_entry_create tampoco se dispara.
  4. HTTPException: 400 Bad Request (error code: 50035): Invalid Form Body — In embeds.0.fields.2.value: Must be 1024 or fewer in length → causa: metiste más de 1024 caracteres en un campo de embed → solución: recórtalo como hace cut() en el ejemplo. Si los mensajes largos son habituales, también es buena idea adjuntar el contenido como archivo de texto con discord.File.
  5. TypeError: 'async_generator' object is not iterable → causa: recorriste guild.audit_logs() con for → solución: cámbialo a async for. Es un iterador asíncrono.
  6. El registro de edición aparece dos veces cada vez que pego un enlace → causa: Discord envía otro evento de edición al añadir la vista previa (embed) del enlace → solución: como en el ejemplo, sáltalo si before.content == after.content.
  7. El bot ni siquiera arranca por discord.errors.PrivilegedIntentsRequired → causa: activaste los intents privilegiados en el código pero no en el portal → solución: pestaña Bot del portal → Privileged Gateway Intents, activa los dos.

🙌 Para terminar

Hoy combinamos tres cosas, eventos raw + consulta del registro de auditoría (Audit Log) + on_audit_log_entry_create, y completamos un bot de registros que anota en un solo canal, como embeds, desde mensajes borrados y editados hasta entradas, salidas, cambios de rol, expulsiones y baneos. En resumen:

  • El contenido de un mensaje borrado solo se ve si está en la caché → sube max_messages y activa los 2 intents privilegiados
  • Quién lo borró se busca en guild.audit_logs() por target, canal y tiempo (si lo borra el propio autor, no hay registro)
  • Las expulsiones y baneos llegan en tiempo real con ejecutor y motivo mediante on_audit_log_entry_create
  • Límite de 1024 caracteres por campo de embed, y el canal de registros bloqueado solo para administradores

En la próxima entrega veremos la ampliación de las salas de voz temporales que adelantamos en el #31: delegar el dueño de la sala, expulsar, salas solo con invitación (lista blanca) y una "sala de party" que crea también un canal de texto. ¡No te la pierdas! 🙌

¿Qué registro necesitas más en tu servidor? Cuéntamelo en los comentarios y lo añadiré a la sección extra 😎


📚 También te puede interesar

Serie · Parte 31 de 32

Crear un bot de Discord

  1. #1Crear un bot de Discord - ¡Domina por completo los Slash Commands!
  2. #2¡Añade opciones (argumentos) a tus Slash Commands!
  3. #3¡Añade subcomandos (Subcommand) a tus Slash Commands!
  4. #4¡Envía embeds con Slash Commands!
  5. #5¡Implementa interacciones con botones (Button)!
  6. #6¡Implementa menús desplegables (Select Menu)!
  7. #7Pide texto al usuario con un modal (Modal) (ejemplo en Python)
  8. #8Decora embeds (Embed) con imágenes y campos (ejemplo en Python)
  9. #9¡Integra la IA (API de Claude y ChatGPT)! (ejemplo en Python)
  10. #10¡Añade soporte multilingüe (i18n)! (ejemplo en Python)
  11. #11¡Crea un panel web con Flask! (ejemplo en Python)
  12. #12¡Gestiona los cooldowns y permisos de tus comandos! (ejemplo en Python)
  13. #13¡Administra tu servidor automatizando roles y canales! (ejemplo en Python)
  14. #14¡Guarda tus datos! JSON vs SQLite (ejemplo en Python)
  15. #15슬래시 명령어 그룹(Group)으로 명령어 정리하기! (파이썬 예제) (en coreano)
  16. #16리마인더·알림 기능 만들기! 봇이 먼저 말 걸게 하기 (파이썬 예제) (en coreano)
  17. #17RAG로 문서 기반 답변봇 만들기! 우리 서버 전용 FAQ 봇 (파이썬 예제) (en coreano)
  18. #18투표(Poll) 기능 만들기! 다수결로 정하는 서버 규칙 (파이썬 예제) (en coreano)
  19. #19Cog로 코드 쪼개기! 1000줄 봇도 안 더러워지는 구조 (파이썬 예제) (en coreano)
  20. #20자동 환영 메시지·온보딩 만들기! 규칙 동의 버튼으로 자동 역할 부여 (파이썬 예제) (en coreano)
  21. #21키워드 자동응답·금지어 필터 만들기! 경고 누적으로 자동 제재까지 (파이썬 예제) (en coreano)
  22. #22슬래시 명령어 자동완성(Autocomplete) 만들기! 금지어를 골라서 추가·삭제 (파이썬 예제) (en coreano)
  23. #23aiosqlite로 비동기 DB 제대로 설계하기! JSON 탈출 (파이썬 예제) (en coreano)
  24. #24채널 난독화(Channel Obfuscation) 대응하기! 11/16부터 안 보이는 채널이 생겨요 (파이썬 예제) (en coreano)
  25. #25봇이 죽지 않게! 에러 핸들링·로깅 체계 잡기 (파이썬 예제) (en coreano)
  26. #26티켓(Ticket) 문의 시스템 만들기! 버튼 하나로 1:1 상담 채널 자동 생성 (파이썬 예제) (en coreano)
  27. #27티켓 담당자 배정·우선순위 만들기! 스태프가 여러 명이어도 안 꼬이게 (파이썬 예제) (en coreano)
  28. #28레벨·경험치(XP) 시스템 만들기! 채팅할수록 레벨업, 랭킹판까지 (파이썬 예제) (en coreano)
  29. #29화폐·상점(Economy) 시스템 만들기! 코인 벌어서 아이템 사기 (파이썬 예제) (en coreano)
  30. #30¡Crea un bot de encuestas y votaciones (Poll)! Encuestas nativas + botones con recuento en tiempo real (ejemplo en Python) 🗳️
  31. #32¡Crea un canal de registros y auditoría (Audit Log) del servidor! Registra automáticamente mensajes borrados y editados, entradas, salidas y expulsiones (ejemplo en Python) 🕵️Estás aquí
  32. #33임시 음성방 확장, 파티방 만들기! 방장 위임·강퇴·초대 전용(비공개) 방 + 전용 채팅방 자동 생성 (파이썬 예제) 🎉 (en coreano)
Ver las 32Contraer

Autor

Mojangcam

Un registro práctico de desarrollo e IT: código que escribo, servidores que mantengo y los errores que resuelvo por el camino.