Categoría: Bots de Discord

Crear un bot de Discord#30¡Crea un bot de encuestas y votaciones (Poll)! Encuestas nativas + botones con recuento en tiempo real (ejemplo en Python) 🗳️

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

Índice10 secciones

¡Hola! Soy Mojangcam 😎

En el episodio anterior creamos un sistema de economía (monedas y tienda) para que los miembros ganen monedas y compren objetos. ¡Tu servidor acaba de ganar algo nuevo con lo que entretenerse!

Esta vez vamos a crear una función que se usa muchísimo al administrar un servidor: un bot de encuestas y votaciones (Poll). Veremos las dos formas de hacerlo.

  1. Encuestas nativas — el bot muestra la interfaz de encuestas que trae Discord de serie (¡la opción más sencilla!)
  2. Encuestas personalizadas con botones — las construyes tú con botones y controlas a tu gusto el recuento en tiempo real, el bloqueo de votos duplicados y el cierre de la votación

Elegir la fecha de un evento del servidor, decidir el próximo contenido, preguntar si se aprueba un cambio en las reglas... Si hasta ahora ponías reacciones con emojis y las contabas a mano cada vez, ¡con este post se acabó! 🙌

![](./img-01.png)

📌 Qué necesitas

  • Python 3.8+ (se recomienda 3.10 o superior)
  • discord.py 2.x — las Poll nativas requieren 2.5 o superior
  • python-dotenv — ¡imprescindible para no escribir el token del bot directamente en el código!
Terminal
pip install -U "discord.py>=2.5" python-dotenv

Puedes comprobar la versión instalada así.

Terminal
python -c "import discord; print(discord.__version__)"

Y nunca escribas el token del bot directamente en el código. Crea un archivo .env en la carpeta de tu proyecto.

Texto
token=여기에_봇_토큰_붙여넣기

💡 Consejo: ¡No olvides añadir una línea con .env a tu .gitignore! Si tu token acaba subido a GitHub, Discord lo invalida automáticamente. (Menos mal, la verdad 😅)

🧩 Configurar los Intents

Para recibir los eventos de voto de las encuestas nativas necesitas el intent polls. Si solo vas a usar el método de botones, no hace falta.

Python
intents = discord.Intents.default()
intents.message_content = True
intents.polls = True  # necesario para recibir eventos de encuesta (on_poll_vote_add, etc.)

📌 Paso 1 - Mostrar una encuesta nativa (el camino más fácil)

Desde discord.py 2.5 existe la clase discord.Poll, así que tu bot puede mostrar tal cual la interfaz de encuestas integrada en la app de Discord.

Python
@bot.tree.command(name="투표", description="Muestra una encuesta integrada de Discord")
async def native_poll(interaction: discord.Interaction, 질문: str, 항목: str):
    # Las opciones llegan separadas por comas: "Pollo,Pizza,Jokbal"
    choices = [c.strip() for c in 항목.split(",") if c.strip()][:10]

    if len(choices) < 2:
        await interaction.response.send_message("¡Pon al menos 2 opciones!", ephemeral=True)
        return

    poll = discord.Poll(question=질문, duration=datetime.timedelta(hours=24))
    for c in choices:
        poll.add_answer(text=c)

    await interaction.response.send_message(poll=poll)

Vamos a verlo por partes 💡

  • discord.Poll(question, duration) : Crea el objeto de la encuesta. question admite hasta 300 caracteres, y duration se indica con un datetime.timedelta, hasta 32 días (768 horas).
  • poll.add_answer(text=..., emoji=...) : Añade una opción. Solo se permiten hasta 10. Con el argumento emoji también puedes ponerle un emoji.
  • send_message(poll=poll) : ¡Basta con pasarla al mensaje como poll=! El recuento, el cierre y el bloqueo de duplicados los gestiona Discord por ti.
  • Ojo: tu bot necesita el permiso Crear encuestas (Send Polls).

