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.
14 KiB
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 ensrc/client/middleware.js(principal + sub-apps para estáticos).@koa/router— enrutado.backend.js:442instanciaconst router = new koaRouter(). Toda la API es una única cadena fluidarouter.param(...).get(...).post(...)(líneas ~1270-7124).koa-static/koa-mount— sirven assets estáticos (/assets,/maptiles,/game-assets,/js...) montados enmiddleware.js.koa-body— parseo demultipart/form-datay POST. Se aplica por ruta:.post('/votes/create', koaBody(), ...).expressycorsfiguran 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 streamslive),pull-cat,pull-many,pull-pushable.
Cliente / servidor SSB
ssb-client— cliente RPC muxrpc para conectar al sbot (engui.js).ssb-config,ssb-keys,ssb-ref— config, claves ed25519, validación de identificadores (@feed,%msg,&blob).secret-stack+ssb-caps+ssb-dby los plugins SSB → ver 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):
.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):
.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:
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
noauthcon 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:
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):
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:
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.jses el modelo núcleo: exportaabout,blob,friend,meta,post,vote,spreads,lifetime. Su funcióntransform()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:
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 comoexports.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 surenderXxxLink()que solo emite el enlace si el módulo está activo en config. - i18n:
i18nes 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, ruensrc/client/assets/translations/). El middleware llamasetLanguage(...)en cada petición. Las cadenas se referencian comoi18n.agendaTitle, con fallbacki18n.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 concheckMod(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.