De RSS a Telegram i X: com automatitzar un flux de publicació sense perdre control editorial
Tutorial per automatitzar la publicació de contingut des de RSS a Telegram i X, amb validació humana i control editorial real.

Tinc un canal de Telegram i un compte de X on publico contingut tècnic curat. Durant un temps ho feia tot a mà: llegia articles, seleccionava els que em semblaven interessants, escrivia un breu comentari i els publicava. Funcionava, però em menjava entre 30 i 45 minuts cada dia només en això. I el pitjor no era el temps: era la inconsistència. Els dies que no en tenia ganes, no publicava. I un canal que publica de forma irregular perd audiència ràpid.
Així que vaig muntar un sistema automatitzat. No perquè publiqués sol, sinó perquè em preparés tot i jo només hagués d’aprovar o descartar. Aquesta diferència és important: no busco un bot que spamegi enllaços. Busco un assistent que m’estalviï la part mecànica i em deixi centrar-me en el criteri editorial.
Aquest article explica el flux complet que uso, basat en el que vaig construir per a Rolsfera. Si gestioneu un canal de contingut tècnic i voleu automatitzar sense perdre el control, això us estalviarà bastantes hores de prova i error.
El flux complet
Abans d’entrar a cada peça, aquest és el flux de principi a fi:
RSS Feeds → Parseig → Filtrat → Resum (IA) → Cua de revisió → Aprovació humana → Formatatge → Publicació (Telegram / X)Sembla simple. I conceptualment ho és. La complexitat real està als detalls de cada pas: com filtres, com resums, com formates per a cada plataforma, com gestiones errors i duplicats.
Anem peça per peça.
Pas 1: Recollir contingut des de RSS
El punt d’entrada són feeds RSS. Tinc una llista d’unes 40 fonts que segueixo: blogs tècnics, mitjans especialitzats, newsletters que publiquen via RSS, repos de GitHub amb releases feed i algun subreddit via RSS.
# feeds.py - Lista de fuentes con metadatos
FEEDS = [
{
"url": "https://blog.pragmaticengineer.com/rss/",
"name": "Pragmatic Engineer",
"category": "engineering",
"priority": "high",
},
{
"url": "https://martinfowler.com/feed.atom",
"name": "Martin Fowler",
"category": "architecture",
"priority": "high",
},
{
"url": "https://news.ycombinator.com/rss",
"name": "Hacker News",
"category": "general",
"priority": "medium",
},
# ... 37 fonts més
]Cada font té una categoria i una prioritat. La prioritat no és arbitrària: l’ajusto segons l’historial d’articles que acabo aprovant de cada font. Si el 80% del que publica Pragmatic Engineer em sembla publicable, és high. Si de Hacker News només en publico un 10%, és medium.
El parseig el faig amb feedparser en Python. n8n dispara el procés cada 30 minuts a través d’un cron trigger que crida un endpoint HTTP del meu servei.
import feedparser
from datetime import datetime, timedelta
def fetch_new_articles(feed_url: str, since_hours: int = 2) -> list[dict]:
feed = feedparser.parse(feed_url)
cutoff = datetime.utcnow() - timedelta(hours=since_hours)
articles = []
for entry in feed.entries:
published = entry.get("published_parsed")
if published:
pub_date = datetime(*published[:6])
if pub_date < cutoff:
continue
articles.append({
"title": entry.get("title", "").strip(),
"url": entry.get("link", ""),
"summary": entry.get("summary", ""),
"published": entry.get("published", ""),
})
return articlesPas 2: Filtrat i deduplicació
No tot el que entra val la pena processar-ho. El filtrat té dos nivells:
Deduplicació. Si el mateix article ja és a la base de dades (per URL o per hash de contingut), es descarta. Això és crític perquè molts feeds comparteixen les mateixes notícies.
Filtrat per rellevància bàsica. Abans de gastar tokens d’IA, aplico filtres simples:
# Palabras clave que indican contenido relevante para mi audiencia
INCLUDE_KEYWORDS = [
"python", "backend", "api", "architecture", "kubernetes",
"database", "automation", "scraping", "llm", "self-hosted",
"devops", "data engineering", "microservices",
]
# Contenido que normalmente descarto
EXCLUDE_PATTERNS = [
"sponsored", "advertisement", "podcast episode",
"weekly roundup", # demasiado genérico
]
def passes_basic_filter(article: dict) -> bool:
text = f"{article['title']} {article['summary']}".lower()
for pattern in EXCLUDE_PATTERNS:
if pattern in text:
return False
for keyword in INCLUDE_KEYWORDS:
if keyword in text:
return True
return False # si no coincide con nada relevante, no pasaAquest filtrat és bast. Ho sé. Però redueix el volum d’articles que arriben al pas d’IA en un 60-70%, cosa que té un impacte directe en cost i temps de processament.
Pas 3: Resum amb IA
Els articles que passen el filtre s’envien a un LLM per generar un resum curt i una classificació. El prompt està dissenyat per obtenir exactament el que necessito per decidir si publicar:
def generate_summary(article: dict) -> dict:
prompt = f"""Eres un editor técnico. Analiza este artículo y responde en JSON:
Título: {article['title']}
Contenido: {article['content'][:2500]}
Responde con:
{{
"summary": "Resumen de 2-3 frases, directo, sin relleno",
"topic": "Tema principal (ej: Python, DevOps, arquitectura)",
"is_actionable": true/false, // ¿aporta algo práctico?
"suggested_comment": "Frase de 1 línea que usaría al compartirlo"
}}
NO incluyas frases genéricas tipo 'Este artículo explora...'
Sé directo y concreto."""
response = call_llm(prompt, model="gpt-4o-mini")
return json.loads(response)Uso gpt-4o-mini per a aquesta tasca perquè no necessito el model més potent. Un resum de 2-3 frases i una classificació és alguna cosa que els models petits fan bé. Reservo models més grans per quan necessito generar contingut original o fer anàlisis més complexes.
El prompt importa més del que sembla. Afegir “NO incluyas frases genéricas” va ser la diferència entre obtenir resums útils i obtenir farciment tipus “Este interesante artículo examina…”.
Pas 4: Cua de revisió
Els articles processats arriben a una cua on els reviso. A la pràctica és una taula a PostgreSQL amb una interfície web mínima al damunt:
SELECT
a.title,
a.url,
a.ai_metadata->>'summary' AS summary,
a.ai_metadata->>'suggested_comment' AS comment,
a.ai_metadata->>'topic' AS topic,
a.source_name,
a.created_at
FROM articles a
WHERE a.status = 'pending'
ORDER BY
CASE
WHEN a.source_priority = 'high' THEN 1
WHEN a.source_priority = 'medium' THEN 2
ELSE 3
END,
a.created_at DESC;La revisió em porta entre 5 i 10 minuts al dia. Els articles de fonts d’alta prioritat apareixen primer. Per a cadascun, faig una de tres coses: aprovar (de vegades editant el comentari suggerit per la IA), descartar o guardar per a més tard.
Pas 5: Formatatge per plataforma
Cada plataforma té les seves regles. El que funciona a Telegram no funciona a X i viceversa.
Telegram permet missatges llargs, Markdown, emojis i enllaços amb preview. El meu format típic:
def format_for_telegram(article: dict) -> str:
comment = article["ai_metadata"]["suggested_comment"]
title = article["title"]
url = article["url"]
topic = article["ai_metadata"]["topic"]
return f"""🔗 *{title}*
{comment}
📌 Tema: {topic}
👉 [Leer artículo]({url})"""X (Twitter) té límit de 280 caràcters. El format és més compacte:
def format_for_x(article: dict) -> str:
comment = article["ai_metadata"]["suggested_comment"]
url = article["url"]
topic = article["ai_metadata"]["topic"]
# X cuenta caracteres, los URLs ocupan ~23
max_comment_len = 280 - 23 - len(f" #{topic}") - 5
if len(comment) > max_comment_len:
comment = comment[:max_comment_len - 3] + "..."
return f"{comment} #{topic} {url}"Pas 6: Publicació
La publicació la gestiona n8n. Quan marco un article com a aprovat, el seu estat canvia a la base de dades. Un workflow de n8n que corre cada 5 minuts detecta articles aprovats i els publica.
El flux a n8n és simple:
- Trigger cron cada 5 minuts
- HTTP Request a l’endpoint que retorna articles aprovats
- Split In Batches per no publicar-ho tot de cop
- Telegram Node per enviar al canal
- HTTP Request a l’API de X per publicar el tweet
- HTTP Request per actualitzar l’estat a
published
La part de “no publicar-ho tot de cop” és important. Si aprovo 8 articles al matí i es publiquen tots alhora, l’experiència per als seguidors és dolenta. Uso un sistema de slots temporals: màxim 2 publicacions per hora, amb un mínim de 20 minuts entre elles.
# Lógica de scheduling simplificada
from datetime import datetime, timedelta
def get_next_publish_slot(last_published_at: datetime) -> datetime:
min_gap = timedelta(minutes=20)
next_slot = last_published_at + min_gap
# No publicar entre las 23:00 y las 08:00
if next_slot.hour >= 23 or next_slot.hour < 8:
next_slot = next_slot.replace(hour=8, minute=0, second=0)
if next_slot <= last_published_at:
next_slot += timedelta(days=1)
return next_slotOn usar n8n i on Python
Un dubte que em va sorgir al principi va ser fins on portar la lògica a n8n i quan moure-la a Python. Després de diverses iteracions, la meva regla és aquesta:
| Tasca | Eina | Per què |
|---|---|---|
| Orquestració (triggers, scheduling) | n8n | Interfície visual, fàcil de modificar |
| Crides a APIs externes simples | n8n | Nodes natius per a Telegram, HTTP |
| Parseig de RSS | Python | feedparser és més robust que el node RSS de n8n |
| Filtrat i deduplicació | Python | Lògica complexa amb accés a BD |
| Processament amb IA | Python | Control fi sobre prompts i respostes |
| Formatatge de contingut | Python | Lògica de plantilles i truncament |
| Monitorització i alertes | n8n | Error handling visual, fàcil de depurar |
La regla general: n8n per orquestrar, Python per processar. Quan intentes ficar lògica de processament complexa en nodes de n8n, acabes amb un workflow il·legible. I quan intentes replicar l’orquestració visual de n8n en codi, acabes reinventant un scheduler.
Errors comuns (i com els gestiono)
Rate limits
Tant l’API de Telegram com la de X tenen límits de peticions. Telegram és bastant permissiu amb bots, però X és estricte. La meva solució: un sistema de cua amb reintents exponencials.
import time
def publish_with_retry(publish_fn, content, max_retries=3):
for attempt in range(max_retries):
try:
return publish_fn(content)
except RateLimitError as e:
wait_time = (2 ** attempt) * 30 # 30s, 60s, 120s
print(f"Rate limit. Reintentando en {wait_time}s...")
time.sleep(wait_time)
raise PublishError(f"Fallo tras {max_retries} intentos")Duplicats que es colen
De vegades el mateix article apareix a diversos feeds amb URLs lleugerament diferents (amb paràmetres UTM, per exemple). La solució és normalitzar URLs abans de comparar:
from urllib.parse import urlparse, urlunparse, parse_qs, urlencode
def normalize_url(url: str) -> str:
parsed = urlparse(url)
# Eliminar parámetros de tracking
params = parse_qs(parsed.query)
clean_params = {
k: v for k, v in params.items()
if not k.startswith("utm_")
}
clean_query = urlencode(clean_params, doseq=True)
return urlunparse(parsed._replace(query=clean_query, fragment=""))Format trencat a publicacions
Markdown que es renderitza malament a Telegram, tweets que excedeixen el límit de caràcters, enllaços que no generen preview. Això ho vaig descobrir amb el temps. La solució va ser afegir validacions abans de publicar:
def validate_telegram_message(text: str) -> bool:
if len(text) > 4096:
return False
# Verificar que el Markdown está balanceado
if text.count("*") % 2 != 0:
return False
if text.count("_") % 2 != 0:
return False
return TrueBots baneats
Em va passar un cop amb X. Publicava massa ràpid i el compte va ser suspès temporalment. Des d’aleshores: màxim 10 publicacions al dia, amb distribució horària, i mai publico exactament a intervals regulars (afegeixo un jitter aleatori d’1-5 minuts per no semblar un bot).
La revisió humana: per què no ho faig tot automàtic
Podria treure el pas de revisió i deixar que el sistema publiqués tot el que passés els filtres. Tècnicament és trivial. Però no ho faig per tres raons:
La IA s’equivoca. No sempre, però prou com perquè un canal sense supervisió acabi publicant contingut irrellevant o mal classificat. Un resum que sembla correcte pot estar traient de context la conclusió de l’article.
El criteri editorial és el que diferencia el canal. Qualsevol pot muntar un bot que publiqui tot el que surt a Hacker News. El que fa valuós un canal de contingut és que algú amb criteri ha decidit que allò val la pena.
Em manté connectat amb el contingut. Si ho automatitzo tot, perdo contacte amb el que s’està publicant al meu sector. La revisió diària de 10 minuts és també la meva sessió de lectura ràpida.
Automatitzar no és eliminar l’humà. És alliberar l’humà de les tasques mecàniques perquè es concentri en el que aporta valor: el criteri.
Costos reals del sistema
Perquè no quedi tot en abstracte:
| Concepte | Cost mensual aprox. |
|---|---|
| VPS (n8n + serveis) | ~10€ |
| API LLM (resums, classificació) | ~15-25€ |
| API de X (tier bàsic) | 0€ (free tier) |
| Telegram Bot API | 0€ |
| PostgreSQL (self-hosted a VPS) | 0€ (inclòs a VPS) |
| Total | ~25-35€/mes |
No és gratuït, però és menys del que costa una subscripció a qualsevol eina SaaS de gestió de contingut. I el sistema és meu, el controlo i el puc modificar sense dependre que una empresa canviï els seus preus o tanqui la seva API.
Conclusió
Muntar aquest flux em va portar unes dues setmanes de treball intermitent. La primera versió era molt més simple: un script de Python que llegia RSS, generava un resum i l’enviava a Telegram. Tot automàtic, sense revisió. El resultat era mediocre: publicacions irrellevants, duplicats i un to genèric que no representava el meu criteri.
La versió actual és millor perquè accepta que l’automatització total no és l’objectiu. L’objectiu és reduir el treball mecànic al mínim i deixar que la decisió editorial segueixi sent humana.
Si esteu pensant a muntar alguna cosa similar, el meu consell és que comenceu pel pas més simple (RSS → Telegram, sense IA, sense filtrat complex) i aneu afegint capes segons necessiteu. La complexitat d’aquest sistema no va venir d’un disseny inicial brillant, sinó d’anar resolent problemes reals una setmana rere una altra.


