Análisis de la arquitectura de social_poster — una aplicación en Django que se encarga sola del 95 % de la rutina, pero sin quitarme el derecho a veto.

El problema con el que empezó todo

Una estrategia de contenido para varias plataformas es una rutina diaria y silenciosa. Escribes un post → entras a Reddit → lo adaptas al subreddit → entras a LinkedIn → lo reescribes en texto plano → Pinterest → Tumblr → Medium. Quince minutos por un solo post. Todos los días. Los brazos se te caen antes de que se acabe el plan de contenidos.

La solución obvia suena bonita: «pongo un bot que lo reparta todo solo». Pero aquí se esconde la trampa con la que chocan casi todos los que automatizan las publicaciones. Un autoposter totalmente autónomo, tarde o temprano, publicará algo que no debería haber publicado: un markdown roto en LinkedIn, un titular cortado a media palabra en Pinterest, un borrador en lugar de la versión final. Y eso no son logs abstractos en la consola: es tu cara delante de tu audiencia.

Por eso no me puse a construir «un bot que publica solo». Construí social_poster: un sistema que hace todo el trabajo pesado de forma automática, pero antes de publicar me manda una vista previa a Telegram con dos botones: ✅ Publicar / ❌ Rechazar.

Y esa es la idea principal de este artículo: la automatización human-in-the-loop. No «el humano en lugar de la automatización» ni «la automatización en lugar del humano», sino una automatización que se hace cargo de la mecánica pero deja la decisión en tus manos.

El esquema en una servilleta

Antes de zambullirnos en el código, aquí tienes todo el flujo de un vistazo:

Fuente de contenido
      │
      ▼
  Django  ──► crea Post + PlatformVariant para cada plataforma
      │
      ▼
  Celery  ──► arma la vista previa de todas las variantes
      │
      ▼
 Telegram ──► envía la vista previa + botones [✅ / ❌]
      │
   approve
      │
      ▼
  Celery  ──► 5 tareas de publicación en paralelo (una por plataforma)
      │
      ▼
 Telegram ◄── informe: dónde se publicó, dónde falló, enlaces

Lo clave aquí es el punto de approve/reject en Telegram, justo en el medio. Todo lo anterior se prepara automáticamente. Todo lo posterior se lanza solo después de mi toque.

Análisis nodo por nodo: por qué así y no de otra forma

Django como centro de gravedad

Tres modelos sostienen todo el sistema: Post (el contenido original), PlatformVariant (la versión adaptada a una plataforma concreta) y PublishLog (qué se envió, a dónde y con qué resultado). Como bonus viene Django Admin: un panel gratis donde después reviso qué se publicó y qué falló, sin una sola línea de frontend.

Se podría haber resuelto con FastAPI a secas, pero aquí Django gana precisamente por sus «pilas incluidas»: ORM, admin y migraciones de fábrica. Es uno de esos casos en los que la herramienta «aburrida» es la correcta.

Celery — porque las APIs de las redes son lentas y se caen

Si publicas en cinco APIs de forma síncrona dentro de una sola petición, esa petición se quedará colgada más de 30 segundos y se caerá con el primer timeout. Y si se cae Reddit, no debería sufrir LinkedIn.

Por eso cada plataforma es una tarea de Celery independiente, con sus propios reintentos y backoff exponencial. Las tareas son independientes: si una se cae, las demás se publican como si nada.

Alternativas que consideré:

Herramienta Por qué no la elegí
RQ Más simple que Celery, pero más flojo con reintentos y programaciones — poco para 5 APIs inestables
Temporal Lo uso en mi trabajo principal, un sueño durable, pero para cinco tareas es matar moscas a cañonazos
asyncio.gather a secas Sin persistencia: se cae el worker y pierdes el estado de la publicación

Celery resultó ser el término medio: no es sobreingeniería, pero es lo bastante durable como para dormir tranquilo.

Telegram como interfaz de aprobación

