NapNodes

← Guías de Bots de Discord

Bots de Discord · actualizada el 24 de octubre de 2026

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.

¿Se lo pasas a tu grupo?WhatsApp

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

  1. 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.

  2. 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.

requirements.txt
discord.py
python-dotenv
.env
DISCORD_TOKEN=pega-aquí-tu-token
app.py
import 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.

  1. Arranca el bot

    En una terminal, en la carpeta: pip install -r requirements.txt y python app.py. Cuando diga «Conectado como…», ya está. Los comandos pueden tardar unos minutos en salir la primera vez.

  2. 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.

  3. 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

Todas las guías de Bots de Discord, ordenadas →