Bot de cumpleaños para Discord: felicita solo a cada miembro en su día (Python y JavaScript)
Felicitar a cada uno en su día es de lo que más une a una comunidad de Discord, y de lo primero que se olvida. Con este bot, cada miembro apunta su cumpleaños una vez (sin el año) y el bot le felicita por la mañana en el canal que elijas, aunque haya estado apagado a esa hora. Código entero en discord.py y en discord.js, sin base de datos y sin intents privilegiados.
Qué hace el bot
Cuatro comandos: /cumple para apuntar tu día y tu mes, /borrar-cumple para quitarlo, /cumples para ver los próximos 10 y /canal-cumples para elegir dónde se felicita (este, de serie, solo lo ve quien gestiona el servidor).
Cada día, a partir de las 9:00 de la hora de España, el bot escribe en ese canal «🎂 ¡Feliz cumpleaños, @Ana y @Luis! 🎉», con una mención a cada uno para que le llegue el aviso. Apunta qué día ha felicitado ya, así que un reinicio no repite la felicitación, y si estaba apagado a las 9 felicita en cuanto vuelve a encenderse ese mismo día.
No guarda el año: nadie tiene que decir su edad. Quien nació un 29 de febrero recibe la felicitación el 28 los años que no son bisiestos. Si alguien se ha ido del servidor, el bot no le nombra. Todo va en cumples.json, junto al bot, y un mismo bot lleva los cumpleaños de varios servidores a la vez.
¿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 e invítalo
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 o en JavaScript. Los «Privileged Gateway Intents» se quedan apagados.
Invítalo con dos permisos
En «OAuth2» → «URL Generator», marca «bot» y «applications.commands», y en los permisos «View Channels» y «Send Messages» (en total, el número 3072). No hace falta «Mention Everyone»: el bot solo menciona a personas, nunca a @everyone ni a un rol. Con la calculadora de permisos de bot los ves en castellano y te da el enlace.
2. El bot en Python (discord.py)
Tres ficheros en una carpeta. El token va en .env, sin comillas. Hace falta Python 3.10 o más nuevo. tzdata trae las zonas horarias por si el sistema donde corre el bot no las tiene. Para felicitar a otra hora, cambia HORA; para otro país, ZONA (por ejemplo, America/Mexico_City o Atlantic/Canary).
discord.py
python-dotenv
tzdataDISCORD_TOKEN=pega-aquí-tu-tokenimport json
import os
from datetime import date, datetime
from pathlib import Path
from zoneinfo import ZoneInfo
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 cumpleaños se guardan junto al bot: un reinicio no los pierde.
DATOS = Path("cumples.json")
ZONA = ZoneInfo("Europe/Madrid") # cámbiala si tu grupo vive en otra zona horaria
HORA = 9 # a partir de esta hora se felicita
MESES = ["enero", "febrero", "marzo", "abril", "mayo", "junio", "julio",
"agosto", "septiembre", "octubre", "noviembre", "diciembre"]
SIN_AVISOS = discord.AllowedMentions.none()
def leer() -> dict:
return json.loads(DATOS.read_text("utf-8")) if DATOS.exists() else {}
def guardar(datos: dict) -> None:
DATOS.write_text(json.dumps(datos, indent=2), "utf-8")
def servidor(datos: dict, guild_id: int) -> dict:
return datos.setdefault(str(guild_id), {"canal": None, "ultimo": None, "cumples": {}})
def proximo(dia: int, mes: int, hoy: date) -> date:
"""El próximo cumpleaños a partir de hoy (hoy incluido)."""
for anio in (hoy.year, hoy.year + 1):
try:
fecha = date(anio, mes, dia)
except ValueError:
fecha = date(anio, 2, 28) # nació un 29 de febrero y este año no es bisiesto
if fecha >= hoy:
return fecha
def juntar(nombres: list[str]) -> str:
return nombres[0] if len(nombres) == 1 else ", ".join(nombres[:-1]) + " y " + nombres[-1]
class Bot(discord.Client):
def __init__(self):
# Sin intents privilegiados: a cada persona se la busca por su ID cuando le toca.
super().__init__(intents=discord.Intents.default())
self.tree = app_commands.CommandTree(self)
async def setup_hook(self):
await self.tree.sync() # publica los comandos
felicitar.start()
bot = Bot()
@tasks.loop(minutes=30) # mira cada media hora: si el bot estaba apagado a las 9, felicita al volver
async def felicitar():
ahora = datetime.now(ZONA)
if ahora.hour < HORA:
return
hoy = ahora.date()
for guild_id, datos in leer().items():
if datos["ultimo"] == hoy.isoformat() or datos["canal"] is None:
continue # ya felicitado hoy, o sin canal
guild = bot.get_guild(int(guild_id))
canal = bot.get_channel(datos["canal"])
if guild is None or canal is None:
continue
quienes = []
for user_id, (dia, mes) in datos["cumples"].items():
if proximo(dia, mes, hoy) != hoy:
continue
try:
await guild.fetch_member(int(user_id)) # ¿sigue en el servidor?
except discord.HTTPException:
continue # se ha ido (o Discord no contesta): a esa persona no se la nombra
quienes.append(f"<@{user_id}>")
if quienes:
try:
await canal.send(f"🎂 ¡Feliz cumpleaños, {juntar(quienes)}! 🎉",
allowed_mentions=discord.AllowedMentions(users=True, roles=False, everyone=False))
except discord.HTTPException as error:
print(f"No he podido felicitar en {guild_id}: {error!r}")
continue # se vuelve a intentar en la siguiente vuelta
fresco = leer() # se relee por si alguien ha usado un comando mientras tanto
servidor(fresco, int(guild_id))["ultimo"] = hoy.isoformat()
guardar(fresco)
@felicitar.before_loop
async def antes_de_felicitar():
await bot.wait_until_ready()
@bot.tree.command(name="cumple", description="Guarda tu cumpleaños (sin el año)")
@app_commands.guild_only()
@app_commands.describe(dia="Día del mes", mes="Mes, del 1 al 12")
async def cumple(interaction: discord.Interaction, dia: app_commands.Range[int, 1, 31], mes: app_commands.Range[int, 1, 12]):
try:
date(2000, mes, dia) # 2000 fue bisiesto: el 29 de febrero vale
except ValueError:
await interaction.response.send_message("Esa fecha no existe.", ephemeral=True)
return
datos = leer()
s = servidor(datos, interaction.guild_id)
s["cumples"][str(interaction.user.id)] = [dia, mes]
guardar(datos)
falta = "" if s["canal"] else " Aún no hay canal para felicitar: alguien que gestione el servidor tiene que usar /canal-cumples."
await interaction.response.send_message(f"Apuntado: el {dia} de {MESES[mes - 1]}.{falta}", ephemeral=True)
@bot.tree.command(name="borrar-cumple", description="Borra tu cumpleaños de este servidor")
@app_commands.guild_only()
async def borrar_cumple(interaction: discord.Interaction):
datos = leer()
borrado = servidor(datos, interaction.guild_id)["cumples"].pop(str(interaction.user.id), None)
guardar(datos)
texto = "Borrado: ya no te felicitaré." if borrado else "No tenías ningún cumpleaños apuntado."
await interaction.response.send_message(texto, ephemeral=True)
@bot.tree.command(name="cumples", description="Los próximos cumpleaños del servidor")
@app_commands.guild_only()
async def cumples(interaction: discord.Interaction):
hoy = datetime.now(ZONA).date()
apuntados = servidor(leer(), interaction.guild_id)["cumples"]
if not apuntados:
await interaction.response.send_message("Nadie ha apuntado su cumpleaños todavía. Usa /cumple.", ephemeral=True)
return
orden = sorted(apuntados.items(), key=lambda p: proximo(p[1][0], p[1][1], hoy))[:10]
lineas = [f"{dia} de {MESES[mes - 1]}: <@{user_id}>" for user_id, (dia, mes) in orden]
# Se ve el nombre, pero no se avisa a nadie.
await interaction.response.send_message("🎂 Próximos cumpleaños:\n" + "\n".join(lineas), allowed_mentions=SIN_AVISOS)
@bot.tree.command(name="canal-cumples", description="Elige el canal donde se felicita")
@app_commands.guild_only()
@app_commands.default_permissions(manage_guild=True) # de serie, solo quien gestiona el servidor
async def canal_cumples(interaction: discord.Interaction, canal: discord.TextChannel):
if not canal.permissions_for(interaction.guild.me).send_messages:
await interaction.response.send_message(f"No puedo escribir en {canal.mention}: dame «Ver canal» y «Enviar mensajes» allí.", ephemeral=True)
return
datos = leer()
servidor(datos, interaction.guild_id)["canal"] = canal.id
guardar(datos)
await interaction.response.send_message(f"Hecho: felicitaré en {canal.mention} a partir de las {HORA}:00.", ephemeral=True)
@bot.event
async def on_ready():
print(f"Conectado como {bot.user}")
bot.run(os.environ["DISCORD_TOKEN"])El mismo bot en JavaScript (discord.js)
package.json, .env e index.js. Hace falta Node.js 20 o más nuevo. La fecha y la hora de la zona elegida salen de Intl.DateTimeFormat, que ya viene con Node: no hace falta ninguna librería de fechas.
{
"name": "bot-cumpleanos",
"version": "1.0.0",
"main": "index.js",
"dependencies": {
"discord.js": "^14.16.0",
"dotenv": "^16.4.0"
}
}DISCORD_TOKEN=pega-aquí-tu-tokenrequire("dotenv").config(); // lee el token del fichero .env
const fs = require("node:fs");
const path = require("node:path");
const { ChannelType, Client, Events, GatewayIntentBits, InteractionContextType, MessageFlags, PermissionFlagsBits, SlashCommandBuilder } = require("discord.js");
// Los cumpleaños se guardan junto al bot: un reinicio no los pierde.
const DATOS = path.join(__dirname, "cumples.json");
const ZONA = "Europe/Madrid"; // cámbiala si tu grupo vive en otra zona horaria
const HORA = 9; // a partir de esta hora se felicita
const MEDIA_HORA = 30 * 60 * 1000; // mira cada media hora: si el bot estaba apagado a las 9, felicita al volver
const MESES = ["enero", "febrero", "marzo", "abril", "mayo", "junio", "julio", "agosto", "septiembre", "octubre", "noviembre", "diciembre"];
const leer = () => (fs.existsSync(DATOS) ? JSON.parse(fs.readFileSync(DATOS, "utf8")) : {});
const guardar = (datos) => fs.writeFileSync(DATOS, JSON.stringify(datos, null, 2));
const servidor = (datos, guildId) => (datos[guildId] ??= { canal: null, ultimo: null, cumples: {} });
const dos = (n) => String(n).padStart(2, "0");
const juntar = (nombres) => (nombres.length === 1 ? nombres[0] : `${nombres.slice(0, -1).join(", ")} y ${nombres.at(-1)}`);
// La fecha («AAAA-MM-DD») y la hora de ahora en la zona elegida.
function ahora() {
const partes = new Intl.DateTimeFormat("en-CA", { timeZone: ZONA, year: "numeric", month: "2-digit", day: "2-digit", hour: "2-digit", hourCycle: "h23" }).formatToParts(new Date());
const p = Object.fromEntries(partes.map((x) => [x.type, x.value]));
return { hoy: `${p.year}-${p.month}-${p.day}`, hora: Number(p.hour) };
}
// El próximo cumpleaños a partir de hoy (hoy incluido), como «AAAA-MM-DD».
function proximo(dia, mes, hoy) {
const anio = Number(hoy.slice(0, 4));
for (const a of [anio, anio + 1]) {
const bisiesto = new Date(Date.UTC(a, 1, 29)).getUTCDate() === 29;
const d = dia === 29 && mes === 2 && !bisiesto ? 28 : dia; // nació un 29 de febrero y este año no es bisiesto
const fecha = `${a}-${dos(mes)}-${dos(d)}`;
if (fecha >= hoy) return fecha;
}
}
// Sin intents privilegiados: a cada persona se la busca por su ID cuando le toca.
const client = new Client({ intents: [GatewayIntentBits.Guilds] });
async function felicitar() {
const { hoy, hora } = ahora();
if (hora < HORA) return;
for (const [guildId, datos] of Object.entries(leer())) {
if (datos.ultimo === hoy || !datos.canal) continue; // ya felicitado hoy, o sin canal
const guild = client.guilds.cache.get(guildId);
const canal = client.channels.cache.get(datos.canal);
if (!guild || !canal) continue;
const quienes = [];
for (const [userId, [dia, mes]] of Object.entries(datos.cumples)) {
if (proximo(dia, mes, hoy) !== hoy) continue;
const miembro = await guild.members.fetch(userId).catch(() => null); // ¿sigue en el servidor?
if (miembro) quienes.push(`<@${userId}>`);
}
if (quienes.length) {
try {
await canal.send({ content: `🎂 ¡Feliz cumpleaños, ${juntar(quienes)}! 🎉`, allowedMentions: { parse: ["users"] } });
} catch (error) {
console.error(`No he podido felicitar en ${guildId}:`, error);
continue; // se vuelve a intentar en la siguiente vuelta
}
}
const fresco = leer(); // se relee por si alguien ha usado un comando mientras tanto
servidor(fresco, guildId).ultimo = hoy;
guardar(fresco);
}
}
const comandos = [
new SlashCommandBuilder().setName("cumple").setDescription("Guarda tu cumpleaños (sin el año)").setContexts(InteractionContextType.Guild)
.addIntegerOption((o) => o.setName("dia").setDescription("Día del mes").setRequired(true).setMinValue(1).setMaxValue(31))
.addIntegerOption((o) => o.setName("mes").setDescription("Mes, del 1 al 12").setRequired(true).setMinValue(1).setMaxValue(12)),
new SlashCommandBuilder().setName("borrar-cumple").setDescription("Borra tu cumpleaños de este servidor").setContexts(InteractionContextType.Guild),
new SlashCommandBuilder().setName("cumples").setDescription("Los próximos cumpleaños del servidor").setContexts(InteractionContextType.Guild),
new SlashCommandBuilder().setName("canal-cumples").setDescription("Elige el canal donde se felicita").setContexts(InteractionContextType.Guild)
.setDefaultMemberPermissions(PermissionFlagsBits.ManageGuild) // de serie, solo quien gestiona el servidor
.addChannelOption((o) => o.setName("canal").setDescription("Canal de texto").setRequired(true).addChannelTypes(ChannelType.GuildText)),
];
client.on(Events.InteractionCreate, async (i) => {
if (!i.isChatInputCommand()) return;
const privado = { flags: MessageFlags.Ephemeral };
const datos = leer();
const s = servidor(datos, i.guildId);
if (i.commandName === "cumple") {
const dia = i.options.getInteger("dia");
const mes = i.options.getInteger("mes");
if (new Date(Date.UTC(2000, mes - 1, dia)).getUTCDate() !== dia) { // 2000 fue bisiesto: el 29 de febrero vale
await i.reply({ content: "Esa fecha no existe.", ...privado });
return;
}
s.cumples[i.user.id] = [dia, mes];
guardar(datos);
const falta = s.canal ? "" : " Aún no hay canal para felicitar: alguien que gestione el servidor tiene que usar /canal-cumples.";
await i.reply({ content: `Apuntado: el ${dia} de ${MESES[mes - 1]}.${falta}`, ...privado });
} else if (i.commandName === "borrar-cumple") {
const habia = i.user.id in s.cumples;
delete s.cumples[i.user.id];
guardar(datos);
await i.reply({ content: habia ? "Borrado: ya no te felicitaré." : "No tenías ningún cumpleaños apuntado.", ...privado });
} else if (i.commandName === "cumples") {
const { hoy } = ahora();
const orden = Object.entries(s.cumples).sort((a, b) => proximo(...a[1], hoy).localeCompare(proximo(...b[1], hoy))).slice(0, 10);
if (!orden.length) {
await i.reply({ content: "Nadie ha apuntado su cumpleaños todavía. Usa /cumple.", ...privado });
return;
}
const lineas = orden.map(([userId, [dia, mes]]) => `${dia} de ${MESES[mes - 1]}: <@${userId}>`);
// Se ve el nombre, pero no se avisa a nadie.
await i.reply({ content: `🎂 Próximos cumpleaños:\n${lineas.join("\n")}`, allowedMentions: { parse: [] } });
} else if (i.commandName === "canal-cumples") {
const canal = i.options.getChannel("canal");
if (!canal.permissionsFor(i.guild.members.me).has(PermissionFlagsBits.SendMessages)) {
await i.reply({ content: `No puedo escribir en ${canal}: dame «Ver canal» y «Enviar mensajes» allí.`, ...privado });
return;
}
s.canal = canal.id;
guardar(datos);
await i.reply({ content: `Hecho: felicitaré en ${canal} a partir de las ${HORA}:00.`, ...privado });
}
});
client.once(Events.ClientReady, async (c) => {
await c.application.commands.set(comandos.map((x) => x.toJSON())); // publica los comandos
console.log(`Conectado como ${c.user.tag}`);
setInterval(() => felicitar().catch(console.error), MEDIA_HORA);
felicitar().catch(console.error);
});
client.login(process.env.DISCORD_TOKEN);3. Pruébalo
Para volver a probar el mismo día, apaga el bot, borra la línea "ultimo" de ese servidor en cumples.json y enciéndelo. Quién ve /canal-cumples se cambia sin tocar el código en «Ajustes del servidor» → «Integraciones» → tu bot.
Arranca el bot
En una terminal, en la carpeta:
pip install -r requirements.txtypython app.py(onpm installynode index.js). Cuando diga «Conectado como…», ya está. Los comandos pueden tardar unos minutos en salir la primera vez.Elige el canal
Escribe
/canal-cumplesy elige el canal de texto donde quieres las felicitaciones. Si el bot no puede escribir allí, te lo dice en el momento.Apunta un cumpleaños de hoy
Escribe
/cumplecon el día y el mes de hoy. Si ya son más de las 9, como mucho en media hora aparece la felicitación en el canal. Luego,/cumplesenseña la lista sin avisar a nadie.
La hora, la zona horaria y el 29 de febrero
El bot no usa la hora del ordenador donde corre, sino la de ZONA. Así da igual que esté en tu casa o en un servidor de otro país: felicita a las 9 de España (o de donde pongas), y el cambio de hora de marzo y de octubre lo lleva solo.
No felicita a las 9 en punto, sino en la primera vuelta a partir de las 9: mira cada 30 minutos y apunta en cumples.json el día que ya ha felicitado. Por eso no repite aunque se reinicie, y no se salta el día si estaba apagado a esa hora.
Un 29 de febrero solo existe los años bisiestos. Los demás años, quien nació ese día recibe la felicitación el 28, y en /cumples sale con esa fecha.
Si algo no va
No felicita a nadie: mira que alguien haya usado /canal-cumples (al apuntar un cumpleaños sin canal, el bot lo avisa) y que el bot siga pudiendo escribir en ese canal. Si le han quitado el permiso, la consola dice «Missing Permissions» (el código 50013) y lo vuelve a intentar en la siguiente vuelta.
Felicita a la hora que no es: cambia ZONA a la de tu grupo. Los nombres válidos son los de la base de zonas horarias, como Europe/Madrid, Atlantic/Canary o America/Argentina/Buenos_Aires. En Python, si dice «No time zone found», falta tzdata en requirements.txt.
No salen los comandos: qué mirar cuando los comandos no aparecen. Si el bot está en línea y no contesta, mi bot no responde.
Ideas para ampliarlo
Un rol de «🎂 Cumpleañero» para ese día: el bot necesita además «Manage Roles», su rol tiene que estar por encima del que da, y al día siguiente hay que quitarlo (guarda en cumples.json a quién se lo diste). Cómo dar y quitar roles desde el código, en el bot de roles.
Una felicitación más vistosa, con color e imagen: cambia el texto por un embed (embeds en un bot de Discord). Y si ya tienes un bot de bienvenida, puedes juntar los dos en el mismo: en el mismo plan gastan lo mismo que uno.
4. Déjalo encendido siempre
Un bot de cumpleaños tiene que estar encendido cada mañana: si vive en tu ordenador y está apagado ese día, no felicita a nadie. Hay que alojarlo en un servidor; opciones gratis y de pago, comparadas, en dónde alojar tu bot de Discord.
En NapNodes hay un bot gratis de 256 MB que no se apaga por estar vacío; se renueva cada 7 días con «Encender» (sus pegas, en esta guía): si se te olvida renovarlo, esos días no hay felicitaciones. Créalo, elige Node.js o Python en «Configuración» → «Lenguaje», sube tus ficheros en «Archivos» y pulsa «Encender»: las librerías de requirements.txt o package.json se instalan solas al arrancar, y cumples.json se queda en la carpeta del bot entre reinicios. Para no tener que renovar, el plan Mini (512 MB) está encendido siempre (precios de los bots).
Preguntas frecuentes
¿Cómo hago que un bot de Discord felicite los cumpleaños?
Con un bot que guarde el día y el mes de cada miembro y mire cada media hora si a alguien le toca, para felicitarle en un canal. El código completo en Python y en JavaScript está en esta guía.
¿El bot de cumpleaños necesita intents privilegiados?
No. Cada uno apunta su fecha con un comando, y el día de su cumpleaños el bot comprueba que sigue en el servidor buscándolo por su ID, sin repasar la lista de miembros.
¿Qué pasa con quien nació un 29 de febrero?
Los años bisiestos se le felicita el 29; los demás, el 28 de febrero.
¿Qué permisos necesita el bot de cumpleaños?
Ver canales y enviar mensajes (el número de permisos es 3072). No necesita mencionar a todos: solo menciona a quien cumple años.
¿Dónde dejo el bot de cumpleaños 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.
Sigue leyendo
- Bot contador de miembros para Discord: canales con miembros, conectados y mejoras (Python y JavaScript)
Haz tu bot contador de miembros de Discord: canales de voz con miembros, conectados y mejoras que se ponen al día solos. Código en discord.py y discord.js.
- Bot de sorteos para Discord en Python: botón para participar, ganadores al azar y repetir
Haz tu bot de sorteos de Discord en Python: /sorteo con botón para participar, cuenta atrás, ganadores al azar y /resortear. Código completo, sin anuncios.
- Bot de bienvenida para Discord: mensaje al entrar y rol automático (Python y JavaScript)
Haz un bot de Discord que salude a quien entra y le dé un rol solo, en Python o JavaScript: el permiso que siempre falta, la jerarquía de roles y las normas.