# T-HUB

T-HUB

Copia de trabajo del [Tactical Media Broadcast Hub](https://gitlab.moxexley.com/mediatools/tacticalmediahub) para la red de acampadas: [acampada26s.net](https://acampada26s.net), [tomalacalle.net](https://tomalacalle.net) y [takethestreet.net](https://takethestreet.net). El despliegue en uso es [app.occupymedia.cc](https://app.occupymedia.cc). Seguimos la rama `main` de upstream y mandamos allí los cambios que les sirvan. Lo propio de T-HUB va en este repo. Para revisar y validar los cambios, ver [docs/T-HUB-VALIDACION.txt](docs/T-HUB-VALIDACION.txt). Cambios respecto a upstream: - Instagram registrado en conexión y publicación; claves de la app de Meta solo en el entorno del servidor. - Panel **Outreach** para escribir por privado a mano, contacto a contacto, con seguimiento. - RLS forzado en `media_assets` (migración 042). - Estilo de acampada26s.net en la web: fondo blanco, tinta negra, neón `#00e5ff` en sombras y foco, títulos en Anton. Logo de tres tiendas cableadas a un switch con antena, dibujado como el de la acampada (`docs/brand/`). | Remoto | URL | |---|---| | `origin` | `https://gitea.laenre.net/hacklab/T-HUB.git` | | `upstream` | `https://gitlab.moxexley.com/mediatools/tacticalmediahub.git` | ```bash git fetch upstream && git merge upstream/main ``` ## Arquitectura ### 1. Una cuenta Una persona conecta una cuenta del colectivo una sola vez. A partir de ahí redacta, alguien aprueba y el Hub publica. La contraseña de la red social no pasa nunca por el Hub: la red devuelve un token y el Hub lo guarda cifrado. ```mermaid flowchart LR P([Persona]) -->|1. Conectar| H[T-HUB] H -->|2. Pantalla de permiso| R[(Red social)] R -->|3. Token| H P -->|4. Redacta| H H -->|5. Aprobación| A([Otra persona del colectivo]) A -->|6. Visto bueno| H H -->|7. Publica a su hora| R ``` ### 2. Varias cuentas compartidas Cada colectivo tiene sus cuentas y sus reglas de aprobación. Un borrador puede ir a varias cuentas a la vez: el Hub crea una copia por destino y cada una pasa la regla de su cuenta (automática, una persona, mayoría o consenso). Lo que deba salir en toda la red sube a la cola del colectivo padre. ```mermaid flowchart TB subgraph TTS[TAKE THE STREET] Q[Cola común] --> IG1[Instagram internacional] Q --> M1[Mastodon @takethestreet] end subgraph S26[ACAMPADA 26S] D[Borrador] --> IG2[Instagram @acampada26s] D --> M2[Mastodon @26s] D --> T2[Telegram acampada26s] end subgraph SUB[Acampadas locales] B[Barcelona] --> IG3[Instagram propio] S[Sevilla] --> IG4[Instagram propio] end S26 -->|Sube para toda la red| Q SUB -->|Sube a 26S| D IG2 -.->|Regla: 2 aprobaciones| G{{Aprobación}} M2 -.->|Regla: automática| G ``` ### 3. Parte técnica La web solo habla con la API. La API guarda el estado en Postgres con aislamiento por colectivo (RLS), los tokens van en un vault cifrado y el envío programado lo hace el scheduler con Temporal. Cada red es un conector; fuera de los conectores no hay código que distinga redes. ```mermaid flowchart LR subgraph Cliente W[apps/web
Next.js] end subgraph Control API[apps/api
NestJS] SCH[apps/scheduler
Temporal worker] REG[ConnectorRegistry
packages/connector-*] end subgraph Datos PG[(PostgreSQL
RLS por colectivo)] RD[(Redis
estado OAuth 10 min)] V[Vault
AES-256-GCM + KEK] S3[(S3 / R2
medios cifrados)] end subgraph Medios MW[apps/media-worker
FFmpeg] PT[PeerTube
tube.occupymedia.cc] end subgraph Redes MA[Mastodon API] IG[Graph API
Instagram] TG[Telegram Bot API] end W -->|HTTPS + sesión| API API --> PG API --> RD API --> V V --> PG API -->|connect / identity| REG SCH -->|publish| REG SCH --> PG REG -->|providerFetch
control de salida| MA REG --> IG REG --> TG MW --> S3 MW -->|RTMP| PT ``` Conectar una cuenta (OAuth con `state` de un solo uso y PKCE donde la red lo admite): 1. `POST /connections/authorize`: la API guarda en Redis, cifrado, el estado de la petición y devuelve la URL de la red. 2. La persona acepta en la red y la red vuelve a `GET /connections/callback`. 3. La API comprueba `state`, cookie y permisos, canjea el código y guarda el token en el vault. ## Estado por red | Red | Conectar | Publicar | Notas | |---|---|---|---| | Mastodon | Sí | Texto e imágenes (con texto alternativo) | Sin claves de servidor; cada cuenta registra la app en su instancia. | | Telegram | Sí | Texto e imágenes | Token del bot de BotFather. | | Instagram | Sí, con `INSTAGRAM_CLIENT_ID/SECRET` | No todavía | Meta descarga la imagen de una URL pública y los medios están cifrados: falta un endpoint temporal. Ver [docs/oauth-connect.md](docs/oauth-connect.md). | | PeerTube | No | Directo sí | Falta el puente OAuth en tube.occupymedia.cc. | | X | No | No | Sin conector. API de pago por uso. | | TikTok | No | No | Fuera de alcance: la API exige auditoría y sin ella solo publica en privado. | ## Panel Outreach (mensajes privados) Lista de cuentas públicas (artistas, colectivos, medios) a las que escribir por privado para pedir difusión. Cada contacto tiene su mensaje, se puede rellenar con una plantilla (`{name}`, `{handle}`) y se marca enviado, respondido o rechazado. Se importa en CSV: ``` name,platform,handle,profileUrl,category,language Climáximo,bluesky,climaximopt.bsky.social,https://bsky.app/profile/climaximopt.bsky.social,clima,pt ``` El Hub **no envía** los mensajes. "Copy & open profile" copia el texto y abre el perfil; la persona lo pega y lo envía desde la propia red. Motivos: | Red | Abrir un privado por API | Problema | |---|---|---| | Instagram | No | La API solo deja responder a quien te escribió en las últimas 24 h. | | X | Sí, de pago | Mensajes en masa a desconocidos van contra sus normas de automatización: suspensión. | | Mastodon | Sí (mención privada) | Las instancias silencian o bloquean cuentas que lo hacen en masa; puede caer la instancia entera. | | Bluesky | Sí | Mismo criterio de spam; los chats de desconocidos suelen estar cerrados. | El contador de la pantalla avisa a partir de 20 primeros mensajes al día por colectivo. Repartidlos entre varias personas y días, y personalizad cada uno. ## Requisitos Medido en el despliegue actual (ver [docs/ops/2026-10-12-cloud-hosting-requirements.md](docs/ops/2026-10-12-cloud-hosting-requirements.md)). Los medios van a R2, así que el disco no crece con los vídeos. | Nivel | CPU | RAM | Disco | Para qué | |---|---|---|---|---| | Mínimo | 4 vCPU | 8 GB | 160 GB SSD | Todo el stack, transcodificación fuera de la máquina | | Recomendado | 8 vCPU | 16 GB | 240 GB SSD | Directos, transcodificación en la máquina, Collabora | | Nodo de render (opcional) | 4+ núcleos o GPU con NVENC | 8 GB | 50 GB | Se une por Tailscale; nunca guarda claves | Uso real hoy: unos 2,6 GB de RAM entre todos los servicios. Ancho de banda: | Uso | Necesario | |---|---| | Panel, publicación, APIs de redes | < 1 Mbps sostenido | | Un directo saliente (1080p30 hacia PeerTube) | 6 Mbps de subida por directo; 3 Mbps a 720p | | Espectadores | Los sirve Cloudflare desde la caché de HLS; el origen no escala con la audiencia | | Subidas de vídeo | Picos de lo que dé la línea; límite de 2 GiB por archivo | Coste orientativo: VPS de 30 €/mes + R2 de 1 a 5 $/mes. Cloudflare en plan gratuito. ## Red y puertos Entrada desde internet: **ninguna** directa. Todo entra por el túnel de Cloudflare; el nginx de borde solo acepta rangos de Cloudflare en 80/443. | Puerto | Servicio | Exponer a | |---|---|---| | 443/tcp | nginx de borde | Solo rangos de Cloudflare | | 80/tcp | nginx de borde (redirección) | Solo rangos de Cloudflare | | 3000/tcp | API | Solo 127.0.0.1 | | 3001/tcp | Web | Solo 127.0.0.1 | | 9000/tcp | PeerTube | Solo 127.0.0.1 o Tailscale | | 1935/tcp | RTMP de PeerTube | Solo Tailscale (worker de medios) | | 6167/tcp | Matrix (conduwuit) | Solo 127.0.0.1 | | 9200, 9980/tcp | oCIS, Collabora | Solo 127.0.0.1 | | 5432, 6379, 7233 | Postgres, Redis, Temporal | Nunca fuera de la red de Docker | Salida que hace falta: 443/tcp a las APIs de las redes y a R2, 7844/tcp+udp para `cloudflared`, 41641/udp para Tailscale. Docker publica puertos saltándose ufw. Para cerrarlos: publicar en `127.0.0.1:PUERTO` o en la IP de Tailscale, o añadir reglas en la cadena `DOCKER-USER`. ## Seguridad y monitorización Prácticas para cualquier despliegue. Los fallos de seguridad se comunican en privado a quien mantiene el código, no en issues públicas. - **Registro solo por invitación.** Un registro abierto da a cualquiera un colectivo propio y permisos de Owner sobre él. - **Un secreto por servicio.** La web y los workers no necesitan `SUPERPOSTER_KEK`, `S3_KEK` ni `SESSION_SECRET`. Claves distintas para el vault y para los medios. - **FFmpeg siempre en sandbox** (`NODE_ENV=production`, `MEDIA_SANDBOX=on`), sin red y sin variables de entorno heredadas. - **Cabeceras**: CSP estricta, HSTS, `X-Content-Type-Options`, `Referrer-Policy` en web y API. - **Límites de peticiones** en `/auth/*`, invitaciones, OIDC y subidas públicas, por `CF-Connecting-IP`. - **Metadatos fuera**: quitar EXIF/GPS de fotos y vídeos antes de guardarlos o publicarlos. - **Copias cifradas** (`age` o GPG) y fuera de la máquina; probar la restauración. - **Auditoría inalterable**: quitar `UPDATE`/`DELETE` sobre `audit_log` al rol `sp_app` y anclar fuera la cabeza de la cadena. Qué vigilar (Prometheus + Grafana ya están en `infra/`; Grafana nunca con acceso anónimo): | Señal | Alerta si | |---|---| | `401`/`403` en `/auth/login` por IP | > 20 en 5 min | | Registros nuevos / invitaciones canjeadas | Cualquiera fuera de lo previsto | | Eventos `audit_log`: `member.*`, `connection.removed`, `group.deleted`, `outreach.removed` | Picos o actores raros | | `connections` en estado `needs_credentials` | Cualquiera: token revocado o caducado | | Errores 5xx de la API | > 1 % durante 5 min | | Memoria de API y workers | > 80 % | | Cola de publicación `failed`/`unknown` | Cualquiera | | Logins al panel de Cloudflare y cambios de DNS | Siempre | ## Desarrollo ```bash corepack enable pnpm install pnpm exec turbo run build typecheck --filter=@tactical-media-broadcast-hub/api... pnpm --filter @tactical-media-broadcast-hub/connector-instagram test ``` Los tests de integración de `apps/api` que usan Postgres se saltan si no hay base de datos configurada. --- ## Upstream: Tactical Media Broadcast Hub Self-hosted multi-tenant social / video / livestream management platform with a generic connector system and a Virtual Channel / Auto Director subsystem. Built for collectives that need to coordinate media across platforms without concentrating access in one person's hands. Every credential lives in an encrypted vault. Every editorial decision lands in a tamper-evident audit log. Every feature is multi-tenant with row-level security. ### Features - **Publishing pipeline** — drafts → group approval → schedule → fenced dispatch → receipts. Per-connection publish governance: `auto`, one-admin, majority or consensus sign-off per account — boosts included. Multi-platform fan-out (Mastodon, Telegram, Bluesky, PeerTube, Facebook, Instagram, Threads, LinkedIn, Reddit, Twitch, Discord, Slack). DST-proof scheduling. - **Files SSO** — the Hub is the OIDC issuer for oCIS Drive: one sign-in, auto-provisioned personal drives, rotating refresh tokens that die with your Hub session (logout propagates). - **Column wall** — live multi-platform feed columns, TweetDeck-style. Safe rendering (hostile-HTML tested). Per-source cooldown + adaptive backoff. - **Live Control Room** — browser multiview, TAKE/CUT, preview/program, lower-thirds, scrolling chyrons, audio mixing, tally. - **Auto-Director** — priority rules switch sources automatically so the stream never dies mid-action. - **Chat governance** — bridged chats (Telegram/Twitch/Discord) into one overlay. Polls with configurable quorum. Approval gates on every post. Machine-readable reason codes. - **Distributed transcode workers** — laptops and GPUs join as render workers over Tailscale. Per-job sandboxed FFmpeg (UID/seccomp/no-network). - **Archive** — content-addressed encrypted S3, dedup, full-text search, CSV/JSON export. Deleted platform content stays citable. - **Analytics** — month-partitioned snapshots, current + previous month with deltas, per-provider breakdown. ### Quickstart ```bash pnpm install pnpm lint && pnpm typecheck && pnpm build # infrastructure docker compose -f infra/docker-compose.yml up -d postgres redis temporal minio otel-collector prometheus grafana # everything incl. media stub + mock live source + mock RTMP sink docker compose -f infra/docker-compose.yml --profile dev up -d # apps pnpm dev # api :3000, web :3001(adjust if colliding), scheduler, media-worker :3100 curl localhost:3000/healthz curl localhost:3000/readyz ``` ### Documentation | Section | Path | |---|---| | Engineering guide | [`docs/engineering-guide.md`](docs/engineering-guide.md) | | Admin guide | [`docs/admin-guide.md`](docs/admin-guide.md) | | User manual | [`docs/user-manual-v2.md`](docs/user-manual-v2.md) | | Design docs | [`docs/design/`](docs/design/) | | Streaming architecture | [`docs/design/streaming-architecture.md`](docs/design/streaming-architecture.md) | | Deployment (occupymedia.cc) | [`infra/occupymedia/DEPLOY.md`](infra/occupymedia/DEPLOY.md) | ### Deterministic live-source testing ```bash pnpm --filter @tactical-media-broadcast-hub/media-worker soak -- --hours 8 --out ``` ### Security & trust model Self-hosted, end to end — no corporate cloud, no accounts needed to run the system itself. Multi-tenant with row-level security; platform credentials in an encrypted vault the database never sees in plaintext; every editorial action in a hash-chained audit log; anonymous-infrastructure friendly (the host carries boxes, not content — storage is ciphertext).