Bot de sorteos para Discord en Python: botón para participar, ganadores al azar y repetir
Los bots de sorteos más usados son de otros: llevan su marca, sus anuncios y a veces cobran por más de un ganador. Este lo haces tú en discord.py, en un fichero: /sorteo publica el premio con un botón «Participar» y la cuenta atrás, al acabar elige a los ganadores al azar y los menciona, y /resortear saca otro si alguno no contesta. Los sorteos se guardan en un fichero, así que un reinicio no los borra.
Qué hace el bot
Quien puede gestionar el servidor escribe /sorteo con el premio, los minutos que dura y cuántos ganan. El bot publica una tarjeta con el premio, la hora a la que termina (cada uno la ve en su zona horaria, con la cuenta atrás) y el botón «Participar». Pulsarlo apunta; pulsarlo otra vez desapunta. La respuesta solo la ve quien pulsa, así que el canal no se llena de «me apunto».
Cada 30 segundos el bot mira si algún sorteo ha terminado. Cuando acaba uno, elige a los ganadores entre los apuntados, los menciona en el canal respondiendo a la tarjeta y quita el botón. Si un ganador no da señales, /resortear elige a otro sin repetir a los que ya ganaron.
Elige con el azar del sistema (el módulo secrets de Python), no con una semilla que se pueda adivinar. Y no hace falta ningún intent especial: los botones y los comandos llegan como interacciones.
¿Prefieres no montarlo tú? Hosting de bots de Discord desde 1,50 € al mes, sin permanencia y con devolución íntegra en 72 horas. O crea un bot de Discord gratis, sin tarjeta.
1. Crea el bot en Discord
Crea la aplicación y saca el token
En discord.com/developers/applications, «New Application»; en la pestaña «Bot», «Reset Token» y copia el token. Es la contraseña del bot: no lo compartas ni lo subas a GitHub. Si es tu primer bot, el proceso con calma está en tu primer bot en Python.
Invítalo con los permisos justos
En «OAuth2» → «URL Generator», marca «bot» y «applications.commands», y en los permisos «View Channels», «Send Messages», «Embed Links» y «Read Message History» (sin este último no puede responder a la tarjeta ni quitarle el botón). Con la calculadora de permisos de bot los ves en castellano y te da el enlace. Los «Privileged Gateway Intents» se quedan apagados.
2. El código
Tres ficheros en una carpeta. El token va en .env, sin comillas. Hace falta Python 3.10 o más nuevo. Los sorteos se guardan en sorteos.json, que el bot crea solo junto a app.py.
discord.py
python-dotenvDISCORD_TOKEN=pega-aquí-tu-tokenimport json
import os
import secrets
import time
from pathlib import Path
import discord
from discord import app_commands
from discord.ext import tasks
from dotenv import load_dotenv
load_dotenv() # lee el token del fichero .env
# Los sorteos se guardan junto al bot: un reinicio no los borra.
SORTEOS = Path("sorteos.json")
azar = secrets.SystemRandom() # el azar del sistema: nadie puede adivinar quién gana
def leer() -> dict:
return json.loads(SORTEOS.read_text("utf-8")) if SORTEOS.exists() else {}
def guardar(datos: dict) -> None:
SORTEOS.write_text(json.dumps(datos, ensure_ascii=False, indent=2), "utf-8")
class Participar(discord.ui.View):
def __init__(self):
super().__init__(timeout=None) # sin caducidad: el botón sigue valiendo tras reiniciar
@discord.ui.button(label="Participar", emoji="🎉", style=discord.ButtonStyle.success, custom_id="sorteo:participar")
async def participar(self, interaction: discord.Interaction, boton: discord.ui.Button):
datos = leer()
sorteo = datos.get(str(interaction.message.id))
if sorteo is None or sorteo["terminado"]:
await interaction.response.send_message("Este sorteo ya ha terminado.", ephemeral=True)
return
apuntados = sorteo["participantes"]
if interaction.user.id in apuntados:
apuntados.remove(interaction.user.id)
texto = "Te has desapuntado. Si cambias de idea, vuelve a pulsar."
else:
apuntados.append(interaction.user.id)
texto = f"¡Estás dentro! Sois {len(apuntados)}. Para desapuntarte, pulsa otra vez."
guardar(datos)
await interaction.response.send_message(texto, ephemeral=True)
class Bot(discord.Client):
def __init__(self):
super().__init__(intents=discord.Intents.default())
self.tree = app_commands.CommandTree(self)
async def setup_hook(self):
self.add_view(Participar()) # vuelve a escuchar los botones ya publicados
await self.tree.sync() # publica /sorteo y /resortear
revisar.start()
bot = Bot()
async def canal_de(sorteo: dict):
return bot.get_channel(sorteo["canal"]) or await bot.fetch_channel(sorteo["canal"])
async def anunciar(id_mensaje: str, sorteo: dict, ganadores: list[int]) -> None:
"""Menciona a los ganadores respondiendo a la tarjeta del sorteo."""
canal = await canal_de(sorteo)
if ganadores:
menciones = ", ".join(f"<@{g}>" for g in ganadores)
texto = f"🎉 ¡Enhorabuena, {menciones}! Premio: **{sorteo['premio']}**."
else:
texto = f"Nadie se ha apuntado al sorteo de **{sorteo['premio']}**."
tarjeta = discord.MessageReference(message_id=int(id_mensaje), channel_id=canal.id, fail_if_not_exists=False)
await canal.send(texto, reference=tarjeta)
async def cerrar_tarjeta(id_mensaje: str, sorteo: dict) -> None:
"""Quita el botón y apunta cuántos participaron."""
try:
mensaje = await (await canal_de(sorteo)).fetch_message(int(id_mensaje))
tarjeta = mensaje.embeds[0]
tarjeta.color = 0x888888
tarjeta.set_footer(text=f"Terminado · {len(sorteo['participantes'])} participantes")
await mensaje.edit(embed=tarjeta, view=None)
except (discord.HTTPException, IndexError):
pass # han borrado la tarjeta: el resultado ya está anunciado
@tasks.loop(seconds=30)
async def revisar():
ahora = time.time()
for id_mensaje, sorteo in leer().items():
if sorteo["terminado"] or sorteo["fin"] > ahora:
continue
# Primero se cierra (nadie más se apunta) y luego se elige.
datos = leer()
datos[id_mensaje]["terminado"] = True
guardar(datos)
sorteo = datos[id_mensaje]
apuntados = sorteo["participantes"]
ganadores = azar.sample(apuntados, min(sorteo["ganadores"], len(apuntados)))
try:
await anunciar(id_mensaje, sorteo, ganadores)
await cerrar_tarjeta(id_mensaje, sorteo)
except discord.HTTPException as error:
print(f"No he podido anunciar el sorteo {id_mensaje}: {error!r}")
datos = leer() # otra vez: mientras tanto, otros se han podido apuntar a otros sorteos
datos[id_mensaje]["ganadores_ids"] = ganadores
guardar(datos)
@revisar.before_loop
async def antes_de_revisar():
await bot.wait_until_ready()
@bot.tree.command(name="sorteo", description="Empieza un sorteo en este canal")
@app_commands.guild_only()
@app_commands.default_permissions(manage_guild=True)
@app_commands.describe(premio="Qué se sortea", minutos="Cuánto dura: de 1 minuto a 28 días (40320)", ganadores="Cuántos ganan")
async def sorteo(interaction: discord.Interaction, premio: str,
minutos: app_commands.Range[int, 1, 40320], ganadores: app_commands.Range[int, 1, 20] = 1):
fin = int(time.time()) + minutos * 60
tarjeta = discord.Embed(
title=f"🎁 Sorteo: {premio}",
# <t:…:R> es una marca de tiempo de Discord: «dentro de 2 horas», en la hora de cada uno.
description=f"Pulsa **Participar** para apuntarte.\nTermina <t:{fin}:R> (<t:{fin}:f>).\nGanadores: {ganadores}",
color=0x5B4AE8,
)
tarjeta.set_footer(text=f"Lo organiza {interaction.user.display_name}")
await interaction.response.send_message(embed=tarjeta, view=Participar())
mensaje = await interaction.original_response()
datos = leer()
datos[str(mensaje.id)] = {
"canal": interaction.channel_id, "premio": premio, "ganadores": ganadores, "fin": fin,
"participantes": [], "terminado": False, "ganadores_ids": [],
}
guardar(datos)
@bot.tree.command(name="resortear", description="Elige otro ganador de un sorteo terminado")
@app_commands.guild_only()
@app_commands.default_permissions(manage_guild=True)
@app_commands.describe(id_mensaje="Clic derecho en la tarjeta del sorteo → «Copiar ID del mensaje»", cuantos="Cuántos ganadores nuevos")
async def resortear(interaction: discord.Interaction, id_mensaje: str, cuantos: app_commands.Range[int, 1, 20] = 1):
id_mensaje = id_mensaje.strip()
sorteo = leer().get(id_mensaje)
canal = bot.get_channel(sorteo["canal"]) if sorteo else None
# Que sea de este servidor: el id de otro servidor no vale aquí.
if sorteo is None or not sorteo["terminado"] or getattr(getattr(canal, "guild", None), "id", None) != interaction.guild_id:
await interaction.response.send_message("No encuentro un sorteo terminado con ese id en este servidor.", ephemeral=True)
return
# Sin repetir: quien ya ganó no entra otra vez.
quedan = [p for p in sorteo["participantes"] if p not in sorteo["ganadores_ids"]]
if not quedan:
await interaction.response.send_message("Ya no queda nadie a quien elegir.", ephemeral=True)
return
nuevos = azar.sample(quedan, min(cuantos, len(quedan)))
await interaction.response.send_message("Elegido. Lo anuncio en el canal del sorteo.", ephemeral=True)
await anunciar(id_mensaje, sorteo, nuevos)
datos = leer()
datos[id_mensaje]["ganadores_ids"] += nuevos
guardar(datos)
@bot.event
async def on_ready():
print(f"Conectado como {bot.user}")
bot.run(os.environ["DISCORD_TOKEN"])Por qué no se pierde nada al reiniciar
El botón no guarda nada: cuando alguien lo pulsa, Discord le pasa al bot el id del mensaje y el del botón (custom_id). Por eso la vista lleva timeout=None y un custom_id fijo, y setup_hook la registra con add_view al arrancar: la tarjeta de un sorteo de una semana sigue funcionando aunque el bot se reinicie a mitad.
Los apuntados, la hora de fin y los ganadores van en sorteos.json, con el id de la tarjeta como clave. Si el bot estaba apagado cuando tocaba terminar un sorteo, lo termina en cuanto vuelve, en la primera vuelta de 30 segundos.
3. Pruébalo
Quién puede usar /sorteo y /resortear se cambia sin tocar el código en «Ajustes del servidor» → «Integraciones» → tu bot: por ejemplo, dejárselo también al rol de moderadores.
Arranca el bot
En una terminal, en la carpeta:
pip install -r requirements.txtypython app.py. Cuando diga «Conectado como…», ya está. Los comandos pueden tardar unos minutos en salir la primera vez.Haz un sorteo de un minuto
En un canal de prueba, /sorteo con premio «Prueba», 1 minuto y 1 ganador. Pulsa «Participar» con tu cuenta y con otra (o con un amigo). Al minuto, el bot responde a la tarjeta con el ganador y el botón desaparece.
Repite el sorteo
Activa el «Modo desarrollador» en tus ajustes de Discord → «Avanzado», haz clic derecho en la tarjeta del sorteo, «Copiar ID del mensaje», y escribe /resortear con ese id: elige a otro de los apuntados.
Si algo no va
«Esta interacción ha fallado» al pulsar «Participar»: el bot está apagado. En cuanto vuelva, el mismo botón funciona (no hace falta repetir el sorteo).
El sorteo termina pero no sale el ganador: al bot le falta «Send Messages» o «Read Message History» en ese canal; mira los permisos del canal y la consola, donde apunta «No he podido anunciar…». Si no salen los comandos, qué mirar cuando no aparecen; si el bot está en línea y no contesta, mi bot no responde.
La hora que enseña la tarjeta es la de cada uno: la escribe Discord a partir de una marca de tiempo. Si quieres poner una a mano en un anuncio, el generador de marcas de tiempo te la da lista.
4. Déjalo encendido siempre (gratis)
Un sorteo de varios días solo termina a su hora si el bot está encendido: si vive en tu ordenador y lo apagas, nadie puede apuntarse y el ganador sale cuando lo vuelvas a encender. Hay que alojarlo en un servidor.
En NapNodes: crea el bot gratis (nace en Node.js; para este, en «Configuración» → «Lenguaje», «Pasar a Python»), sube los tres ficheros en «Archivos» y pulsa «Encender». Las librerías de requirements.txt se instalan solas al arrancar y app.py es el fichero de inicio que viene puesto. sorteos.json aparece en «Archivos» junto a app.py.
El bot gratis tiene 256 MB, de sobra para este bot aunque se apunten miles, y se renueva con un clic cada semana: si se te pasa, se apaga hasta que lo enciendes. Para no tener que acordarte, el plan Mini (512 MB) está encendido siempre sin renovar nada (precios de los bots). Todo el montaje, en la guía de hosting para tu bot.
Ideas para ampliarlo
Que solo participe un rol (por ejemplo, los suscriptores o quien lleve un nivel): en participar(), mira si interaction.user tiene ese rol con discord.utils.get(interaction.user.roles, name="Suscriptor") y, si no, contéstale que no puede. Con el bot de niveles, los roles por nivel ya los tienes.
Un /terminar-sorteo para acabarlo antes: pon su fin en el pasado en sorteos.json y la siguiente vuelta lo cierra. O juntarlo con el bot de moderación y el de bienvenida en uno solo: los tres caben en el gratis.
Preguntas frecuentes
¿Cómo hago un sorteo en Discord?
Con un bot: publica el premio con un botón para apuntarse y, al acabar el tiempo, elige ganadores al azar entre los apuntados. El código completo en Python, con /sorteo y /resortear, está en esta guía.
¿Cómo elige el bot al ganador?
Al azar entre quienes han pulsado «Participar», con el módulo secrets de Python, que usa el azar del sistema. Nadie puede saber antes quién va a ganar.
¿Se puede repetir un sorteo si el ganador no contesta?
Sí: /resortear con el id de la tarjeta del sorteo elige a otro de los apuntados, sin repetir a quien ya ganó.
¿Qué pasa si el bot se reinicia durante un sorteo?
Nada: los apuntados y la hora de fin están en sorteos.json y el botón sigue funcionando. Si tocaba terminar mientras estaba apagado, lo termina al volver.
¿Dónde dejo el bot de sorteos encendido siempre?
En un servidor. En NapNodes hay un bot gratis de 256 MB que se renueva con un clic cada semana, o el plan Mini, encendido siempre.
¿Te ha servido esta guía?
¿Lo montamos?
Hosting de bots de Discord desde 1,50 € al mes, sin sobreventa: tu RAM es solo tuya. Y si quieres empezar sin pagar, tienes un bot de Discord gratis, sin tarjeta.
Herramienta gratis: Permisos de un bot de Discord. Marca los permisos de tu bot en castellano y sal con el número y el enlace de invitación; o pega un número y mira qué permisos da.
Herramienta gratis: Marcas de tiempo de Discord. Elige día, hora y zona y copia el <t:…> para que en Discord cada uno vea la hora en la suya, o cuánto falta.
Sigue leyendo
- Bot de verificación para Discord en Python: un botón y una pregunta antes de entrar
Haz tu bot de verificación de Discord con discord.py: botón «Verificarme», una pregunta contra bots y un rol que abre el servidor. Código completo y gratis.
- Bot de roles para Discord: botones para que cada uno elija sus roles (Python y JavaScript)
Haz tu bot de autoroles para Discord en Python o JavaScript: un mensaje con botones para ponerse y quitarse roles, sin base de datos y sin roles por reacción.
- Bot de moderación para Discord en Python: avisos, silenciar, expulsar, banear y borrar mensajes
Haz tu bot de moderación de Discord en Python: /aviso con historial, /silenciar, /expulsar, /banear, /borrar mensajes y un canal de registro. Código completo.