Si quieres detectar el momento en que se añade o se quita un voto, usa eventos.

Python
@bot.event
async def on_poll_vote_add(user, answer):
    print(f"{user} votó por '{answer.text}'")

💬 Este evento solo se dispara cuando el mensaje está en la caché del bot. Para captar también mensajes antiguos, usa on_raw_poll_vote_add.

Las encuestas nativas son muy cómodas, pero tienen límites: máximo 10 opciones, no se pueden restringir por rol, no puedes mostrar los resultados a tu manera en tu propio embed y no puedes procesar el recuento mientras la votación está en curso. ¡Por eso pasamos al paso 2!


📌 Paso 2 - Encuesta personalizada con botones (recuento en tiempo real)

La estructura es sencilla: un botón por cada opción y un diccionario que guarda quién pulsó qué.

Python
class PollView(discord.ui.View):
    def __init__(self, question: str, choices: list[str], author_id: int, timeout: float = 3600):
        super().__init__(timeout=timeout)
        self.question = question
        self.choices = choices
        self.author_id = author_id
        self.votes: dict[int, int] = {}  # {ID de usuario: índice de la opción}

        for idx, label in enumerate(choices):
            self.add_item(VoteButton(label=label, index=idx))

Vamos a verlo por partes 💡

  • self.votes : Es la pieza clave. La clave es el ID del usuario, así que si la misma persona vuelve a pulsar, el valor simplemente se sobrescribe. Es decir, el voto doble queda bloqueado por diseño. ¿Cambiaste de opinión? ¡Pulsa otro botón y listo!
  • timeout : Cuando pasa este tiempo (en segundos), los botones dejan de funcionar automáticamente. 3600 = 1 hora.
  • author_id : Se guarda para que solo quien abrió la encuesta pueda pulsar "cerrar antes de tiempo".

Así es el botón en sí.

Python
class VoteButton(discord.ui.Button):
    def __init__(self, label: str, index: int):
        super().__init__(label=label, style=discord.ButtonStyle.secondary)
        self.index = index

    async def callback(self, interaction: discord.Interaction):
        view: PollView = self.view
        view.votes[interaction.user.id] = self.index
        await interaction.response.edit_message(embed=view.build_embed())
  • callback : Se ejecuta en el momento en que alguien pulsa el botón. Registra el voto y vuelve a dibujar el mensaje al instante. ¡Ese es el secreto del "recuento en tiempo real"!
  • edit_message : En lugar de enviar un mensaje nuevo, reemplaza el mensaje existente. Así el chat no se llena de basura 👍

📌 Paso 3 - Dibujar los resultados como gráfico de barras

Mostrar solo números es un poco aburrido, así que vamos a hacer un gráfico de barras con caracteres de bloque.

Python
def build_embed(self) -> discord.Embed:
    counts = [0] * len(self.choices)
    for choice_idx in self.votes.values():
        counts[choice_idx] += 1
    total = sum(counts)

    lines = []
    for label, cnt in zip(self.choices, counts):
        ratio = (cnt / total) if total else 0
        bar = "█" * round(ratio * 12) or "░"
        lines.append(f"`{bar:<12}` **{cnt} votos** ({ratio*100:.0f}%) · {label}")

    embed = discord.Embed(title=f"🗳️ {self.question}", description="\n".join(lines))
    embed.set_footer(text=f"{total} votos en total · Pulsa un botón para votar (pulsa otro para cambiar tu voto)")
    return embed
  • bar:<12 : Al envolverlo en comillas invertidas y fijar el ancho en 12 casillas, las barras quedan alineadas en lugar de irregulares.
  • or "░" : Añade un carácter de casilla vacía para que una fila con 0 votos no quede en blanco.
  • El total de votos es igual a len(self.votes). ¡Una persona, un voto!

![](./img-02.png)


📌 Código completo (listo para copiar y pegar)

