He creado un bot de Telegram

14 de octubre de 2025

Y voy a intentar explicarte como lo hice

Cubrir una necesidad

Si, yo también, te entiendo. Yo también soy de esos que llegan a un gimnasio y no saben por donde empezar, que pasado un tiempo se estancan haciendo los mismos ejercicios o que directamente no lo pisa debido a la pereza que le da el hecho de empezar. He estado en esos sitios antes.

He aquí que un día, haciendo scroll en una red social de cuyo nombre preferiría no acordarme, llegué a un anuncio de un servicio que consistía en que un entrenador personal se encargaría de enviarte cada día un entrenamiento de Kettlebell (también llamada pesa Rusa) a tu Telegram, con la idea de que serían entrenamientos de no más de media hora y que como no tendrías que buscarlos, sino que te llegarían al teléfono, tendrías más facilidad de superar las ganas de sofá. También te preguntaba que tal ibas y te animaba.

He de reconocer que la idea me llamó la atención, dado que cada vez más, he estado interesándome por este tipo de entrenamientos donde prima la constancia por encima del conocimiento. Ni corto ni perezoso, pinché el enlace dispuesto a formar parte de dicho grupo, hasta que llegué al punto de pasar por caja: 10 euros al mes.

Dándole vueltas a la cabeza, me di cuenta que en realidad, yo no necesitaba nadie que animara, y teniendo en cuenta que Internet está plagado de rutinas de este tipo, ¿porque no hacer yo algo al respecto como programador? Me parecía un ejercicio interesante de desarrollo de una aplicación desde los inicios hasta el despliegue en producción, fuera de mi zona de confort y de mi entorno laboral. Por primera vez, me plantee terminar un “pet project” y lanzarlo al mundo.

Así surgió la idea de Kettlebot, un bot de Telegram con varias funciones, siendo la principal, el enviarte un entrenamiento aleatorio en Telegram, igual que lo haría el humano, pero a coste cero. Y en el horario que tu elijas.

Anatomía de un bot de Telegram

El gran Kettlebot

No voy a entrar en este artículo en como crear el bot en Telegram, ni como conseguir la Token de tu bot, dado que existen muy buenos tutoriales en otras plataformas a tal efecto, me voy a centrar en la parte de programación, que es de lo que va este blog.

La estructura del proyecto es la siguiente:

kettlebot/
├── kettlebot.py          # Punto de entrada principal
├── config.py             # Configuración (token de Telegram)
├── requirements.txt      # Dependencias del proyecto
├── bot/
│   ├── handlers.py       # Comandos del bot (/start, /wod, /programar, etc.)
│   └── utils.py          # Funciones auxiliares (cargar datos, selección aleatoria)
└── data/
    ├── workouts.json     # Base de datos de entrenamientos
    ├── motivation.json   # Colección de frases motivadoras
    └── generate_workouts.py  # Script para generar entrenamientos

La clase principal del proyecto es kettlebot.py, es lo que en otros lenguajes y/o proyectos consideraríamos nuestra clase Main. La nuestra tiene esta pinta:

from telegram.ext import Application, CommandHandler
from config import TELEGRAM_TOKEN
from bot.handlers import start, wod, programar, motivacion, ayuda, stop

def main():
    app = Application.builder().token(TELEGRAM_TOKEN).build()

    # Handlers
    app.add_handler(CommandHandler("start", start))
    app.add_handler(CommandHandler("wod", wod))
    app.add_handler(CommandHandler("programar", programar))
    app.add_handler(CommandHandler("motivacion", motivacion))
    app.add_handler(CommandHandler("stop", stop))
    app.add_handler(CommandHandler("ayuda", ayuda))

    print("🤖 Bot en marcha...")
    app.run_polling()

if __name__ == "__main__":
    main()

Lo que hacemos aquí es, básicamente, realizar los imports más importantes, declarar la aplicación y definir su Token de Telegram, sin la que la aplicación no sabría a que bot pertenece, y definimos sus Handlers. Tanto la declaración de la aplicación como los Handlers, se hace gracias a la librería de Python de Telegram, la cual es obligatoria para la construcción de este tipo de aplicaciones.

