Saltar al contenido principal
SocialCutter

Mensajeria y bots

Bot de Telegram que adapta tus fotos con SocialCutter

Bot de Telegram en Python que recibe una foto, la procesa con la API de SocialCutter y la devuelve en el formato exacto de cada red con sendPhoto.

  • Telegram
  • bot
  • sendPhoto
  • Python
  • requests
  • SocialCutter
  • redes sociales

Qué hace este bot

Un bot que recibe una foto en Telegram y contesta con esa misma foto ya adaptada a las medidas de cada red social. El usuario manda la imagen con una orden en el pie (/ig, /story, /li, /todo) y recibe una respuesta por formato, cada una con la plataforma, las medidas y el enlace de descarga.

Conviene decir esto claro: SocialCutter no publica en redes sociales. Genera los ficheros con el encuadre centrado y devuelve una URL pública por salida. El envío al chat lo hace el bot; la publicación en Instagram, LinkedIn, X o TikTok la hace la herramienta de publicación o tu integración con la API de cada plataforma.

Requisitos

  • Token del bot, creado con @BotFather.
  • Python 3 y la librería requests (pip install requests).
  • Una clave sc_ de SocialCutter, creada en https://dash.socialcutter.theboomer.dev en Perfil → API keys.
  • Forma de recibir mensajes: sondeo con getUpdates (sencillo) o webhook HTTPS con setWebhook (mejor en producción).
pip install requests
export TELEGRAM_BOT_TOKEN="123456:ABC-tu-token"
export SOCIALCUTTER_API_KEY="sc_tu_clave"

La referencia del Bot API está en https://core.telegram.org/bots/api y la de SocialCutter en https://docs.socialcutter.theboomer.dev.

El flujo, paso a paso

  1. Llega la foto y la orden. El usuario manda una foto con el pie /ig linkedin. Si no hay orden, el bot aplica un destino por defecto (uno solo, para no gastar de más).
  2. Descarga el fichero. getFile devuelve file_path y el fichero se baja desde https://api.telegram.org/file/bot<token>/<file_path>.
  3. Pide los formatos. El fichero se sube a POST /api/v1/images/process/upload (multipart, máximo 5 MB) con la lista de destinos.
  4. Responde con sendPhoto. Cada salida del array outputs se envía con su URL pública. Telegram descarga la imagen y la muestra en el chat.

Por qué se descarga el fichero en vez de pasar la URL de Telegram