Python
# discord.py 2.5+ / Python 3.10+
# pip install -U "discord.py>=2.5" python-dotenv
import os
import datetime

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

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

intents = discord.Intents.default()
intents.message_content = True
intents.polls = True

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

class VoteButton(discord.ui.Button):
    def __init__(self, label: str, index: int):
        super().__init__(label=label, style=discord.ButtonStyle.secondary)
        self.index = index

    async def callback(self, interaction: discord.Interaction):
        view: "PollView" = self.view
        if view.closed:
            await interaction.response.send_message("¡Esta encuesta ya está cerrada!", ephemeral=True)
            return
        view.votes[interaction.user.id] = self.index
        await interaction.response.edit_message(embed=view.build_embed(), view=view)

class CloseButton(discord.ui.Button):
    def __init__(self):
        super().__init__(label="Cerrar encuesta", style=discord.ButtonStyle.danger, row=4)

    async def callback(self, interaction: discord.Interaction):
        view: "PollView" = self.view
        if interaction.user.id != view.author_id:
            await interaction.response.send_message("¡Solo quien abrió la encuesta puede cerrarla!", ephemeral=True)
            return
        view.closed = True
        for item in view.children:
            item.disabled = True
        await interaction.response.edit_message(embed=view.build_embed(), view=view)
        view.stop()

class PollView(discord.ui.View):
    def __init__(self, question: str, choices: list[str], author_id: int, timeout: float = 3600):
        super().__init__(timeout=timeout)
        self.question = question
        self.choices = choices
        self.author_id = author_id
        self.votes: dict[int, int] = {}
        self.closed = False

        for idx, label in enumerate(choices):
            self.add_item(VoteButton(label=label, index=idx))
        self.add_item(CloseButton())

    def build_embed(self) -> discord.Embed:
        counts = [0] * len(self.choices)
        for choice_idx in self.votes.values():
            counts[choice_idx] += 1
        total = sum(counts)

        lines = []
        for label, cnt in zip(self.choices, counts):
            ratio = (cnt / total) if total else 0
            bar = "█" * round(ratio * 12) or "░"
            lines.append(f"`{bar:<12}` **{cnt} votos** ({ratio * 100:.0f}%) · {label}")

        title = ("🔒 [마감] " if self.closed else "🗳️ ") + self.question
        embed = discord.Embed(title=title, description="\n".join(lines), color=0x5865F2)
        footer = f"{total} votos en total"
        footer += " · Resultado final" if self.closed else " · Pulsa un botón para votar (pulsa otro para cambiar tu voto)"
        embed.set_footer(text=footer)
        return embed

    async def on_timeout(self):
        self.closed = True
        for item in self.children:
            item.disabled = True

@bot.event
async def on_ready():
    await bot.tree.sync()
    print(f"¡{bot.user} está listo!")

@bot.tree.command(name="투표", description="Inicia una encuesta con botones (separa las opciones con comas)")
async def poll_command(interaction: discord.Interaction, 질문: str, 항목: str, 시간: int = 1):
    choices = [c.strip() for c in 항목.split(",") if c.strip()][:5]
    if len(choices) < 2:
        await interaction.response.send_message("¡Pon al menos 2 opciones! Ej.: Pollo,Pizza,Jokbal", ephemeral=True)
        return

    view = PollView(질문, choices, interaction.user.id, timeout=시간 * 3600)
    await interaction.response.send_message(embed=view.build_embed(), view=view)

@bot.tree.command(name="기본투표", description="Muestra una encuesta nativa de Discord")
async def native_poll(interaction: discord.Interaction, 질문: str, 항목: str, 시간: int = 24):
    choices = [c.strip() for c in 항목.split(",") if c.strip()][:10]
    if len(choices) < 2:
        await interaction.response.send_message("¡Pon al menos 2 opciones!", ephemeral=True)
        return

    poll = discord.Poll(question=질문, duration=datetime.timedelta(hours=시간))
    for c in choices:
        poll.add_answer(text=c)

    await interaction.response.send_message(poll=poll)