Si alguna vez has manejado un bot de Telegram, sabrás que la mayoría de las funciones empiezan por una barra inclinada seguida por un comando: esto es lo que se llama Handler. En la clase kettlebot, en nuestro caso y debido a nuestra organización del proyecto, tenemos el código de los Handlers en una clase a parte, pero la mayoría de las veces pondrás el código en la misma clase principal.

Ya que la hemos mencionado, vamos a poner dicha clase handlers.py:

import re
import datetime
import pytz
from telegram import Update
from telegram.ext import ContextTypes
from .utils import cargar_entrenamientos, seleccionar_wod, cargar_frases_motivadoras, seleccionar_frase

# /start
async def start(update: Update, context: ContextTypes.DEFAULT_TYPE):
    await update.message.reply_text(
        "¡Hola! Soy tu bot de entrenamientos con kettlebell 💪. "
        "Prueba escribiendo /wod para recibir un entrenamiento."
        "Si tienes cualquier duda, escribe /help y te diré todas mis funciones."
    )

# /wod
async def wod(update: Update, context: ContextTypes.DEFAULT_TYPE):
    entrenamientos = cargar_entrenamientos()
    elegido = seleccionar_wod(entrenamientos)

    ejercicios_texto = "\n".join(elegido["exercises"])
    mensaje = f"🏋️‍♂️ {elegido['name']}\n\n{ejercicios_texto}"

    await update.message.reply_text(mensaje)

No la voy a poner entera porque es bastante extensa, pero la estructura es similar en todos los Handlers: se define una función asíncrona, con el código que se pretende que haga, y se cierra con await. Esto es así, porque cuando se hace una petición a la API de Telegram, no podemos bloquear el bot hasta que termine si es que tenemos varios usuarios a la vez.

La otra gran parte del proyecto son, por supuesto, los propios entrenamientos que enviamos. En este caso, hemos almacenado hasta 100 entrenamientos y 30 frases motivadoras, almacenadas en sendos ficheros json dentro de la carpeta Data. Tal y como hemos podido ver en la clase anteriormente mostrada, utilizamos funciones de la clase utils para cargar los datos de los json primero, y para devolver uno de los preparados aleatoriamente.

import json
import random
from pathlib import Path

# Leer los entrenamientos desde JSON
def cargar_entrenamientos():
    path = Path(__file__).parent.parent / "data" / "workouts.json"
    with open(path, "r", encoding="utf-8") as f:
        return json.load(f)

# Elegir un WOD aleatorio
def seleccionar_wod(entrenamientos):
    return random.choice(entrenamientos)

# Leer las frases motivadoras desde JSON
def cargar_frases_motivadoras():
    path = Path(__file__).parent.parent / "data" / "motivation.json"
    with open(path, "r", encoding="utf-8") as f:
        return json.load(f)

# Elegir una frase aleatoria
def seleccionar_frase(frases_motivadoras):
    return random.choice(frases_motivadoras)

Programando la programación (horaria)

Valga esta redundancia en el título para hacer saber que me dispongo a explicar, de manera lo más sencilla que pueda, una de las partes más complicadas del bot, y es la capacidad de enviar un entrenamiento de manera automática a la hora que desee el usuario, en función de la hora de su servidor.

# Job programado
async def job_enviar_entrenamiento(context: ContextTypes.DEFAULT_TYPE):
    print("Ejecutando job programado...")  # Añade esta línea
    entrenamientos = cargar_entrenamientos()
    elegido = seleccionar_wod(entrenamientos)

    ejercicios_texto = "\n".join(elegido["exercises"])
    mensaje = f"🏋️‍♂️ {elegido['name']}\n\n{ejercicios_texto}"

    await context.bot.send_message(chat_id=context.job.chat_id, text=mensaje)


