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.
350 lines
14 KiB
Markdown
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)**.
|