Categoría: Bots de Discord

Crear un bot de Discord#12¡Gestiona los cooldowns y permisos de tus comandos! (ejemplo en Python)

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

Índice12 secciones

¡Hola! Soy Mojangcam 😎

En el #11 creamos con Flask un panel web para ver el estado del bot. Ya tiene bastantes funciones, pero en cuanto lo metes en un servidor de verdad, siempre pasa algo así.

Alguien spamea /뽑기 (sorteo) 10 veces por segundo y llena el canal, o cualquiera pulsa /청소 (limpiar) y borra de golpe las conversaciones de los demás. 😱

Por eso, en este episodio vamos a aprender el cooldown de comandos (Cooldown) y las comprobaciones de permisos (Permission) y roles (Role). ¡Con solo configurar estas dos cosas, tu bot de repente parece un "bot bien administrado"!

🧠 ¿Por qué necesitas cooldowns y permisos?

  • Evitar el spam : si el spam acumula peticiones a la API, chocas con el límite de velocidad (rate limit) de Discord y el bot entero deja de responder.
  • Equilibrio : los comandos con recompensa, como la asistencia, los sorteos o la experiencia, no tienen sentido sin un tiempo de espera.
  • Evitar accidentes : si cualquiera puede usar comandos como /청소 o /밴 (banear), el servidor se viene abajo en un día.
  • Manejo de errores : si el bot se queda callado cuando una comprobación falla, el usuario piensa "¿Se rompió el bot?" y vuelve a spamear. Es imprescindible mostrar un mensaje que lo explique.