# /programar
async def programar(update: Update, context: ContextTypes.DEFAULT_TYPE):
    """
    Uso esperado: /programar 8:30 [timezone]
    Programa un WOD diario a la hora indicada (24h) en la zona horaria especificada.
    Ejemplos: /programar 8:30 America/Mexico_City, /programar 8:30 Europe/Madrid
    """
    if not context.args or len(context.args) < 1 or len(context.args) > 2:
        await update.message.reply_text(
            "Formato: /programar HH:MM [zona_horaria]\n"
            "Ejemplos:\n"
            "• /programar 8:30 America/Mexico_City\n"
            "• /programar 8:30 Europe/Madrid\n"
            "• /programar 8:30 America/Argentina/Buenos_Aires\n"
            "• /programar 8:30 UTC"
        )
        return

    hora_usuario = context.args[0]
    if not re.match(r"^\d{1,2}:\d{2}$", hora_usuario):
        await update.message.reply_text("Formato inválido. Usa HH:MM, por ejemplo 07:45")
        return

    try:
        horas, minutos = map(int, hora_usuario.split(":"))
        if not (0 <= horas <= 23 and 0 <= minutos <= 59):
            await update.message.reply_text("La hora debe estar entre 00:00 y 23:59")
            return
    except ValueError:
        await update.message.reply_text("La hora debe estar entre 00:00 y 23:59")
        return

    # Procesar zona horaria
    timezone_str = context.args[1] if len(context.args) == 2 else "UTC"
    try:
        timezone = pytz.timezone(timezone_str)
    except pytz.exceptions.UnknownTimeZoneError:
        await update.message.reply_text(
            f"❌ Zona horaria '{timezone_str}' no válida.\n"
            "Ejemplos de zonas válidas:\n"
            "• America/Mexico_City\n"
            "• Europe/Madrid\n"
            "• America/Argentina/Buenos_Aires\n"
            "• America/New_York\n"
            "• Asia/Tokyo\n"
            "• UTC"
        )
        return

    chat_id = update.effective_chat.id
    jobs = context.job_queue.get_jobs_by_name(str(chat_id))
    for job in jobs:
        job.schedule_removal()

    # Crear tiempo con zona horaria
    time_with_tz = datetime.time(hour=horas, minute=minutos, tzinfo=timezone)

    context.job_queue.run_daily(
        job_enviar_entrenamiento,
        time=time_with_tz,
        chat_id=chat_id,
        name=str(chat_id)
    )

    print("Job programado:", context.job_queue.jobs())

    await update.message.reply_text(
        f"✅ WOD diario programado a las {hora_usuario.zfill(5)} en zona horaria {timezone_str}"
    )

Si nos atenemos al código, podemos ver que se hacen varias cosas:

La función job_enviar_entrenamiento() es la tarea que se ejecuta automáticamente. Hace lo mismo que /wod, pero en lugar de responder a un mensaje, envía el entrenamiento por su cuenta al chat programado usando context.bot.send_message().

La función programar() podemos deshacerla en varios pedazos:

  1. Valida la hora: Verifica que se haya escrito la hora correctamente (formato HH:MM)
  2. Valida la zona horaria: Verifica que la zona horaria sea válida (ejemplo: America/Mexico_City)
  3. Cancela programaciones previas: Si ya tenías una alarma puesta en ese chat, la elimina
  4. Crea la nueva programación: Usa context.job_queue.run_daily() para decirle al bot:
  • ejecuta job_enviar_entrenamiento
  • hazlo todos los días a la hora que indicada
  • definimos el chat donde queremos enviarlo (chat_id)

Funcionamiento

Lo primero que hay que hacer si uno quiere usar el bot, es buscarlo en Telegram, su nombre es @kettlewodbot.

Descripción del bot en Telegram Bienvenida del bot Una vez dentro, se puede acceder a la lista de comandos, con unas breves instrucciones de uso.

Descripción del bot en Telegram

Si se decide programar tenemos unas instrucciones a parte si se pone solo el handler /programar.

Instrucciones de programación

Bot programado

Por supuesto, seguimos teniendo las otras opciones, como la de un entrenamiento individual, o una frase motivadora: Frase motivacional

La mejor terapia contra el Tutorial Hell

Ha sido un proyecto divertido, y sobre todo, muy agradecido y didáctico.

Yo, que como mucha gente antes, he estado atrapado en el tan temido y odiado Tutorial Hell, creo que este tipo de proyectos de fin de semana, son una magnífica manera, tanto de aprender, como de salir de nuestra zona de confort, y sobre todo, de tener la capacidad de llevar un proyecto a cabo de principio a fin.

Al final, en vez de reinventar la rueda, lo mejor es centrarse en las necesidades propias o de nuestros allegados, e intentar solucionarlas.