[0.9.1] FORK_IA_UX: Fase 2 cross-device — relay Karvan messages over SSB private msgs
Makes a Karvan room work between two different phones once they're connected to a pub: - karvan_model: rooms track remoteFeeds (SSB ids of other participants); addRemoteFeed/ getRemoteFeeds/anyRemoteRooms (feed-id validated, capped at MAX_MEMBERS). - backend: inviting registers the invitee's feed; joining by link registers the inviter's feed (read from the invite PM). Posting a message publishes a private 'karvan-relay' to those feeds; an efficient LIVE log-stream subscriber (createLogStream old:false live:true) ingests incoming relays and injects them into the local mirror room (dedup by mid, skips own → no loop). WebRTC SIGNAL frames are NOT relayed (too many/slow for the log). - Chosen over a muxrpc plugin because a classic pub only replicates the LOG (store-and-forward); live muxrpc wouldn't reach the other phone. No SSB-startup changes → no boot risk. Verified: boot OK with the live stream, invite/adopt register feeds, message posts + relay publishes, server stays up, 16/16 tests. Cross-device delivery needs the user's 2-phone+pub test. Note: text chat only — video still needs the wrapper camera permission (Fase 3).
This commit is contained in:
parent
2c2c8f241d
commit
6020c7f8df
3 changed files with 90 additions and 2 deletions
|
|
@ -638,6 +638,59 @@ const trendingModel = require('../models/trending_model')({ cooler, isPublic: co
|
|||
const statsModel = require('../models/stats_model')({ cooler, isPublic: config.public, tribeCrypto, tribesModel });
|
||||
const padsModel = require('../models/pads_model')({ cooler, cipherModel, tribeCrypto, padCrypto, tribesModel });
|
||||
const karvanModel = require('../models/karvan_model')({}); // FORK_IA_UX: salas efímeras en RAM (mensajes temporales + señalización WebRTC)
|
||||
|
||||
// FORK_IA_UX Fase 2 (cross-device): relay de MENSAJES de Karvan por privados SSB. Funciona vía un pub
|
||||
// (el privado se replica al otro móvil); los SIGNAL de WebRTC NO se relayan (serían demasiados/lentos por el log).
|
||||
// No toca el arranque del SSB. Ingesta por stream EN VIVO (no polling) → eficiente.
|
||||
const karvanSeenRelay = new Set(); // mids ya inyectados (evita duplicados/bucles)
|
||||
let karvanRelaySub = false; // ¿suscriptor del log en vivo montado?
|
||||
async function karvanPublishRelay(roomId, mid, from, text) {
|
||||
try {
|
||||
const feeds = karvanModel.getRemoteFeeds(roomId);
|
||||
if (!feeds.length) return;
|
||||
const ssbClient = await cooler.open();
|
||||
const recps = [...new Set([ssbClient.id, ...feeds])].slice(0, 8);
|
||||
await new Promise((res, rej) => ssbClient.private.publish(
|
||||
{ type: 'karvan-relay', roomId, mid: String(mid || '').slice(0, 60), from: String(from || '?').slice(0, 80), text: String(text || '').slice(0, 2000), private: true },
|
||||
recps, (e, m) => e ? rej(e) : res(m)));
|
||||
} catch (e) {}
|
||||
}
|
||||
async function karvanStartRelay() {
|
||||
if (karvanRelaySub) return;
|
||||
karvanRelaySub = true;
|
||||
try {
|
||||
const ssbClient = await cooler.open();
|
||||
const me = ssbClient.id;
|
||||
pull(
|
||||
ssbClient.createLogStream({ old: false, live: true }),
|
||||
pull.drain((m) => {
|
||||
try {
|
||||
if (!m || !m.value) return;
|
||||
let dec; try { dec = ssbClient.private.unbox({ key: m.key, value: m.value, timestamp: m.timestamp }); } catch (_) { return; }
|
||||
const c = dec && dec.value && dec.value.content;
|
||||
if (!c || c.type !== 'karvan-relay' || !c.roomId || !c.mid) return;
|
||||
if (dec.value.author === me) return; // no reinyectar lo propio (evita bucle A→B→A)
|
||||
if (karvanSeenRelay.has(c.mid)) return;
|
||||
if (!karvanModel.getRoom(c.roomId)) return; // solo si tenemos esa sala localmente
|
||||
karvanSeenRelay.add(c.mid);
|
||||
if (karvanSeenRelay.size > 5000) karvanSeenRelay.clear();
|
||||
karvanModel.postMessage(c.roomId, { from: c.from, text: c.text, mid: c.mid });
|
||||
} catch (e) {}
|
||||
})
|
||||
);
|
||||
} catch (e) { karvanRelaySub = false; }
|
||||
}
|
||||
// ¿quién me invitó a esta sala? (para registrar su feed en el espejo local) — reutiliza el lector de privados
|
||||
async function karvanInviteAuthor(roomId) {
|
||||
try {
|
||||
const msgs = await pmModel.listAllPrivate();
|
||||
for (const m of (msgs || [])) {
|
||||
const c = m && m.value && m.value.content;
|
||||
if (c && c.subject === 'KARVAN_INVITE' && typeof c.text === 'string' && c.text.indexOf('/karvan/' + roomId) !== -1) return m.value.author;
|
||||
}
|
||||
} catch (e) {}
|
||||
return null;
|
||||
}
|
||||
const tagsModel = require('../models/tags_model')({ cooler, isPublic: config.public, padsModel, tribesModel });
|
||||
const tribesContentModel = require('../models/tribes_content_model')({ cooler, isPublic: config.public, tribeCrypto, tribesModel });
|
||||
const searchModel = require('../models/search_model')({ cooler, isPublic: config.public, padsModel, tribeCrypto, tribesModel });
|
||||
|
|
@ -3658,11 +3711,16 @@ router
|
|||
// unirse por enlace/invitación crea un espejo local, pero SOLO en una navegación real (documento):
|
||||
// un <img>/subrecurso de una web maliciosa NO debe poder crear/expulsar salas por GET (CSRF). El navegador
|
||||
// controla sec-fetch-dest/accept, así que son señales fiables.
|
||||
let justAdopted = false;
|
||||
if (!room) {
|
||||
const isNav = ctx.get('sec-fetch-dest') === 'document' || (ctx.get('accept') || '').includes('text/html');
|
||||
if (isNav) room = karvanModel.adoptRoom({ id: ctx.params.id });
|
||||
if (isNav) { room = karvanModel.adoptRoom({ id: ctx.params.id }); justAdopted = !!room; }
|
||||
}
|
||||
if (!room) { ctx.redirect('/karvan'); return; }
|
||||
if (justAdopted) { // al unirte por enlace, registra el feed de quien te invitó → relay cross-device
|
||||
const author = await karvanInviteAuthor(ctx.params.id);
|
||||
if (author) { karvanModel.addRemoteFeed(ctx.params.id, author); karvanStartRelay(); }
|
||||
}
|
||||
let selfId = ''; try { selfId = getViewerId(); } catch (e) {}
|
||||
const msgs = karvanModel.listMessages(room.id, 0) || [];
|
||||
ctx.body = karvanRoomView(room, msgs, { selfId, invite: ctx.query.invite || '' });
|
||||
|
|
@ -3679,6 +3737,7 @@ router
|
|||
// le llega al INBOX del contacto con un enlace a la sala. Sin código SSB nuevo.
|
||||
await pmModel.sendMessage([to], 'KARVAN_INVITE',
|
||||
'You have been invited to an ephemeral Karvan room' + (room.title ? ' ("' + room.title + '")' : '') + ' -> /karvan/' + id);
|
||||
karvanModel.addRemoteFeed(id, to); karvanStartRelay(); // relay cross-device hacia el invitado
|
||||
ctx.redirect('/karvan/' + id + '?invite=ok');
|
||||
} catch (e) { ctx.redirect('/karvan/' + id + '?invite=fail'); }
|
||||
})
|
||||
|
|
@ -3692,6 +3751,7 @@ router
|
|||
mid: b.mid,
|
||||
});
|
||||
if (!msg) { ctx.status = 404; ctx.body = { error: 'no room' }; return; }
|
||||
karvanPublishRelay(ctx.params.id, msg.mid || (msg.from + '-' + msg.ts + '-' + msg.seq), msg.from, msg.text); // → feeds remotos (si hay)
|
||||
const wantsJson = (ctx.get('accept') || '').indexOf('application/json') !== -1
|
||||
|| (ctx.get('content-type') || '').indexOf('application/json') !== -1;
|
||||
if (wantsJson) ctx.body = { ok: true, seq: msg.seq };
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue