OASIS_LINUX/docs/devs/architecture/02-arquitectura-software.md
SITO ddd2787b73 Oasis 0.9.5 con Karvan para escritorio
Rama de desarrollo experimental sobre el Oasis de epsylon, sin afiliacion con el
proyecto original. Base: 0.9.5.

Trae el modulo Karvan —salas efimeras en memoria con autodestruccion por TTL, chat
que funciona sin JavaScript y llamadas de audio y video sobre WebRTC— con el enlace
en el menu lateral por el mismo patron que el resto de modulos: renderKarvanLink
calcado de renderPollsLink, entrada en modules_view y en la lista de /modules.

Los estilos del modulo van en su propia hoja, styles/karvan.css, siguiendo el patron
de highlight.css: asi se ve igual con cualquier tema, sin depender del tema movil.

Sin servidores ICE de terceros: no hay STUN de fabrica, porque un STUN aprende la IP
publica y el momento de cada llamada. Solo candidatos host salvo que se configure un
TURN propio en oasis-config.json, con credenciales efimeras por el mecanismo REST de
coturn.

El codigo propio vive separado —views/fork/, backend/fork_routes.js,
models/karvan_model.js y translations/fork/— de modo que sobre los ficheros de
upstream solo hay enganches de una linea y cada version nueva se integra sin
arrastrar nada.

Respecto al arbol de Android: fuera el wrapper y main.js, que es su arranque; el
tema vuelve a Dark-SNH y se restauran los modulos que en movil se recortan por peso.

Verificado arrancando sin OASIS_MOBILE: nueve rutas OK, menu lateral de upstream con
27 grupos y el enlace de Karvan, cero rastro de la interfaz movil (ni hexagonos, ni
topbar, ni barra inferior), y una sala creada y servida.
2026-08-19 00:35:08 +02:00

350 lines
14 KiB
Markdown

# 2. Arquitectura de software del backend
Oasis usa un patrón **MVC casero** (sin framework MVC):
```
src/backend/backend.js → el ROUTER monolítico (Koa, ~7250 líneas, ~472 rutas)
src/models/*_model.js → la lógica de DATOS (lee/escribe SSB)
src/views/*_view.js → el RENDER de HTML (con hyperaxe)
```
Flujo de una funcionalidad:
```
navegador → backend.js (ruta) → xModel.metodo() (SSB) → xView(data) (HTML) → ctx.body
```
---
## 2.1 Stack de software (dependencias clave)
Todas las dependencias están en `src/server/package.json`. Importante: casi
todos los `require` usan **ruta relativa explícita** al node_modules del
servidor, p.ej. `require("../server/node_modules/@koa/router")`.
**Capa web / HTTP**
- **`koa`** — servidor HTTP base. Se instancian varias apps Koa en
`src/client/middleware.js` (principal + sub-apps para estáticos).
- **`@koa/router`** — enrutado. `backend.js:442` instancia
`const router = new koaRouter()`. Toda la API es **una única cadena fluida**
`router.param(...).get(...).post(...)` (líneas ~1270-7124).
- **`koa-static` / `koa-mount`** — sirven assets estáticos (`/assets`,
`/maptiles`, `/game-assets`, `/js`...) montados en `middleware.js`.
- **`koa-body`** — parseo de `multipart/form-data` y POST. Se aplica por ruta:
`.post('/votes/create', koaBody(), ...)`.
- `express` y `cors` figuran como dependencias pero **el flujo principal es
Koa**; Express no se usa en el router.
**Motor de plantillas (vistas)**
- **`hyperaxe`** — hyperscript → HTML. **No hay archivos `.html`**; el HTML se
construye programáticamente desde JS: `div(...)`, `nav(...)`, `form(...)`...
Cada elemento expone `.outerHTML`.
- **`ssb-markdown` + `ssb-msgs` + `ssb-ref`** — render de Markdown SSB con
resolución de menciones/enlaces internos (`src/views/markdown.js`).
- **`highlight.js`, `qrcode`, `moment`, `pretty-ms`** — sintaxis, QR, fechas.
- **`dompurify` + `jsdom` + `is-svg`** — saneado anti-XSS
(`src/backend/sanitizeHtml.js`).
**Streams SSB**
- **`pull-stream`** — el paradigma de streams de SSB. Es el mecanismo central de
lectura: `pull(source, pull.filter(...), pull.collect(cb))`.
- Auxiliares: `pull-paramap` (map asíncrono), `pull-sort`, `pull-abortable`
(cancelar streams `live`), `pull-cat`, `pull-many`, `pull-pushable`.
**Cliente / servidor SSB**
- **`ssb-client`** — cliente RPC muxrpc para conectar al sbot (en `gui.js`).
- **`ssb-config`, `ssb-keys`, `ssb-ref`** — config, claves ed25519, validación
de identificadores (`@feed`, `%msg`, `&blob`).
- **`secret-stack` + `ssb-caps` + `ssb-db`** y los plugins SSB → ver
[01-red-ssb.md](01-red-ssb.md).
**Otros**: `module-alias`, `lodash`, `minimist`, `open`, `archiver`,
`file-type`, `pdfjs-dist`, `openpgp`, `axios`, `node-llama-cpp` +
`@xenova/transformers` (módulo IA local).
---
## 2.2 Flujo de una petición
**a) Construcción del servidor**`src/client/middleware.js`: la función
exportada `({host, port, middleware, allowHost})` crea la app Koa, monta
estáticos, fija cabeceras de seguridad (CSP, `X-Frame-Options`...), valida el
host, aplica los middlewares recibidos y finalmente `app.listen({host, port})`
(línea 180). En `backend.js:7239`:
`const app = http({ host, port, middleware, allowHost: config.allowHost })`.
**b) Cadena de middleware**`backend.js:7126-7238`: incluye (1) bloqueo de
no-GET en modo público, (2) `setLanguage(...)` por cookie, (3) gate de
indexación (muestra `indexingView` si la base no está sincronizada), (4) refresco
de estado compartido cada 60 s, y por último `routes` (las rutas del router).
**c) Ruta GET — ejemplo agenda** (`backend.js:1977-1982`):
```js
.get('/agenda', async (ctx) => {
const filter = qf(ctx);
let data = await agendaModel.listAgenda(filter); // MODELO: lee SSB
if (Array.isArray(data)) data = await applyListFilters(data, ctx);
ctx.body = await agendaView(data, filter); // VISTA: genera HTML
})
```
`agendaModel.listAgenda` abre el cliente (`cooler.open()`), lee el log
(`createLogStream`), agrega ítems y devuelve datos. `agendaView` los recibe y
construye HTML con hyperaxe, envuelto en `template(...)`. El HTML se asigna a
`ctx.body` y Koa lo sirve.
**d) Ruta POST — ejemplo votos** (`backend.js:5572`):
```js
.post('/votes/create', koaBody(), async ctx => {
const b = ctx.request.body;
const parsedOptions = b.options ? b.options.split(',')... : defaultOptions;
await votesModel.createVote(stripDangerousTags(b.question), b.deadline, parsedOptions, ...);
ctx.redirect(safeReturnTo(ctx, '/votes?filter=mine', ['/votes']));
})
```
El POST **sanea** la entrada (`stripDangerousTags`), llama al modelo que
**publica un mensaje SSB**, y **redirige** (patrón Post/Redirect/Get). El GET
posterior vuelve a leer del modelo y renderiza.
---
## 2.3 Conexión al servidor SSB: el "cooler"
El **cooler** es la abstracción que gestiona la conexión al sbot. Se define en
`src/client/gui.js` (importado en `backend.js:441` como
`const ssb = require("../client/gui")`) y se instancia en `backend.js:540`:
```js
const cooler = ssb({ offline: config.offline, port: config.port,
host: config.host, isPublic: config.public });
```
- Si existe un sbot **en el mismo proceso** (`internalSSB`), `cooler.open()` lo
devuelve directamente sin RPC.
- Si no, conecta por muxrpc vía socket Unix `noauth` con backoff exponencial.
**Patrón de uso en todo el código**:
`const ssb = await cooler.open();` y luego se usan los métodos muxrpc:
| Método | Para qué |
|---|---|
| `ssb.createUserStream({id, reverse, limit})` | feed de un usuario |
| `ssb.createLogStream({reverse, limit})` | log global (lo más usado) |
| `ssb.query.read({query:[{$filter:{...}}]})` | consultas ssb-query |
| `ssb.messagesByType({type, private})` | mensajes por tipo |
| `ssb.backlinks.read({query})` | referencias/backlinks |
| `ssb.get(id, cb)` | un mensaje por id |
| `ssb.publish(content, cb)` | **publicar un mensaje** |
| `ssb.blobs.want/get/add` | blobs |
| `ssb.friends.isFollowing/isBlocking/graph` | grafo social |
| `ssb.conn.peers / ssb.status() / ssb.progress()` | estado de red |
---
## 2.4 Patrón de un MODELO
Todos los modelos siguen la **factory pattern**:
`module.exports = ({ cooler, isPublic, ...deps }) => ({ ...métodos })`. Reciben
el `cooler` por inyección desde `backend.js` (líneas 553-602). Muchos cachean el
cliente:
```js
let ssb;
const openSsb = async () => { if (!ssb) ssb = await cooler.open(); return ssb; };
```
**Publicar (escribir)**`ssb.publish(content, cb)` con un campo `type`.
Ejemplo `votes_model.js` (`createVote`):
```js
const content = {
type: 'votes', question, options, deadline, createdBy: userId,
status: 'OPEN', votes: {...}, totalVotes: 0, createdAt: new Date().toISOString()
};
return new Promise((res, rej) =>
ssbClient.publish(content, (err, msg) => err ? rej(err) : res(msg)));
```
**Leer**`pull-stream` + `collect`:
```js
pull(
ssbClient.createLogStream({ limit: logLimit }),
pull.collect((err, results) => err ? reject(err) : resolve(results))
);
```
Luego se procesa en memoria: se construye un índice, se filtra por `type`, se
ordena.
**Borrar / editar — modelo de tombstone** (el log es *append-only*: nada se
borra físicamente):
- **Borrado lógico**: se publica `{ type:'tombstone', target:<msgId>, deletedAt,
author }`.
- **Validación** (`src/models/tombstone_validator.js`,
`buildValidatedTombstoneSet`): solo acepta el tombstone si **el autor del
tombstone coincide con el autor del mensaje objetivo** — evita borrados por
terceros.
- **Edición**: se publica una versión nueva con `replaces: <idAnterior>` y se
lapida la anterior, formando una cadena de versiones; al leer se sigue la
cadena hasta la "punta".
- Las comprobaciones de autoría (`if (c.createdBy !== userId) throw`) se hacen en
el modelo.
**Tipos de mensaje SSB** (campo `content.type`): los nativos SSB `post`, `vote`
(likes), `contact` (follow/block), `about` (perfil), y los **propios de Oasis**:
`votes`, `tombstone`, `task`, `event`, `calendar`, `transfer`, `tribe`,
`market`, `report`, `job`, `project`, `gameScore`, etc.
> **`main_models.js`** es el modelo núcleo: exporta `about`, `blob`, `friend`,
> `meta`, `post`, `vote`, `spreads`, `lifetime`. Su función `transform()`
> enriquece cada mensaje con metadatos (autor, avatar, votos, timestamps) antes
> de pasarlo a las vistas.
---
## 2.5 Patrón de una VISTA
Las vistas exportan funciones que reciben datos del modelo y devuelven HTML.
Patrón típico:
```js
const { div, h2, p, section, button, form } = require("../server/node_modules/hyperaxe");
const { template, i18n, userLink } = require('./main_views');
exports.agendaView = async (data, filter) =>
template(i18n.agendaTitle, section( h2(i18n.agendaTitle), /* ... */ ));
```
**Layout común — `main_views.js`**:
- `template(titlePrefix, ...elements)` construye el documento completo:
`html → head (title, CSS, favicon) → body (header, sidebar de navegación,
main-content)`. Se exporta como `exports.template`.
- **Navegación**: helper `navLink({href, emoji, text, current})` genera cada
`<li><a>`. `navGroup({id, emoji, title})` agrupa enlaces en secciones
colapsables del menú lateral. Cada módulo tiene su `renderXxxLink()` que solo
emite el enlace si el módulo está activo en config.
- **i18n**: `i18n` es un objeto compartido importado por todas las vistas.
`setLanguage(lang)` lo muta in-place (11 idiomas:
`en, es, fr, eu, de, it, pt, zh, ar, hi, ru` en
`src/client/assets/translations/`). El middleware llama `setLanguage(...)` en
cada petición. Las cadenas se referencian como `i18n.agendaTitle`, con
fallback `i18n.x || 'Default'`. **Nunca texto literal en las vistas.**
- **Temas**: `const theme = currentConfig.themes.current || "Dark-SNH"` →
`<link href="/assets/themes/${theme}.css">`.
- **Helpers reutilizables** exportados por `main_views`: `userLink`,
`renderStateChip`, `renderVisibilityChip`, `renderSpreadButton`, `errorView`,
`formatCarbon`, `markdown(input, mentions)`.
---
## 2.6 Configuración
**`src/configs/config-manager.js`** — gestor central de `oasis-config.json`:
- Si el archivo no existe, escribe un `defaultConfig`.
- `getConfig()` lee y parsea el JSON **en cada llamada** (sin caché → cambios en
caliente).
- `saveConfig(newConfig)` reescribe el archivo.
Qué controla **`oasis-config.json`**:
- `themes.current` — tema CSS activo.
- `ux.current` — modo de interfaz (`"blocks"` o `"ainav"`).
- **`modules`** — ~50 flags `<nombre>Mod: "on"|"off"` que activan/desactivan cada
módulo. En backend se comprueba con `checkMod(ctx, mod)` (líneas 170-177): si
está `off`, redirige a `/modules`.
- `wallet`/`walletPub`, `ai.prompt`, `ssbLogStream.limit` (límite de mensajes
leídos del log), `homePage`, `language`, `wish`, `pmVisibility`,
`lanBroadcasting`.
**`src/configs/shared-state.js`** — estado en memoria (no persistente):
contador de inbox, huella de carbono, nº de peers online, valor eco, etc.
Lo actualiza el middleware periódico y lo consume `template`.
---
## 2.7 Tabla de módulos (modelo ↔ vista)
| Modelo | Vista | Notas |
|---|---|---|
| `main_models.js` | `main_views.js` | Núcleo: layout, i18n, temas, about/blob/friend |
| `activity_model` | `activity_view` | Feed de actividad |
| `agenda_model` | `agenda_view` | Agrega tasks/events/transfers/market/... |
| `audios_model` | `audio_view` | |
| `banking_model` | `banking_views` | ECOin, epochs |
| `blockchain_model` | `blockchain_view` | |
| `bookmarking_model` | `bookmark_view` | |
| `calendars_model` | `calendars_view` | Cifrado |
| `chats_model` | `chats_view` | Cifrado |
| `cipher_model` | `cipher_view` | |
| `courts_model` | `courts_view` | |
| `cv_model` | `cv_view` | |
| `documents_model` | `document_view` | |
| `events_model` | `event_view` | |
| `favorites_model` | `favorites_view` | Agrega favoritos |
| `feed_model` | `feed_view` | |
| `forum_model` | `forum_view` | Cifrado |
| **`games_model`** | **`games_view`** | Hall of fame + shell de juegos |
| `images_model` | `image_view` | |
| `inhabitants_model` | `inhabitants_view` | Perfiles |
| `jobs_model` | `jobs_view` | |
| `larp_model` | `larp_view` | |
| `legacy_model` | `legacy_view` | |
| `logs_model` | `logs_view` | |
| `maps_model` | `maps_view` | Cifrado |
| `market_model` | `market_view` | |
| `melody_model` | `melody_view` | |
| `opinions_model` | `opinions_view` | |
| `pads_model` | `pads_view` | Cifrado |
| `parliament_model` | `parliament_view` | |
| `pixelia_model` | `pixelia_view` | |
| `pm_model` | `pm_view` | Mensajería privada |
| `projects_model` | `projects_view` | |
| `reports_model` | `report_view` | |
| `search_model` | `search_view` | |
| `shops_model` | `shops_view` | |
| `stats_model` | `stats_view` | |
| `tags_model` | `tags_view` | |
| `tasks_model` | `task_view` | |
| `torrents_model` | `torrents_view` | |
| `transfers_model` | `transfer_view` | |
| `trending_model` | `trending_view` | |
| `tribes_model` + `tribes_content_model` | `tribes_view` | Cifrado |
| `videos_model` | `video_view` | |
| `votes_model` | `vote_view` | |
| `wallet_model` | `wallet_view` | |
| (sin modelo) | `AI_view` | Lógica en `src/AI/*` |
| `crypto.js`/`tribe_crypto.js` | — | Cifrado simétrico por dominio |
---
## 2.8 Mapa de dependencias entre archivos
**`backend.js` → modelos** (líneas 541-602): importa `main_models` y **todos**
los `*_model.js` como factories invocadas con `{ cooler, isPublic, ...deps }`.
Hay **inyección de dependencias entre modelos**: p.ej. `agenda_model` recibe
`{calendarsModel, eventsModel, tasksModel, marketModel, jobsModel,
projectsModel}`; `favorites_model` recibe casi todos los modelos de contenido.
**`backend.js` → vistas** (líneas ~1162-1216 + `require` inline): importa las
funciones de render de cada `*_view.js`. Algunas se cargan diferidas dentro de
los handlers (`require('../views/...')`).
**Modelos → importan**: `pull-stream`, `moment`, `./tombstone_validator`,
`../configs/config-manager`, helpers, y reciben `cooler` por parámetro (no
importan `gui` directamente). `tombstone_validator.js` no tiene dependencias
(función pura).
**Vistas → importan**: hyperaxe, `{template, i18n, userLink, ...}` de
`main_views`, `moment`, y a veces `../server/SSB_server` para obtener
`config.keys.id` (el feedId propio).
**Estado compartido**: `config-manager.js` (lee/escribe `oasis-config.json`) y
`shared-state.js` (memoria) son importados por backend, modelos y vistas; son
los puntos de coordinación transversal. `nameCache.js` (un Map en memoria) lo
alimenta `main_models` y lo consume `main_views`.
Siguiente: **[03-modulos-y-juegos.md](03-modulos-y-juegos.md)**.