Bot de Telegram que resume links con Gemini

Un bot para un grupo de amigos que siguen AI de cerca. Tira un link, el bot lo resume, el que quiere lo lee. Sin comentario previo. Sin pedirle a nadie que explique.

Python Telegram Bot API Gemini 2.5 Flash trafilatura systemd VPS
01

El driver

Dos problemas en simultáneo.

El primero es un sesgo con nombre. Cuando estás en un grupo donde hay gente que sabe más que vos sobre un tema, tenés menos chances de compartir algo relacionado con ese tema. No porque no sea relevante — sino porque el solo hecho de estar en un grupo de expertos activa lo que los psicólogos llaman evaluation apprehension: la inhibición de participar cuando sentís que otros te están evaluando.

Einstein lo experimentó él mismo. A pesar de ser quien era, relataba sentirse fuera de lugar en reuniones con Bohr y otros físicos, y escribió que se sentía "un estafador involuntario" dado el estima que otros tenían de su trabajo. El insight contraintuitivo es que incluso Einstein callaba en grupos de expertos.

El bot invierte esa dinámica. Tirás el link sin opinar, sin comentario previo. El resumen aparece solo. El que sabe más que vos sobre ese tema decide si lo lee, si lo ignora, o si agrega contexto. Pero vos ya lo tiraste — sin arriesgar nada.

El formato importa: compartir un link con comentario es tomar postura. Compartir un link solo es abrir una puerta. El bot te permite hacer lo segundo sin costo.

El segundo es más simple. En AI las cosas se mueven rápido y hay personas en el grupo que están al día desde ángulos distintos. El problema no es que nadie quiera compartir — es que nadie tiene tiempo de explicar lo que tira, ni de esperar que los demás lo lean antes de seguir. El bot achica ese fricción: el resumen está disponible para quien quiera, cuando quiera, sin depender de que alguien lo explique.

02

La arquitectura

Cinco módulos, cada uno con una sola responsabilidad:

bot.py       —— punto de entrada, long polling con Telegram
   ↓
handlers.py  —— orquesta el flujo por mensaje
   ↓
fetcher.py   —— extrae la URL y descarga el contenido
   ↓
summarizer.py llama a Gemini y formatea los bullets
   ↓
Telegram API —— postea el resumen en el grupo

El flujo por mensaje es lineal: si en cualquier punto algo falla o no hay contenido útil, el handler decide si postear el fallback o silencio, y sigue.

MóduloResponsabilidadFalla cómo
fetcher.pyDescarga y extrae textoDevuelve None → fallback visible
summarizer.pyLlama a GeminiDevuelve None → silencio + log
handlers.pyOrquesta y envíaNo propaga excepciones al poller

Cada módulo devuelve None en caso de error y logea a stdout. Si algo falla, el grupo no ve un error técnico sino un mensaje amigable o silencio.

03

El stack

python-telegram-bot v21 maneja el long polling. En Python 3.14 el event loop ya no se crea automáticamente, así que el bot usa asyncio.run() con el patrón async with app de la librería.

trafilatura es la pieza clave para extracción de contenido. Descarga la página, identifica el cuerpo del artículo e ignora navegación, ads y footers. Funciona bien con la mayoría de sitios de noticias y blogs. No funciona con sitios que requieren JavaScript para renderizar (X/Twitter, Instagram, TikTok) — para esos hay un blocklist explícito.

google-genai 2.0 (el SDK nuevo, no google-generativeai que está deprecado) llama a Gemini 2.5 Flash. El output se escapa con html.escape() antes de enviarlo a Telegram para evitar errores de parseo con caracteres especiales.

python-telegram-bot[ext]>=21,<22
google-genai>=1.0
trafilatura>=1.12
python-dotenv>=1.0

Dos variables de entorno: BOT_TOKEN y GEMINI_API_KEY. El bot no arranca si falta alguna.

04

Lo que funciona y lo que no

Funciona bien: artículos de noticias, blogs, papers, documentación técnica, páginas con texto estático. trafilatura extrae bien el contenido principal y Gemini produce bullets concisos y correctos en español.

No funciona (y lo dice): X/Twitter, Instagram, TikTok, LinkedIn. Todos son JS-only. El bot responde con un fallback amigable en lugar de silencio:

📎 No pude leer el contenido de ese link — puede ser un video, PDF o página protegida.

Caso edge que apareció en producción: trafilatura a veces descarga la página de error "JavaScript deshabilitado" de X.com y la extrae como si fuera contenido válido. Gemini obedientemente la resume. La solución fue doble: blocklist de dominios + detección de frases de junk en el texto extraído.

El modelo: empecé con gemini-2.0-flash pero no estaba disponible para API keys nuevas. gemini-1.5-flash tampoco existe en el SDK nuevo. El modelo correcto con google-genai 2.0 es gemini-2.5-flash.

05

Deploy en VPS

El bot corre como servicio systemd en un VPS Ubuntu 24.04. No Docker, no infraestructura extra. Long polling hace outbound a Telegram — no necesita puertos abiertos ni dominio.

El archivo de servicio define el usuario, el directorio de trabajo, el archivo de variables de entorno, y el comando de arranque. Con Restart=on-failure y RestartSec=5 el servicio se recupera solo si el proceso muere. Los logs quedan en el journal del sistema.

El deploy es un rsync local → VPS + systemctl restart. Sin CI/CD, sin contenedores. Para el volumen de un grupo de Telegram es más que suficiente.

06

Próximos pasos

  • Caché de URLs: si el mismo link se comparte dos veces, el bot lo procesa dos veces. Un dict en memoria con TTL de 24 horas evitaría el doble procesamiento y el doble costo de API.
  • Múltiples links por mensaje: hoy procesa solo el primero. En grupos donde se comparten varios links juntos, podría procesar todos con un delay entre cada uno para no hacer flood.
  • Soporte para PDFs: trafilatura no extrae PDFs. Con pdfminer o pymupdf se podría agregar soporte para links directos a PDFs.
  • Comando /off: poder silenciar el bot en el grupo sin tener que eliminarlo.

La pregunta interesante de fondo es si vale la pena agregarle memoria: que el bot recuerde qué temas circularon en el grupo y pueda conectar un artículo nuevo con algo que se compartió hace tres semanas. Eso ya cambia la naturaleza del bot.