1. Crea la aplicación
- En el Discord Developer Portal, pulsa New Application.
- En la página Bot, restablece y copia el token. Guárdalo en una variable de entorno.
- Deja apagados los intents privilegiados. Este bot no los necesita; los intents del gateway de Discord explicados explica por qué.
- Invita al bot con los scopes
botyapplications.commandsy solo los permisos que usa. La calculadora de permisos genera la URL.
Los conceptos de la plataforma están en la guía de bots de Discord.
2. Instala discord.py
La versión más reciente cuando se escribió esta guía era discord.py 2.7.1; requiere Python 3.8 o más reciente, aunque conviene usar una versión actual de Python.
python3 -m venv .venv && . .venv/bin/activate
pip install "discord.py==2.*"
3. Cliente con árbol de comandos
# bot.py
import os
import discord
from discord import app_commands
TEST_GUILD = discord.Object(id=int(os.environ["TEST_GUILD_ID"]))
class Bot(discord.Client):
def __init__(self):
super().__init__(intents=discord.Intents.default())
self.tree = app_commands.CommandTree(self)
async def setup_hook(self):
# En desarrollo: copia los comandos a un servidor para que se actualicen al instante
self.tree.copy_global_to(guild=TEST_GUILD)
await self.tree.sync(guild=TEST_GUILD)
bot = Bot()
@bot.event
async def on_ready():
print(f"Conectado como {bot.user} ({bot.user.id})")
@bot.tree.command(description="Comprueba que el bot está vivo")
async def ping(interaction: discord.Interaction):
await interaction.response.send_message(f"Pong ({round(bot.latency * 1000)} ms)", ephemeral=True)
bot.run(os.environ["DISCORD_TOKEN"])
Intents.default() excluye los intents privilegiados, y los comandos de barra llegan como interacciones de todos modos. setup_hook corre una vez antes de conectar, el lugar adecuado para sincronizar.
Sincronizar en producción. tree.sync() sin servidor publica comandos globales. Tiene límite de tasa, así que no lo llames en cada arranque; sincroniza cuando cambien los comandos, por ejemplo con un comando solo para el dueño o un script de despliegue.
4. Opciones tipadas y listas de opciones
discord.py convierte las anotaciones de tipo en opciones del comando, y Discord las valida antes de que corra tu código:
from typing import Literal
@bot.tree.command(description="Tira dados")
@app_commands.describe(caras="Número de caras", cantidad="Cuántos dados")
async def dados(interaction: discord.Interaction,
caras: app_commands.Range[int, 2, 100] = 6,
cantidad: app_commands.Range[int, 1, 10] = 1):
import random
tiradas = [random.randint(1, caras) for _ in range(cantidad)]
await interaction.response.send_message(f"{tiradas} (total {sum(tiradas)})")
@bot.tree.command(description="Elige tus notificaciones")
async def avisos(interaction: discord.Interaction, modo: Literal["todo", "menciones", "nada"]):
await interaction.response.send_message(f"Notificaciones: {modo}", ephemeral=True)
Literal se convierte en una lista fija de opciones; las anotaciones discord.Member, discord.TextChannel y discord.Role se convierten en selectores de usuario, canal y rol. Revisa el JSON del comando con nuestro constructor de comandos de Discord.
5. Trabajo lento: difiere
Tienes 3 segundos para responder. Para algo más lento, difiere y envía un mensaje de seguimiento en menos de 15 minutos:
@bot.tree.command(description="Resume una página web")
async def resumir(interaction: discord.Interaction, url: str):
await interaction.response.defer(thinking=True)
texto = await obtener_y_resumir(url) # tu función lenta
await interaction.followup.send(texto[:2000]) # el límite de contenido es 2000 caracteres
Las respuestas largas hay que dividirlas; el divisor de mensajes muestra cómo.
6. Botones con views
class Confirmar(discord.ui.View):
def __init__(self, autor_id: int):
super().__init__(timeout=60)
self.autor_id = autor_id
async def interaction_check(self, interaction: discord.Interaction) -> bool:
return interaction.user.id == self.autor_id # solo quien llamó puede pulsar
@discord.ui.button(label="Confirmar", style=discord.ButtonStyle.danger)
async def confirmar(self, interaction: discord.Interaction, button: discord.ui.Button):
await interaction.response.edit_message(content="Listo.", view=None)
self.stop()
@bot.tree.command(description="Borra tus datos guardados")
async def olvidar(interaction: discord.Interaction):
await interaction.response.send_message("¿Seguro?", view=Confirmar(interaction.user.id), ephemeral=True)
Las views viven en memoria; tras un reinicio, los botones viejos dejan de funcionar salvo que registres views persistentes con un custom_id fijo y timeout=None.
7. Permisos y errores
Restringe comandos de forma declarativa:
@bot.tree.command(description="Borra mensajes recientes")
@app_commands.default_permissions(manage_messages=True)
@app_commands.guild_only()
async def purgar(interaction: discord.Interaction, cantidad: app_commands.Range[int, 1, 100]):
await interaction.response.defer(ephemeral=True)
borrados = await interaction.channel.purge(limit=cantidad)
await interaction.followup.send(f"Borré {len(borrados)} mensajes.", ephemeral=True)
default_permissions oculta el comando a los miembros sin ese permiso (los administradores pueden cambiarlo), y el propio bot también necesita Gestionar mensajes en ese canal.
Captura los errores en un solo lugar:
@bot.tree.error
async def on_app_command_error(interaction: discord.Interaction, error: app_commands.AppCommandError):
msg = "Algo salió mal."
if isinstance(error, app_commands.CheckFailure):
msg = "No puedes usar este comando aquí."
if interaction.response.is_done():
await interaction.followup.send(msg, ephemeral=True)
else:
await interaction.response.send_message(msg, ephemeral=True)
raise error # conserva el traceback en los logs
Si algo sigue fallando en silencio, revisa bot de Discord que no responde.
8. Córrelo 24/7
Un bot de Discord mantiene abierta una conexión al gateway, así que necesita un proceso siempre encendido; las funciones serverless que se duermen no sirven para bots de gateway. Opciones:
- Un VPS pequeño con systemd o Docker: bot en Python con Docker, tutorial de VPS con systemd.
- Un servicio worker en un PaaS: desplegar un bot en Railway.
- Comparativa de hostings: hosting 24/7 para bots de Discord.
Maneja SIGTERM para que los reinicios no dejen dos sesiones corriendo; mira apagado limpio para bots.
Fuentes
- Documentación de discord.py: discordpy.readthedocs.io
- discord.py, referencia de interacciones (app_commands): discordpy.readthedocs.io/en/stable/interactions/api.html
- Repositorio y ejemplos de discord.py: github.com/Rapptz/discord.py
- Discord Developer Docs, comandos de aplicación: discord.com/developers/docs/interactions/application-commands
Comparativas y herramientas relacionadas
Preguntas frecuentes
¿Necesito el intent de Message Content para comandos de barra en discord.py?
No. Los comandos de barra, botones y modales llegan como interacciones con sus propios datos. Message Content solo hace falta para leer el texto de mensajes normales.
¿Por qué no aparecen mis comandos?
No se sincronizaron, o se sincronizaron en otro servidor. Sincroniza en un servidor de pruebas mientras desarrollas, globalmente al publicar, e invita al bot con el scope applications.commands.