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.
- Encuestas nativas — el bot muestra la interfaz de encuestas que trae Discord de serie (¡la opción más sencilla!)
- 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ó! 🙌

📌 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!
pip install -U "discord.py>=2.5" python-dotenvPuedes comprobar la versión instalada así.
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.
token=여기에_봇_토큰_붙여넣기💡 Consejo: ¡No olvides añadir una línea con
.enva 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.
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.
@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.
questionadmite hasta 300 caracteres, ydurationse indica con undatetime.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
emojitambié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.
@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é.
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í.
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.
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!

📌 Código completo (listo para copiar y pegar)
# 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).
/투표 질문:오늘 저녁 뭐 먹을까요? 항목:치킨,피자,족발 시간:2Aparece 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!

📌 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
Pollse añadió en la 2.5. - Solución: Ejecuta
pip install -U "discord.py>=2.5"y compruébalo conpython -c "import discord; print(discord.__version__)".
2. AttributeError: 'Intents' object has no attribute 'polls'
- Causa: Es el mismo problema de versión. El intent
pollstambié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
Viewsolo 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+ uncustom_idy 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
- Resumen de la serie - hoja de ruta para leer en orden desde el #1 🗺️
- #28 - Crear un sistema de niveles y experiencia (XP) → https://mojangcam.tistory.com/113
- #29 - Crear un sistema de economía (monedas y tienda) (próximamente)
- #30 - Crear un bot de encuestas y votaciones ← ¡Estás aquí!
- #31 - Guardar los resultados de la votación en una base de datos (próximamente)
Serie · Parte 30 de 32
Crear un bot de Discord
- #1Crear un bot de Discord - ¡Domina por completo los Slash Commands!
- #2¡Añade opciones (argumentos) a tus Slash Commands!
- #3¡Añade subcomandos (Subcommand) a tus Slash Commands!
- #4¡Envía embeds con Slash Commands!
- #5¡Implementa interacciones con botones (Button)!
- #6¡Implementa menús desplegables (Select Menu)!
- #7Pide texto al usuario con un modal (Modal) (ejemplo en Python)
- #8Decora embeds (Embed) con imágenes y campos (ejemplo en Python)
- #9¡Integra la IA (API de Claude y ChatGPT)! (ejemplo en Python)
- #10¡Añade soporte multilingüe (i18n)! (ejemplo en Python)
- #11¡Crea un panel web con Flask! (ejemplo en Python)
- #12¡Gestiona los cooldowns y permisos de tus comandos! (ejemplo en Python)
- #13¡Administra tu servidor automatizando roles y canales! (ejemplo en Python)
- #14¡Guarda tus datos! JSON vs SQLite (ejemplo en Python)
- #15슬래시 명령어 그룹(Group)으로 명령어 정리하기! (파이썬 예제) (en coreano)
- #16리마인더·알림 기능 만들기! 봇이 먼저 말 걸게 하기 (파이썬 예제) (en coreano)
- #17RAG로 문서 기반 답변봇 만들기! 우리 서버 전용 FAQ 봇 (파이썬 예제) (en coreano)
- #18투표(Poll) 기능 만들기! 다수결로 정하는 서버 규칙 (파이썬 예제) (en coreano)
- #19Cog로 코드 쪼개기! 1000줄 봇도 안 더러워지는 구조 (파이썬 예제) (en coreano)
- #20자동 환영 메시지·온보딩 만들기! 규칙 동의 버튼으로 자동 역할 부여 (파이썬 예제) (en coreano)
- #21키워드 자동응답·금지어 필터 만들기! 경고 누적으로 자동 제재까지 (파이썬 예제) (en coreano)
- #22슬래시 명령어 자동완성(Autocomplete) 만들기! 금지어를 골라서 추가·삭제 (파이썬 예제) (en coreano)
- #23aiosqlite로 비동기 DB 제대로 설계하기! JSON 탈출 (파이썬 예제) (en coreano)
- #24채널 난독화(Channel Obfuscation) 대응하기! 11/16부터 안 보이는 채널이 생겨요 (파이썬 예제) (en coreano)
- #25봇이 죽지 않게! 에러 핸들링·로깅 체계 잡기 (파이썬 예제) (en coreano)
- #26티켓(Ticket) 문의 시스템 만들기! 버튼 하나로 1:1 상담 채널 자동 생성 (파이썬 예제) (en coreano)
- #27티켓 담당자 배정·우선순위 만들기! 스태프가 여러 명이어도 안 꼬이게 (파이썬 예제) (en coreano)
- #28레벨·경험치(XP) 시스템 만들기! 채팅할수록 레벨업, 랭킹판까지 (파이썬 예제) (en coreano)
- #29화폐·상점(Economy) 시스템 만들기! 코인 벌어서 아이템 사기 (파이썬 예제) (en coreano)
- #30¡Crea un bot de encuestas y votaciones (Poll)! Encuestas nativas + botones con recuento en tiempo real (ejemplo en Python) 🗳️Estás aquí
- #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) 🕵️
- #33임시 음성방 확장, 파티방 만들기! 방장 위임·강퇴·초대 전용(비공개) 방 + 전용 채팅방 자동 생성 (파이썬 예제) 🎉 (en coreano)