Podría haber montado una página web con un botón «Publicar». ¿Pero para qué, si de todos modos me paso todo el día en Telegram? Botones inline + callback_query son mínimo código y máxima comodidad: la vista previa me llega directo al teléfono y apruebo desde el sofá.

De forma simplificada, el handler de aprobación se ve así:

@dp.callback_query(F.data.startswith("approve:"))
async def on_approve(callback: CallbackQuery):
    post_id = int(callback.data.split(":")[1])
    # encolamos las tareas de publicación en todas las plataformas de golpe
    for platform in ("reddit", "linkedin", "pinterest", "tumblr", "medium"):
        publish_variant.delay(post_id, platform)
    await callback.message.edit_text("Publicando en todas las plataformas…")

Nada de UI de más: solo lo que de verdad se pulsa.

Adaptadores por plataforma

El corazón del sistema es el patrón «una interfaz, cinco implementaciones». Cada adaptador sabe cómo convertir un Post genérico en algo que la plataforma concreta se trague, porque cada una tiene sus manías:

Plataforma Límite Formato La «piedra» principal
Medium grande su propio markdown exige su propio juego de etiquetas, si no publica como borrador
Reddit ~40k reddit-markdown necesita el subreddit y el flair correctos, si no hay autoeliminación
LinkedIn ~3000 plano + emojis no entiende markdown en absoluto, prefiere los enlaces al final
Pinterest corto plano + imagen la imagen es obligatoria, sin ella el post es inválido
Tumblr medio HTML caprichoso con las etiquetas y el formato de los adjuntos

La interfaz común del adaptador es trivial, y es justo eso lo que permite añadir una sexta plataforma sin tocar las demás:

class PlatformAdapter(Protocol):
    def render(self, post: Post) -> PlatformVariant: ...
    def publish(self, variant: PlatformVariant) -> PublishResult: ...

Las piedras con las que de verdad aprendí

Markdown ≠ markdown. Lo más traicionero. Medium tiene su dialecto, Reddit el suyo, y LinkedIn ni siquiera va de markdown, sino de texto plano con emojis. Un mismo original está obligado a pasar por cinco renderizadores distintos, o acabas con un revoltijo de símbolos ** y # en el feed.

Rate limits y deprecaciones de las APIs. Pinterest y LinkedIn adoran cambiar permisos y versiones de la API sin avisar. Se cura fijando la versión a hierro y con una alerta al mismo Telegram ante un 401/403 — para enterarme de la avería por el sistema y no por mis seguidores.

Duplicados en los reintentos. El clásico de los sistemas distribuidos: la tarea se cae después de publicar pero antes de escribir en PublishLog, y el reintento publica una segunda vez. Se cura con una clave idempotente en la publicación: antes de postear, comprobamos que no se haya enviado ya.

Fallos «silenciosos». La API devuelve un 200 OK, pero el post no aparece: cayó en moderación o en shadowban. Por eso registro no el hecho de la respuesta, sino la URL del post publicado, y considero la tarea exitosa solo si el enlace existe de verdad.

Lo que da en la práctica

Quince minutos de trajín manual se convirtieron en veinte segundos: llegó la vista previa, le eché un ojo, pulsé  "Publicar". El control siguió íntegramente en mis manos, y el «uy, publiqué lo que no era» no ha pasado ni una sola vez. La automatización se llevó la rutina, pero no se llevó la responsabilidad: exactamente el equilibrio que considero correcto para todo lo que tiene que ver con la cara pública.

Qué viene en la serie

En el próximo análisis: un pipeline de dos agentes sobre Claude, donde un agente escribe los borradores y el otro los revisa y corrige. Lo más interesante: su salida se conecta a este mismísimo chat de Telegram. Así la generación de contenido se acopla con la distribución en un único conducto de punta a punta: «idea → borrador → aprobación → cinco plataformas».

No construyo estas cosas solo para mí: en yourtarget.pro armo este tipo de automatizaciones human-in-the-loop para negocios — sin hype, con una arquitectura clara y con el humano ahí donde la decisión importa más que la velocidad.