# RisaLiberada v1 — Standard del circuito (subir · moderar · escuchar)

**Fecha:** 2026-08-11
**App:** Risa Liberada (`projects/liberada/risa/`)
**Bot:** @RisaLiberadaBot · **Canal público:** «Banco de la risa»
**Web en vivo:** [risa.liberada.net](https://risa.liberada.net) · **Réplica del sistema:** sección «Replica Nuestro Sistema» de la web (pestaña **Ciencia**), con las versiones *Agentes* (prompt copiable), *Introducción* (flujo paso a paso) y *Desarrollo* (v2 + v3)
**Estado:** estándar v1 — **estable** (base de recuperación; el polish llega después de flove v2, D20)
**Evoluciona a:** `../debates/making-of/superpowers/specs/2026-07-19-risa-banco-telegram-moderation-design.md` · `../debates/making-of/superpowers/plans/2026-08-11-risa-authy-telegram-shared.md` (v2)
**Diagramas:** `mermaid/` (risaliberada-v1-thread · risaliberada-v1-flow · flove-v2-thread · flove-v2-flow) — `.mmd` para Excalidraw, `.png` (3×) y `.svg` (vector, nítidos)

---

## 1. Qué es

Un circuito para que **la gente aporte su risa** y **un grupo de moderadores la publique o la descarte**, sin servidor que mantener. Telegram es la herramienta de tres caras — **subir · moderar · escuchar** — y la web es el escaparate público.

## 2. Superficies y roles

| Rol | Cómo entra | Qué puede hacer |
|-----|-----------|-----------------|
| **Persona que aporta** | DM a @RisaLiberadaBot | Pulsar START, enviar nota de voz/audio, elegir identidad, etiquetas |
| **Moderador/a** | Miembro del **grupo privado de moderación** | Ver la cola, tocar **✅ Publicar / 🗑 Borrar** |
| **Público** | Web (`comunidad.html`) · canal «Banco de la risa» | Escuchar el banco |

La subida es **abierta pero identificada**: cualquiera aporta, pero Telegram da una identidad verificada que frena el spam y da a los moderadores un contacto.

## 3. Workflow — como hilo

### Hilo A · Subir (persona ↔ bot)

1. La persona entra al bot por **deep link** (`t.me/RisaLiberadaBot`, botón «💬 Comparte tu risa» de la web) o **QR** del modal «cómo aportar».
2. **(Primera vez)** pulsa el botón **START / Iniciar**.
3. El bot muestra el **aviso de licencia**: *«Al enviar tu risa la publicas en libre, bajo CC BY-SA 4.0, atribuida al nombre que elijas.»* **Enviar = consentir.**
4. La persona envía su **nota de voz / audio**.
5. El bot pregunta **identidad** — 3 opciones:
   - **① ID Telegram** — el id de Telegram. Solo: se muestra directo. Combinado con ②: se oscurece (hash).
   - **② Nombre público** — la persona elige un **nombre** a mostrar (palabra).
   - **③ Anónimo** — sin nombre («Anónima»).
   - **Combinación ①+②:** el Nombre público **oscurece** el ID — se guarda solo un **hash salado** como handle; el id en claro nunca viaja. *No es claim (eso es v2).*
6. El bot pregunta **etiquetas** (opcional, separadas por coma). Default: **`libre`**.
7. **Confirmación.** ⚠️ *Copy exacto pendiente de Marc:* el spec provisional decía «¡Recibida! 💛 En revisión, pronto en el banco.», pero Marc confirma que el mensaje real es **otro**. Hay que fijar el texto definitivo antes de implementar el bot.
8. El siguiente **cron** recoge el clip → lo mete en `queue.json` (con `file_id`, sin descargar aún) → lo reenvía al **grupo privado de moderación**.

### Hilo B · Moderar (bot ↔ grupo privado)

1. El clip llega con audio reproducible + palabra + tags (+ enlace opt-in) y botones inline **✅ Publicar / 🗑 Borrar** (el `callback_data` lleva el id de la cola).
2. Un moderador toca un botón; el siguiente cron recoge el `callback_query`.
   - **✅ Publicar:** descarga el `file_id` → `ffmpeg` → mp3 normalizado → sube a **Cloudflare R2** (`risa/b_*.mp3`) → `src` = URL R2 pública → **prepend** de la entrada a `risa.json` (newest-first) → commit + push → post del audio al **canal público «Banco de la risa»** → edita el mensaje del grupo a «✅ Publicado por @moderador».
   - **🗑 Borrar:** quita el clip de `queue.json`, edita el mensaje a «🗑 Borrado por @moderador».
3. Se actualiza `offset.txt`.

> El handle del moderador vive **solo** en el grupo privado; nunca llega a la web.

### Hilo C · Escuchar (público)

- **Web:** `fetch` de `risa.json` → **6ª playlist «Banco de la risa»** (mismo renderizador que las otras cinco) + feed **«Últimas risas»** = últimos N reales (`slice(0,N)`). Estados **vacío** («aún no hay risas, sé la primera 💛») y **error de red**. **Tags → chips**. Nombre = **palabra**; con **①+②** el hash habilita el enlace a `t.me/<usuario>` (opt-in `tg_public`); con **solo-①** se muestra el ID directo.
- **Telegram:** el canal acumula cada clip aprobado como audio reproducible; se escucha con scroll.

## 4. Identidad y privacidad

- La **palabra sobrescribe** a cualquier identidad de Telegram en el display. Si el autor no elige palabra → **«Anónima»**; **jamás** se infiere del @username ni del nombre de Telegram.
- **ID Telegram:** en modo **①+②** solo viaja un **hash salado** (handle de autor); en modo **solo-①** viaja el id directo. El `from_user` real y el @username viven **solo** en: DM del autor · grupo privado · cabecera del bot. El id en claro **nunca** en `risa.json` ni en la web.
- **Opt-in ① Telegram:** la única vía de que la identidad tg aparezca en la web; la decide el autor, y solo como **enlace** (o id directo si eligió solo-①).
- **Fuga por tags:** bloqueada — las etiquetas son input del autor o default `libre`; nunca derivadas de la identidad tg.
- `queue.json` (commit) lleva **solo** el id oscurecido: `idHash` si ①+②, `idDirect` si solo-①, nada si ③/②. Salt del hash: `TG_ID_SECRET` (env). El id en claro para el DM de publicación vive en `state/.uploaders.json` (gitignored, local) y `state/drafts.json` tampoco se commitea.

## 5. Modelo de datos (repo `floveorg/risa`)

- **`state/offset.txt`** — último `update_id` de Telegram procesado (idempotencia: no avanza sin commit → reprocesa, nunca pierde).
- **`state/queue.json`** — pendientes (commit; nunca el id en claro):
  ```json
  [{ "id": "q_ab12", "fileId": "<telegram file_id>",
     "idHash": "<hash salado sha256 si ①+②>", "idDirect": "<id si solo-①>",
     "name": "palabra", "tags": "libre", "sel": { "tg": true, "name": true, "anon": false },
     "modMsgId": 678 }]
  ```
- **`state/.uploaders.json`** — gitignored, local: `{ "<clip_id>": <chat_id en claro> }`, para el DM de publicación; se borra al publicar/borrar.
- **`risa.json`** — publicados, **newest-first** (el bot **prepend**):
  ```json
  [{ "id": "b_9f3", "name": "palabra", "t": "Risa de palabra", "src": "https://…r2.dev/risa/b_9f3.mp3",
     "tags": "libre", "when": "2026-08-11", "channel_msg": 42, "tg_public": false }]
  ```

**Contrato F0 (`banco.js`):** el bot escribe `name` + `src` + opcionales (`t`, `tags`, `when`, `tg_public`). **NO** escribe `by` ni `orig`: la web los **compone** desde la licencia constante — `by = "<name> · CC BY-SA 4.0"`, `orig = deed`. Licencia fuera de los datos; un solo sitio.

## 6. GitHub (topología y despliegue)

- **Repo dedicado `floveorg/risa`** (GitHub), separado del repo fuente de flove (Gitea) y del sitio publicado (`floveorg/floveorg.github.io`). Contiene: el workflow del bot, `state/`, `risa.json` y el script de bot. Se sirve por su **propio GitHub Pages**.
  - Aísla binarios del repo principal · concentra el secret en un solo sitio · **no toca** el pipeline `updaty-web`.
  - La web hace `fetch` de `risa.json` desde ese origen (CORS permisivo); `<audio>` cross-origin no necesita CORS.
- **GitHub Actions (cron ~5–10 min):** `getUpdates` → procesa → commit+push. El runner trae `ffmpeg`.
  - **Secrets:** credenciales de Cloudflare R2 (`R2_ACCESS_KEY_ID`, `R2_SECRET_ACCESS_KEY`, `R2_ENDPOINT`, `R2_BUCKET`) + token del bot de Telegram.
  - **Latencia:** un clip aparece/publica dentro de un ciclo (~5–15 min). Aceptable para risas.
  - **Fiabilidad:** cron gratuito *best-effort*; los workflows se pausan tras 60 días sin actividad — cualquier push los reactiva.
  - **Estado:** `offset.txt` en el repo; procesamiento **idempotente por `id`**.
  - **Lógica como funciones puras** (parseo → acciones; añadir a `risa.json`; gestión de offset): migrar a VPS/webhook futuro es *drop-in*, no reescritura.

## 7. Hosting del audio — Cloudflare R2

- Cada clip aprobado se sube a **Cloudflare R2** (bucket `risa`, prefijo `risa/b_*.mp3`), con **URL pública** que se guarda en `src`.
- **Por qué R2 (mejor que Cloudinary):** objeto storage tipo S3 de Cloudflare — **sin coste de egress**, URLs públicas listas para reproducir, sin dependencia de CDN propietario, misma espiritu FOSS que el resto. Sustituye a la enmienda Cloudinary (2026-07-19).
- `src` es una URL simple → **cambiar de host es una línea** (Backblaze B2 / MinIO autoalojado si hiciera falta).

## 8. Licencia y anti-abuso

- **Licencia:** cada clip bajo **CC BY-SA 4.0**. El bot lo declara antes de aceptar; la web compone `by` desde la constante.
- **Anti-abuso:** solo mensajes de **voz/audio**; duración máx (~30 s) y tamaño máx; límite de frecuencia por `from_user`; los moderadores son la puerta final. (Valores exactos: pendientes de fijar.)

## 9. Pendientes del estándar

- [ ] **Copy de confirmación del bot** (Marc) — reemplaza el provisional.
- [ ] Duración/tamaño máx exactos + política de límite de frecuencia.
- [ ] Lenguaje del bot (Python `python-telegram-bot` vs Node `grammY`).
- [ ] Confirmar repo dedicado vs carpeta preservada en el sitio.
- [ ] Sincronizar `banco.js`: default de tags → **`libre`**.
