fuera Karvan: se usan el Telefono y las Salas del dev
El dev ya trae Telefono (llamadas uno a uno, buzon de voz) y Salas (hasta 50, con video, grabacion y emision). La rama quita su modulo propio de salas y llamadas: modelo, vista, JS de cliente, CSS, rutas, credenciales TURN, pruebas y textos. - el router propio pasa a llamarse fork_routes.js, sin Karvan - el audio, la camara y la emision de Android se enchufan desde main.js con setAudioFactory/setVideoFactory/setStreamFactory, que exporta el propio dev; SSB_server.js vuelve a tener su linea original - Clear-SNH.css y modules_view.js vuelven a ser identicos a los del dev - el workflow acampada y la config de serie cambian Karvan por Telefono y Salas - README y guia: las llamadas son las del dev; antes de hacer un modulo propio, mirar si el dev ya lo tiene
This commit is contained in:
parent
3937ddab1e
commit
096a7d110b
30 changed files with 266 additions and 1918 deletions
|
|
@ -11,30 +11,20 @@ const wf = require('./workflows_model');
|
|||
|
||||
const FORK_WORKFLOWS = [
|
||||
{
|
||||
// una acampada: hablar, escribir juntos, compartir lo que pasa y decidir
|
||||
// (tribus = comisiones, parlamento y votaciones para la asamblea)
|
||||
// una acampada: hablar (chats, telefono y salas de voz del dev), escribir juntos,
|
||||
// compartir lo que pasa y decidir (tribus = comisiones, parlamento y votaciones)
|
||||
key: 'acampada',
|
||||
theme: 'Acampada',
|
||||
homePage: 'activity',
|
||||
modules: ['chats', 'karvan', 'pads', 'forum', 'images', 'videos', 'wiki', 'blogs', 'feed', 'invites', 'backup',
|
||||
modules: ['chats', 'phone', 'rooms', 'pads', 'forum', 'images', 'videos', 'wiki', 'blogs', 'feed', 'invites', 'backup',
|
||||
'tribes', 'parliament', 'votes', 'events']
|
||||
}
|
||||
];
|
||||
|
||||
// Karvan es un modulo de la rama: se anade a las listas del dev (todos los modulos,
|
||||
// los del movil y el workflow "default", que se calcula al cargar con una copia)
|
||||
const addKarvan = () => {
|
||||
if (!wf.ALL_MODULES.includes('karvan')) wf.ALL_MODULES.push('karvan');
|
||||
if (!wf.MOBILE_MODULES.includes('karvan')) wf.MOBILE_MODULES.push('karvan');
|
||||
const def = wf.WORKFLOWS.find(w => w.key === 'default');
|
||||
if (def && !def.modules.includes('karvan')) def.modules.push('karvan');
|
||||
};
|
||||
|
||||
let done = false;
|
||||
const register = () => {
|
||||
if (done) return;
|
||||
done = true;
|
||||
addKarvan();
|
||||
for (const w of FORK_WORKFLOWS) {
|
||||
if (wf.WORKFLOWS.some(x => x.key === w.key)) continue;
|
||||
wf.WORKFLOWS.push(w);
|
||||
|
|
|
|||
|
|
@ -1,198 +0,0 @@
|
|||
"use strict";
|
||||
// Módulo KARVAN (self-contained) para Oasis Mobile — inspirado en karvan-protocol
|
||||
// (packages/core/addons/ephemeral.js): salas EFÍMERAS en RAM que se AUTODESTRUYEN por TTL.
|
||||
// - Nada se escribe a disco. Los mensajes viven solo en memoria y desaparecen con la sala.
|
||||
// - TTL de inactividad (idle) que se resetea con cada actividad + TTL absoluto que nunca se resetea.
|
||||
// - Mailbox RAM para señalización WebRTC (data-channel) entre pares.
|
||||
// No depende de @karvan/core ni de cripto nativa: es una capa local sobre el backend de Oasis.
|
||||
|
||||
const crypto = require("crypto");
|
||||
|
||||
const DEFAULT_IDLE_TTL_MS = 30 * 60 * 1000; // 30 min sin actividad -> autodestrucción
|
||||
const DEFAULT_ABS_TTL_MS = 2 * 60 * 60 * 1000; // 2 h absoluto (tope)
|
||||
const MAX_ABS_TTL_MS = 24 * 60 * 60 * 1000; // nunca más de 24 h
|
||||
const MAX_MSGS = 250; // anillo: solo los últimos N en RAM
|
||||
const MAX_SIGNALS = 500;
|
||||
const MAX_ROOMS = 100;
|
||||
const MAX_MEMBERS = 50; // tope de miembros por sala (evita crecimiento sin límite del Set)
|
||||
const MAX_SIGNAL_BYTES = 16384; // SDP/ICE son pequeños; rechaza payloads gigantes (anti-DoS de memoria)
|
||||
|
||||
module.exports = ({ idleTtlMs = DEFAULT_IDLE_TTL_MS, absTtlMs = DEFAULT_ABS_TTL_MS } = {}) => {
|
||||
const rooms = new Map(); // id -> room
|
||||
|
||||
const publicRoom = (r) => ({
|
||||
id: r.id, title: r.title, createdAt: r.createdAt, expiresAt: r.expiresAt,
|
||||
count: r.messages.length, members: r.members.size,
|
||||
});
|
||||
|
||||
const resetIdle = (room) => {
|
||||
if (room.idleTimer) clearTimeout(room.idleTimer);
|
||||
room.idleTimer = setTimeout(() => destroy(room.id), idleTtlMs);
|
||||
if (room.idleTimer.unref) room.idleTimer.unref();
|
||||
};
|
||||
|
||||
const destroy = (id) => {
|
||||
const room = rooms.get(id);
|
||||
if (!room) return;
|
||||
clearTimeout(room.idleTimer);
|
||||
clearTimeout(room.absTimer);
|
||||
// AUTODESTRUCCIÓN: vaciar todo el material en RAM
|
||||
room.messages.length = 0;
|
||||
room.signals.length = 0;
|
||||
room.members.clear();
|
||||
room.remoteFeeds.clear();
|
||||
rooms.delete(id);
|
||||
};
|
||||
|
||||
const createRoom = ({ title = "", ttlMinutes } = {}) => {
|
||||
if (rooms.size >= MAX_ROOMS) {
|
||||
// desalojar la más antigua para no crecer sin límite
|
||||
const oldest = [...rooms.values()].sort((a, b) => a.createdAt - b.createdAt)[0];
|
||||
if (oldest) destroy(oldest.id);
|
||||
}
|
||||
const id = crypto.randomBytes(9).toString("hex"); // 18 hex
|
||||
const createdAt = Date.now();
|
||||
let absMs = absTtlMs;
|
||||
const t = parseInt(ttlMinutes, 10);
|
||||
if (Number.isFinite(t) && t > 0) absMs = Math.min(t * 60000, MAX_ABS_TTL_MS);
|
||||
const room = {
|
||||
id,
|
||||
title: String(title || "").slice(0, 80),
|
||||
createdAt,
|
||||
expiresAt: createdAt + absMs,
|
||||
messages: [],
|
||||
signals: [],
|
||||
members: new Set(),
|
||||
remoteFeeds: new Set(), // feeds SSB de otros nodos con esta sala (para relay cross-device por privados)
|
||||
seq: 0,
|
||||
sigSeq: 0,
|
||||
idleTimer: null,
|
||||
absTimer: null,
|
||||
};
|
||||
rooms.set(id, room);
|
||||
room.absTimer = setTimeout(() => destroy(id), absMs);
|
||||
if (room.absTimer.unref) room.absTimer.unref();
|
||||
resetIdle(room);
|
||||
return publicRoom(room);
|
||||
};
|
||||
|
||||
// Fase 2 (cross-device): registra el feed SSB de otro participante (de una invitación) para relayarle.
|
||||
const addRemoteFeed = (id, feed) => {
|
||||
const room = rooms.get(id);
|
||||
if (!room || typeof feed !== "string" || !/^@[A-Za-z0-9+/=]{20,}\.ed25519$/.test(feed)) return false;
|
||||
if (room.remoteFeeds.has(feed) || room.remoteFeeds.size < MAX_MEMBERS) { room.remoteFeeds.add(feed); return true; }
|
||||
return false;
|
||||
};
|
||||
const getRemoteFeeds = (id) => { const r = rooms.get(id); return r ? [...r.remoteFeeds] : []; };
|
||||
// ¿hay ALGUNA sala con feeds remotos? (para no montar el relay si no hace falta)
|
||||
const anyRemoteRooms = () => { for (const r of rooms.values()) if (r.remoteFeeds.size) return true; return false; };
|
||||
|
||||
// Fase 2 (invitar): unirse a una sala por su id creando un ESPEJO local (desde un enlace/invitación).
|
||||
// El id es la "capacidad": quien lo tiene puede unirse. Aún NO conecta con la sala del otro nodo
|
||||
// (eso es la señalización cross-device, pendiente); crea la sala local para poder abrir el enlace.
|
||||
const adoptRoom = ({ id, title = "", ttlMinutes } = {}) => {
|
||||
if (!id || typeof id !== "string" || !/^[0-9a-f]{6,64}$/.test(id)) return null;
|
||||
const existing = rooms.get(id);
|
||||
if (existing) return publicRoom(existing);
|
||||
if (rooms.size >= MAX_ROOMS) {
|
||||
const oldest = [...rooms.values()].sort((a, b) => a.createdAt - b.createdAt)[0];
|
||||
if (oldest) destroy(oldest.id);
|
||||
}
|
||||
const createdAt = Date.now();
|
||||
let absMs = absTtlMs;
|
||||
const t = parseInt(ttlMinutes, 10);
|
||||
if (Number.isFinite(t) && t > 0) absMs = Math.min(t * 60000, MAX_ABS_TTL_MS);
|
||||
const room = {
|
||||
id, title: String(title || "").slice(0, 80), createdAt, expiresAt: createdAt + absMs,
|
||||
messages: [], signals: [], members: new Set(), remoteFeeds: new Set(), seq: 0, sigSeq: 0, idleTimer: null, absTimer: null,
|
||||
};
|
||||
rooms.set(id, room);
|
||||
room.absTimer = setTimeout(() => destroy(id), absMs);
|
||||
if (room.absTimer.unref) room.absTimer.unref();
|
||||
resetIdle(room);
|
||||
return publicRoom(room);
|
||||
};
|
||||
|
||||
const listRooms = () =>
|
||||
[...rooms.values()].map(publicRoom).sort((a, b) => b.createdAt - a.createdAt);
|
||||
|
||||
const getRoom = (id) => {
|
||||
const r = rooms.get(id);
|
||||
return r ? publicRoom(r) : null;
|
||||
};
|
||||
|
||||
const postMessage = (id, { from = "?", text = "", mid = "" } = {}) => {
|
||||
const room = rooms.get(id);
|
||||
if (!room) return null;
|
||||
const clean = String(text || "").slice(0, 2000);
|
||||
if (!clean) return null;
|
||||
const msg = {
|
||||
seq: ++room.seq,
|
||||
mid: String(mid || "").slice(0, 40),
|
||||
from: String(from || "?").slice(0, 80),
|
||||
text: clean,
|
||||
ts: Date.now(),
|
||||
};
|
||||
room.messages.push(msg);
|
||||
if (room.messages.length > MAX_MSGS) room.messages.shift();
|
||||
if (room.members.has(msg.from) || room.members.size < MAX_MEMBERS) room.members.add(msg.from);
|
||||
resetIdle(room);
|
||||
return msg;
|
||||
};
|
||||
|
||||
const listMessages = (id, since = 0) => {
|
||||
const room = rooms.get(id);
|
||||
if (!room) return null;
|
||||
const s = parseInt(since, 10) || 0;
|
||||
return room.messages.filter((m) => m.seq > s);
|
||||
};
|
||||
|
||||
// --- señalización WebRTC (mailbox RAM) ---
|
||||
const postSignal = (id, { from = "", to = "", payload = null } = {}) => {
|
||||
const room = rooms.get(id);
|
||||
if (!room) return null;
|
||||
if (payload != null) { // rechaza payloads gigantes (SDP/ICE son pequeños)
|
||||
try { if (JSON.stringify(payload).length > MAX_SIGNAL_BYTES) return null; }
|
||||
catch (e) { return null; }
|
||||
}
|
||||
const sig = {
|
||||
seq: ++room.sigSeq,
|
||||
from: String(from || "").slice(0, 80),
|
||||
to: String(to || "").slice(0, 80),
|
||||
payload,
|
||||
ts: Date.now(),
|
||||
};
|
||||
room.signals.push(sig);
|
||||
if (room.signals.length > MAX_SIGNALS) room.signals.shift();
|
||||
if (sig.from && (room.members.has(sig.from) || room.members.size < MAX_MEMBERS)) room.members.add(sig.from);
|
||||
resetIdle(room);
|
||||
return sig;
|
||||
};
|
||||
|
||||
const getSignals = (id, { forId = "", since = 0 } = {}) => {
|
||||
const room = rooms.get(id);
|
||||
if (!room) return null;
|
||||
const s = parseInt(since, 10) || 0;
|
||||
return {
|
||||
signals: room.signals.filter(
|
||||
(x) => x.seq > s && x.from !== forId && (!x.to || x.to === forId)
|
||||
),
|
||||
members: [...room.members].filter((m) => m !== forId),
|
||||
};
|
||||
};
|
||||
|
||||
return {
|
||||
createRoom,
|
||||
adoptRoom,
|
||||
addRemoteFeed,
|
||||
getRemoteFeeds,
|
||||
anyRemoteRooms,
|
||||
listRooms,
|
||||
getRoom,
|
||||
postMessage,
|
||||
listMessages,
|
||||
postSignal,
|
||||
getSignals,
|
||||
_destroy: destroy,
|
||||
};
|
||||
};
|
||||
Loading…
Add table
Add a link
Reference in a new issue