La tentación es dar a SocialCutter la URL del fichero de Telegram como source. No lo hagas: esa URL contiene el token del bot, así que entregarla a un tercero es filtrar la credencial que controla tu bot. Además Telegram solo garantiza que el enlace viva 1 hora y limita la descarga directa a 20 MB (https://core.telegram.org/bots/api#file). Descargando y subiendo por multipart, la credencial no sale de tu servidor y el límite es el de SocialCutter: 5 MB.

Código completo

import json
import os
import time

import requests

BOT_TOKEN = os.environ["TELEGRAM_BOT_TOKEN"]
SC_KEY = os.environ["SOCIALCUTTER_API_KEY"]
TG = f"https://api.telegram.org/bot{BOT_TOKEN}"
SC_URL = "https://api.socialcutter.theboomer.dev"

# Orden del pie del mensaje -> destinos de SocialCutter. 1 uso por destino.
ORDENES = {
    "/ig": [{"platform": "instagram", "format": "post"}],
    "/story": [{"platform": "instagram", "format": "story"}],
    "/li": [{"platform": "linkedin", "format": "post"}],
    "/todo": [
        {"platform": "instagram", "format": "post"},
        {"platform": "linkedin", "format": "post"},
        {"platform": "twitter", "format": "post"},
    ],
}
POR_DEFECTO = ORDENES["/ig"]          # una sola salida si el usuario no pide nada
OPCIONES = {"format": "jpg", "quality": 88}


def tg(metodo, **payload):
    r = requests.post(f"{TG}/{metodo}", json=payload, timeout=60)
    r.raise_for_status()
    return r.json()


def descargar(file_id):
    info = tg("getFile", file_id=file_id)["result"]
    r = requests.get(f"https://api.telegram.org/file/bot{BOT_TOKEN}/{info['file_path']}", timeout=60)
    r.raise_for_status()
    return r.content


def procesar(imagen, destinos):
    r = requests.post(
        f"{SC_URL}/api/v1/images/process/upload",
        headers={"X-API-Key": SC_KEY},
        files={"file": ("foto.jpg", imagen, "image/jpeg")},
        data={"destinations": json.dumps(destinos), "options": json.dumps(OPCIONES)},
        timeout=120,
    )
    r.raise_for_status()
    return r.json()


def atender(message):
    chat_id = message["chat"]["id"]
    if "photo" not in message:
        tg("sendMessage", chat_id=chat_id, text="Mándame una foto con la orden: /ig, /story, /li o /todo.")
        return

    orden = (message.get("caption") or "").split()
    destinos = ORDENES.get(orden[0] if orden else "", POR_DEFECTO)

    foto = message["photo"][-1]                       # el ultimo es el de mayor resolucion
    if foto.get("file_size", 0) > 5 * 1024 * 1024:
        tg("sendMessage", chat_id=chat_id, text="La foto pasa de 5 MB: el límite de SocialCutter.")
        return

    aviso = tg("sendMessage", chat_id=chat_id, text=f"Generando {len(destinos)} formato(s)...")["result"]
    try:
        datos = procesar(descargar(foto["file_id"]), destinos)
    except requests.HTTPError as e:
        codigo = e.response.status_code
        mensaje = {401: "La clave de API no es válida.", 413: "Imagen de más de 5 MB.",
                   429: "Cuota agotada."}.get(codigo, f"Error {codigo} al procesar.")
        tg("editMessageText", chat_id=chat_id, message_id=aviso["message_id"], text=mensaje)
        return
    except requests.RequestException:
        tg("editMessageText", chat_id=chat_id, message_id=aviso["message_id"],
           text="No pude contactar con el servicio. Prueba otra vez.")
        return

    tg("deleteMessage", chat_id=chat_id, message_id=aviso["message_id"])
    for out in datos["outputs"]:
        tg("sendPhoto", chat_id=chat_id, photo=out["url"],
           caption=f"{out['platform']} {out['format']} · {out['width']}x{out['height']}")


def main():
    offset = None
    while True:
        params = {"timeout": 50, "offset": offset} if offset else {"timeout": 50}
        for update in requests.get(f"{TG}/getUpdates", params=params, timeout=60).json()["result"]:
            offset = update["update_id"] + 1
            if "message" in update:
                atender(update["message"])
        time.sleep(0.5)


if __name__ == "__main__":
    main()

Detalles que importan:

  • message["photo"] es un array de tamaños: usa el último, el mayor.
  • OPCIONES fuerza la salida en jpg; el valor por defecto de la API es webp y las fotos de Telegram se envían cómodamente en jpg o png.
  • sendPhoto acepta una URL pública o un file_id; con URL, el máximo documentado es 5 MB (https://core.telegram.org/bots/api#sendphoto). En producción, cambia el sondeo por un webhook (setWebhook).

Cómo limitar el coste

Cada destino consume 1 uso, así que el coste lo fija el bot, no el usuario:

  • Una orden = una lista de destinos. /todo con tres destinos cuesta 3 usos por foto; deja que sea una decisión explícita y avisa en el mensaje.
  • Destinos por defecto mínimos (uno), y órdenes combinadas solo cuando se pidan: /ig /story.
  • Deduplica por file_unique_id y envía Idempotency-Key con el update_id: si repite el fichero o la red reintenta, devuelve las URLs ya generadas sin gastar otro uso.
  • Consulta GET /api/v1/credits antes de lotes grandes y corta con un mensaje claro si queda poco saldo.

Errores típicos

SíntomaCausaSolución
401 de SocialCutterClave ausente o revocadaRevisa X-API-Key; solo hay una clave activa por cuenta
413 de SocialCutterFichero de más de 5 MBComprime antes de subir o pide al usuario una foto menor
400 de Telegram al enviar la fotoEl formato de salida no le sirve al clientePide options.format = jpg o png
El bot no respondeEl proceso murió o el webhook fallaComprueba getWebhookInfo o los logs del sondeo
429 de SocialCutterCuota diaria agotadaPara el bot y avisa; el cupo se renueva cada día

Privacidad

Las salidas se sirven como URLs públicas y sin autenticación (los endpoints de storage y de imagen son abiertos): quien tenga el enlace ve la imagen, así que trátalas como material público y no proceses documentos internos. La foto del usuario sale de Telegram y viaja a la API de SocialCutter. Dilo en el propio bot, sobre todo si es de uso interno.

Coste

  • 1 uso por destino (plataforma y formato) por foto; los destinos repetidos no se cobran dos veces.
  • Los procesamientos fallidos se devuelven.
  • Todos los planes incluyen API y MCP: Free 3 usos/día, Basic 10, Pro 30, Agency 100.

Siguientes pasos

Preguntas frecuentes

¿El bot publica en Instagram, LinkedIn o X?

No. SocialCutter genera las imágenes adaptadas y devuelve una URL pública por formato; el bot las reenvía al chat. Publicar en cada red es tarea de la herramienta de publicación o de tu propia integración con la API de cada plataforma.

¿Cómo recibe el bot la foto del usuario?

Con getUpdates o un webhook. El mensaje trae un array photo con varios tamaños; se usa el último (el de mayor resolución), se pide su file_path con getFile y se descarga con el token del bot.

¿Por qué no paso directamente la URL del fichero de Telegram a SocialCutter?

Porque esa URL incluye el token del bot (https://api.telegram.org/file/bot<token>/<file_path>) y lo estarías entregando a un tercero. Además caduca: Telegram garantiza que el enlace vive al menos 1 hora. Descarga el fichero y súbelo por multipart.

¿Qué formatos puedo pedir?

Instagram post 1080x1080, story 1080x1920 y landscape 1080x566; Facebook post 1200x630, story 1080x1920 y cover 820x312; X post 1200x675 y header 1500x500; LinkedIn post 1200x627 y cover 1128x191; YouTube thumbnail 1280x720 y banner 2560x1440; TikTok cover 1080x1920.

¿Cuánto cuesta cada foto que manda un usuario?

1 uso por destino, es decir por cada combinación de plataforma y formato. Si el bot pide Instagram post y LinkedIn post para la misma foto, son 2 usos; si el usuario elige una sola orden, 1 uso.