📌 Lo que necesitas

  • Python 3.8 o superior (se recomienda 3.10+)
  • discord.py 2.x — este artículo usa la última versión, la 2.7.1
  • El token de tu bot de Discord (si todavía no lo tienes, ¡empieza por el #1 Domina por completo los Slash Commands!)
  • Un servidor de Discord de pruebas (donde puedas crear roles libremente)
cmake
pip install -U discord.py

Así compruebas la versión.

CSS
python -c "import discord; print(discord.__version__)"
# 2.7.1

⚠️ La versión 1.x no es compatible con los slash commands. Si tu versión no empieza por 2., actualízala sin falta con la opción -U.

📌 Paso 1 - Añade un cooldown (Cooldown)

Ponerle un tiempo de espera a un slash command se hace con una sola línea de decorador.

Python
import discord
from discord import app_commands
from discord.ext import commands

intents = discord.Intents.default()
bot = commands.Bot(command_prefix="!", intents=intents)

@bot.tree.command(name="출석", description="¡Registra tu asistencia una vez al día!")
@app_commands.checks.cooldown(1, 60.0, key=lambda i: (i.guild_id, i.user.id))
async def attend(interaction: discord.Interaction):
    await interaction.response.send_message(
        f"¡{interaction.user.mention}, ya registraste tu asistencia! ✅"
    )

Vamos a verlo parte por parte 💡

  • app_commands.checks.cooldown(rate, per, *, key) : es el decorador de cooldown exclusivo para slash commands.
  • rate : cuántas veces se puede usar en per segundos. El código de arriba usa 1, así que es 1 vez.
  • per : la duración del cooldown (en segundos). Como es 60.0, es 1 vez cada 60 segundos.
  • key : es la función que decide "a quién se le cuenta". Su valor de retorno puede ser cualquier cosa hashable.
  • key=lambda i: (i.guild_id, i.user.id) : combina servidor + usuario, así que aunque sea la misma persona, en otro servidor el cooldown corre por separado.

💡 Si omites key por completo, el valor predeterminado es por ID de usuario (compartido entre todos los servidores). Al revés, si pasas key=None, se convierte en un cooldown global que comparten todos. ¡Perfecto para comandos de eventos de todo el servidor!

Además, el orden de los decoradores importa. @bot.tree.command() siempre tiene que ir arriba del todo. Si lo pones debajo, no se registra como comando.

📌 Paso 2 - Avisa cuando se active el cooldown

Si solo ejecutas el código del paso 1, cuando alguien choca con el cooldown el bot no reacciona en absoluto. Para el usuario, es simplemente un bot roto. Vamos a añadir un manejador de errores.

Python
@attend.error
async def attend_error(interaction: discord.Interaction, error: app_commands.AppCommandError):
    if isinstance(error, app_commands.CommandOnCooldown):
        await interaction.response.send_message(
            f"⏳ ¡Demasiado rápido! Vuelve a intentarlo en **{error.retry_after:.1f} segundos**.",
            ephemeral=True,
        )

Vamos a verlo parte por parte 💡

  • @명령어이름.error (el nombre de tu comando + .error) : registra un manejador de errores exclusivo de ese comando. Debe recibir exactamente 2 parámetros: (interaction, error).
  • app_commands.CommandOnCooldown : es la excepción que llega cuando se activa el cooldown.
  • error.retry_after : un valor float que indica cuántos segundos más hay que esperar. Con :.1f lo dejamos bonito, con un solo decimal.
  • ephemeral=True : es un mensaje que solo ve la persona que escribió el comando. Evita que el canal se llene de avisos.

💡 Por cierto, con error.cooldown también puedes sacar los valores originales (rate, per). Es útil cuando quieres poner un aviso como "Este comando solo se puede usar 1 vez cada 60 segundos".

📌 Paso 3 - Comprueba los permisos (Permission)

Ahora vamos a bloquear un comando peligroso. Un comando que borra mensajes solo debería poder usarlo quien tenga el permiso de Gestionar mensajes, ¿verdad?

less
@bot.tree.command(name="청소", description="Borra mensajes del canal de una sola vez")
@app_commands.describe(amount="Cantidad de mensajes a borrar (1-100)")
@app_commands.default_permissions(manage_messages=True)
@app_commands.checks.has_permissions(manage_messages=True)
@app_commands.checks.bot_has_permissions(manage_messages=True)
@app_commands.guild_only()
async def clean(interaction: discord.Interaction, amount: app_commands.Range[int, 1, 100]):
    await interaction.response.defer(ephemeral=True)
    deleted = await interaction.channel.purge(limit=amount)
    await interaction.followup.send(f"🧹 ¡Borré {len(deleted)} mensajes!", ephemeral=True)

Vamos a verlo parte por parte 💡

  • has_permissions(manage_messages=True) : comprueba si el usuario tiene ese permiso. Si no lo tiene, se lanza la excepción MissingPermissions.
  • bot_has_permissions(manage_messages=True) : comprueba si el bot tiene ese permiso. Si no, BotMissingPermissions. ¡Si te lo saltas, el comando revienta a mitad del borrado!
  • default_permissions(manage_messages=True) : oculta por completo el comando de la lista a quien no tenga el permiso.
  • guild_only() : impide usarlo en los mensajes directos (MD).
  • app_commands.Range[int, 1, 100] : limita el valor de entrada a 1-100. Discord lo filtra por ti.

⚠️ ¡No confíes solo en default_permissions! La documentación oficial dice claramente que solo "sirve como pista (serves as a hint)". Los administradores del servidor pueden cambiar este valor en la configuración de Discord. Por eso, si de verdad quieres bloquearlo, tienes que poner también has_permissions. Además, default_permissions se ignora en los subcomandos y, como lo procesa el servidor de Discord, tampoco se llama al manejador de errores.

📌 Paso 4 - Limita por rol (Role)

A menudo querrás restringir con roles que creaste tú en el servidor, como un "rol de staff", en lugar de con permisos.

less
@bot.tree.command(name="공지", description="Envía un anuncio al servidor")
@app_commands.describe(message="Contenido del anuncio")
@app_commands.checks.has_any_role("Administrador", "Staff")
@app_commands.guild_only()
async def notice(interaction: discord.Interaction, message: str):
    embed = discord.Embed(title="📢 Anuncio", description=message, color=0x5865F2)
    embed.set_footer(text=f"Autor: {interaction.user.display_name}")
    await interaction.response.send_message(embed=embed)

Vamos a verlo parte por parte 💡

  • has_role("이름") (el nombre del rol va entre las comillas) : exige un rol concreto. Si no lo tiene, MissingRole.
  • has_any_role("A", "B") : pasa si tiene al menos uno de los roles indicados. Si no, MissingAnyRole.
  • También puedes pasar los roles por ID (entero) en lugar de por nombre (cadena). Los nombres fallan con erratas o cambios, así que en un bot real se recomienda usar el ID.
CSS
@app_commands.checks.has_any_role(123456789012345678, 987654321098765432)

💡 Extra - Libera a los VIP del cooldown (dynamic_cooldown)

También puedes hacer cosas como "un cooldown más corto para el rol de patrocinador". Solo tienes que usar dynamic_cooldown.

Python
from typing import Optional

VIP_ROLE_ID = 123456789012345678  # ID del rol libre de cooldown

def vip_cooldown(interaction: discord.Interaction) -> Optional[app_commands.Cooldown]:
    user = interaction.user
    if isinstance(user, discord.Member) and user.get_role(VIP_ROLE_ID):
        return None                          # ¡Si devuelves None, no hay cooldown!
    return app_commands.Cooldown(1, 30.0)    # Usuarios normales: 1 vez cada 30 segundos

@bot.tree.command(name="뽑기", description="Haz un sorteo al azar")
@app_commands.checks.dynamic_cooldown(vip_cooldown, key=lambda i: (i.guild_id, i.user.id))
async def draw(interaction: discord.Interaction):
    await interaction.response.send_message(f"🎲 Resultado: ¡**{random.randint(1, 100)} puntos**!")

Lo clave es que si devuelves None, ese usuario queda totalmente libre del cooldown. Según la condición, solo tienes que crear y devolver tú mismo un objeto Cooldown(rate, per).

📌 Código completo (puedes copiar y pegar)

Python
# discord.py 2.7.1 / Python 3.8+
import random
import traceback
from typing import Optional

import discord
from discord import app_commands
from discord.ext import commands

TOKEN = "PON_AQUI_EL_TOKEN_DE_TU_BOT"
VIP_ROLE_ID = 123456789012345678  # ID del rol libre de cooldown

intents = discord.Intents.default()
bot = commands.Bot(command_prefix="!", intents=intents)

@bot.event
async def on_ready():
    await bot.tree.sync()
    print(f"✅ Sesión iniciada: {bot.user}")

# ── 1) Cooldown: 1 vez cada 60 segundos por servidor y usuario ──
@bot.tree.command(name="출석", description="¡Registra tu asistencia una vez al día!")
@app_commands.checks.cooldown(1, 60.0, key=lambda i: (i.guild_id, i.user.id))
async def attend(interaction: discord.Interaction):
    await interaction.response.send_message(
        f"¡{interaction.user.mention}, ya registraste tu asistencia! ✅"
    )

# ── 2) Permisos: solo quien tenga Gestionar mensajes ────────
@bot.tree.command(name="청소", description="Borra mensajes del canal de una sola vez")
@app_commands.describe(amount="Cantidad de mensajes a borrar (1-100)")
@app_commands.default_permissions(manage_messages=True)
@app_commands.checks.has_permissions(manage_messages=True)
@app_commands.checks.bot_has_permissions(manage_messages=True)
@app_commands.guild_only()
async def clean(interaction: discord.Interaction, amount: app_commands.Range[int, 1, 100]):
    await interaction.response.defer(ephemeral=True)
    deleted = await interaction.channel.purge(limit=amount)
    await interaction.followup.send(f"🧹 ¡Borré {len(deleted)} mensajes!", ephemeral=True)

# ── 3) Roles: solo 'Administrador' o 'Staff' ────────────────
@bot.tree.command(name="공지", description="Envía un anuncio al servidor")
@app_commands.describe(message="Contenido del anuncio")
@app_commands.checks.has_any_role("Administrador", "Staff")
@app_commands.guild_only()
async def notice(interaction: discord.Interaction, message: str):
    embed = discord.Embed(title="📢 Anuncio", description=message, color=0x5865F2)
    embed.set_footer(text=f"Autor: {interaction.user.display_name}")
    await interaction.response.send_message(embed=embed)

# ── 4) Extra: los VIP no tienen cooldown ────────────────────
def vip_cooldown(interaction: discord.Interaction) -> Optional[app_commands.Cooldown]:
    user = interaction.user
    if isinstance(user, discord.Member) and user.get_role(VIP_ROLE_ID):
        return None
    return app_commands.Cooldown(1, 30.0)

@bot.tree.command(name="뽑기", description="Haz un sorteo al azar")
@app_commands.checks.dynamic_cooldown(vip_cooldown, key=lambda i: (i.guild_id, i.user.id))
async def draw(interaction: discord.Interaction):
    await interaction.response.send_message(f"🎲 Resultado: ¡**{random.randint(1, 100)} puntos**!")

# ── 5) Manejador de errores global (común a todos los comandos) ──
@bot.tree.error
async def on_app_command_error(interaction: discord.Interaction, error: app_commands.AppCommandError):
    if isinstance(error, app_commands.CommandOnCooldown):
        msg = f"⏳ ¡Demasiado rápido! Vuelve a intentarlo en **{error.retry_after:.1f} segundos**."
    elif isinstance(error, app_commands.MissingPermissions):
        msg = "🚫 No tienes permiso para usar este comando."
    elif isinstance(error, app_commands.BotMissingPermissions):
        msg = "🤖 ¡El bot no tiene permisos! Revisa los permisos del bot en Ajustes del servidor → Roles."
    elif isinstance(error, app_commands.MissingAnyRole):
        msg = "🎭 Este comando requiere el rol Administrador o Staff."
    elif isinstance(error, app_commands.CheckFailure):
        msg = "❌ No se cumplen las condiciones para ejecutar este comando."
    else:
        msg = "⚠️ Ocurrió un error desconocido. Vuelve a intentarlo en un momento."
        traceback.print_exception(type(error), error, error.__traceback__)

    if interaction.response.is_done():
        await interaction.followup.send(msg, ephemeral=True)
    else:
        await interaction.response.send_message(msg, ephemeral=True)

bot.run(TOKEN)

💡 En el código completo quité a propósito el @attend.error del paso 2. ¡Te explico por qué justo abajo, en el error frecuente n.º 3!

📌 Resultado

Enciende el bot y prueba los comandos uno por uno en tu servidor: así es como funcionan.

Primero, si escribes /출석 (asistencia), aparece el mensaje de asistencia registrada. ¿Y si lo escribes otra vez enseguida? El aviso de cooldown solo lo ves tú.

¿Ves que debajo del aviso aparece "Solo tú puedes ver esto (이 메시지는 본인만 볼 수 있어요)"? Ese es justo el efecto de ephemeral=True. En el canal no queda ni rastro.

La diferencia de permisos se nota directamente en la lista de comandos. Es el mismo servidor y el mismo bot, pero cambia según los permisos de la cuenta.

A una cuenta sin el permiso de Gestionar mensajes, /청소 ni siquiera le aparece. Es porque default_permissions lo filtra en el lado del servidor de Discord. Y aunque un administrador del servidor abra la configuración de permisos y el comando se vuelva visible, has_permissions lo bloquea una vez más y aparece "🚫 No tienes permiso".

Del mismo modo, /공지 (anuncio) se rechaza con "🎭 Requiere el rol" si no tienes el rol de Administrador o Staff, y si spameas /뽑기 con una cuenta que tenga el rol VIP, sigue funcionando sin cooldown.

📌 Errores frecuentes

1. TypeError: error handler must have 2 parameters

  • Causa : la función del manejador de errores no tiene exactamente 2 parámetros. Pasa cuando le añadiste self o solo recibe uno.
  • Solución : déjala siempre con exactamente 2, como async def handler(interaction, error):. Si lo usas dentro de un Cog, con self serían 3, así que en un Cog sobrescribe cog_app_command_error.

2. Se activa el cooldown, pero el bot no dice nada

  • Causa : no añadiste un manejador de errores. Aunque se lance CommandOnCooldown, si nadie lo captura solo queda en el log y al usuario no le llega nada.
  • Solución : registra sin falta el manejador global @bot.tree.error. (Bloque 5 del código completo)

3. El aviso llega dos veces o sale el error InteractionResponded

  • Causa : registraste a la vez un manejador individual (@명령어.error) y uno global (@bot.tree.error). discord.py siempre llama al manejador global, incluso después de ejecutar el individual. Así que intenta responder otra vez a una interacción ya respondida y revienta con InteractionResponded.
  • Solución : usa solo uno de los dos. Te recomiendo unificarlo todo en el manejador global, como en el código completo. Si de verdad necesitas ambos, sal del manejador global con if interaction.response.is_done(): return.

4. MissingPermissions aparece incluso a los administradores

  • Causa : has_permissions mira los permisos reales calculados por canal. Si ese permiso está denegado en una sobrescritura de permisos del canal (Override), puede bloquear incluso a un administrador del servidor.
  • Solución : en Ajustes del canal → Permisos, comprueba que el permiso de ese rol no esté desactivado con ❌.

5. /청소 se detiene a mitad de ejecución

  • Causa : el bot no tiene el permiso de Gestionar mensajes, o intentó borrar mensajes de hace más de 14 días. Discord no permite borrar en bloque mensajes de más de 14 días.
  • Solución : fíltralo primero con bot_has_permissions y, en canales antiguos, prueba con una cantidad menor.

💬 Para terminar

Hoy le añadimos a nuestro bot de Discord cooldowns y comprobaciones de permisos y roles. En resumen 🙌

  • checks.cooldown(rate, per, key=...) → evita el spam; con key eliges libremente el criterio
  • checks.has_permissions / bot_has_permissions → comprueba los permisos del usuario y del bot
  • checks.has_any_role → restringe por rol del servidor (se recomienda usar el ID)
  • default_permissions → lo oculta de la lista, pero es solo una pista, así que nunca lo uses solo
  • @bot.tree.error → gestiona todos los avisos en un solo lugar

En el próximo episodio veremos Guardar datos del bot de Discord - JSON vs SQLite. Es imprescindible si quieres que los registros de asistencia o los puntos se conserven aunque el bot se apague. ¡No te lo pierdas! 🙌

Si tienes dudas o te atascas en algo, déjalo en los comentarios 💬

📚 Serie Crear un bot de Discord

  1. #1 - Domina por completo los Slash Commands
  2. #2 - Añade opciones (argumentos) a tus Slash Commands
  3. #3 - Añade subcomandos (Subcommand)
  4. #4 - Envía embeds con Slash Commands
  5. #5 - Implementa interacciones con botones (Button)
  6. #6 - Implementa menús desplegables (Select Menu)
  7. #7 - Pide texto al usuario con un modal (Modal)
  8. #8 - Decora embeds con imágenes y campos
  9. #9 - Integra la IA (API de Claude y ChatGPT)
  10. #10 - Añade soporte multilingüe (i18n)
  11. #11 - Crea un panel web con Flask
  12. #12 - Gestiona los cooldowns y permisos de los comandos ← ¡estás leyendo este!

👉 También te puede interesar: Guía para solucionar errores de bots de Discord - ¡Desde errores de intents y token hasta cuando el bot no responde!

Serie · Parte 12 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)Estás aquí
  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) 🕵️
  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.