@bot.event
async def on_poll_vote_add(user, answer):
    print(f"[Voto] {user} → {answer.text}")

bot.run(token)

📌 Resultado

Enciende el bot y pruébalo en tu servidor así (la pregunta es "¿Qué cenamos hoy?" y las opciones son pollo, pizza y jokbal).

Texto
/투표 질문:오늘 저녁 뭐 먹을까요? 항목:치킨,피자,족발 시간:2

Aparece un embed con 3 botones más un botón "Cerrar encuesta". En cuanto alguien pulsa un botón, el gráfico de barras se actualiza al instante. Si la misma persona pulsa otro botón, su voto se mueve, y si vuelve a pulsar el mismo botón, el voto no se suma otra vez.

Cuando quien abrió la encuesta pulsa "Cerrar encuesta", todos los botones se desactivan y el título cambia a 🔒 [마감] (Cerrada). Aunque nadie lo pulse, cuando pasa el 시간 (la duración, 1 hora por defecto), on_timeout la bloquea por su cuenta.

Prueba la encuesta nativa con /기본투표. ¡Aparece tal cual la interfaz de encuestas de la app de Discord que ya conoces!

![](./img-03.png)


📌 Errores frecuentes

1. AttributeError: module 'discord' has no attribute 'Poll'

  • Causa: Tu versión de discord.py es anterior a la 2.5. La clase Poll se añadió en la 2.5.
  • Solución: Ejecuta pip install -U "discord.py>=2.5" y compruébalo con python -c "import discord; print(discord.__version__)".

2. AttributeError: 'Intents' object has no attribute 'polls'

  • Causa: Es el mismo problema de versión. El intent polls también llegó en la 2.5.
  • Solución: Actualiza, o si solo vas a usar el método de botones, borra la línea intents.polls = True.

3. Pulso un botón de la encuesta y sale "Esta interacción ha fallado"

  • Causa: Reiniciaste el bot. Un objeto View solo funciona mientras sigue vivo en memoria.
  • Solución: Para encuestas cortas, simplemente abre una nueva. Si quieres una encuesta que sobreviva aunque el bot se apague, tienes que crear una vista persistente (persistent view) con timeout=None + un custom_id y guardar los votos en una base de datos (SQLite). ¡Si los votos solo viven en un diccionario en memoria, desaparecen todos al reiniciar!

4. Missing Permissions — la encuesta nativa no se publica

  • Causa: El rol del bot no tiene el permiso Crear encuestas (Send Polls).
  • Solución: Ve a Ajustes del servidor → Roles → el rol de tu bot y activa ese permiso. Revisa también las sobrescrituras de permisos de cada canal.

5. Los comandos de barra no aparecen en el servidor

  • Causa: La sincronización global (bot.tree.sync()) puede tardar hasta 1 hora en aplicarse.
  • Solución: Mientras pruebas, sincroniza solo con un servidor concreto usando await bot.tree.sync(guild=discord.Object(id=서버ID)) y se aplica al instante.

📌 Para terminar

Hoy creamos un bot de encuestas y votaciones de dos formas distintas 🙌

  • Encuestas nativas — listo con 5 líneas de código. Discord se encarga del recuento y del cierre, así que es lo más cómodo
  • Encuestas personalizadas con botones — gráfico de barras en tiempo real, bloqueo de duplicados y cierre anticipado, todo bajo tu control

"Quiero preguntar algo rápido" → nativa. "Quiero mostrar los resultados bonitos y poner condiciones" → botones. ¡Con recordar eso te basta!

En el próximo episodio veremos cómo guardar los resultados de la votación en una base de datos para que no se pierdan al reiniciar el bot. Será la solución de fondo para el error 3 de arriba. ¡No te lo pierdas! 😎

📚 Serie Crear un bot de Discord

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