backend: mover las rutas de Karvan a un router propio
backend.js concentra las 577 rutas de upstream en una sola cadena fluida y se mueve +905/-298 lineas por release. Las 8 rutas de Karvan, su modelo, el import de vistas y las tres funciones de relay cross-device vivian intercaladas ahi, asi que cada integracion las ponia en conflicto. Pasan a src/backend/fork_routes.js, un router propio que se inserta en la cadena de middleware justo antes del router de upstream; lo que no casa cae a next(). Las dependencias (cooler, pull, pmModel, checkMod, getViewerId) se inyectan en vez de reconstruirse, para no abrir un segundo cliente muxrpc contra el sbot. backend.js pasa de 9067 a 8932 lineas. Quedan 2 lineas de enganche en lugar de 137. Verificado: el router monta las 8 rutas (/karvan, /karvan/create, /karvan/:id, /karvan/:id/invite, /karvan/:id/msg, /karvan/:id/msgs y /karvan/:id/signal en GET y POST) y no quedan referencias huerfanas en backend.js.
This commit is contained in:
parent
9321718d90
commit
e5e8e53374
2 changed files with 163 additions and 137 deletions
|
|
@ -637,60 +637,7 @@ const documentsModel = require("../models/documents_model")({ cooler, isPublic:
|
||||||
const trendingModel = require('../models/trending_model')({ cooler, isPublic: config.public });
|
const trendingModel = require('../models/trending_model')({ cooler, isPublic: config.public });
|
||||||
const statsModel = require('../models/stats_model')({ cooler, isPublic: config.public, tribeCrypto, tribesModel });
|
const statsModel = require('../models/stats_model')({ cooler, isPublic: config.public, tribeCrypto, tribesModel });
|
||||||
const padsModel = require('../models/pads_model')({ cooler, cipherModel, tribeCrypto, padCrypto, 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 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 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 });
|
const searchModel = require('../models/search_model')({ cooler, isPublic: config.public, padsModel, tribeCrypto, tribesModel });
|
||||||
|
|
@ -1460,7 +1407,6 @@ const { jobsView, singleJobsView, renderJobForm, clearnetJobView } = require("..
|
||||||
const { shopsView, singleShopView, singleProductView, editProductView, shopOrdersView, myPurchasesView, clearnetShopView, renderShopInvitePage } = require("../views/shops_view");
|
const { shopsView, singleShopView, singleProductView, editProductView, shopOrdersView, myPurchasesView, clearnetShopView, renderShopInvitePage } = require("../views/shops_view");
|
||||||
const { chatsView, singleChatView, renderChatInvitePage } = require("../views/chats_view");
|
const { chatsView, singleChatView, renderChatInvitePage } = require("../views/chats_view");
|
||||||
const { padsView, singlePadView, renderPadInvitePage } = require("../views/pads_view");
|
const { padsView, singlePadView, renderPadInvitePage } = require("../views/pads_view");
|
||||||
const { karvanView, karvanRoomView } = require("../views/karvan_view");
|
|
||||||
const { calendarsView, singleCalendarView, renderCalendarInvitePage } = require("../views/calendars_view");
|
const { calendarsView, singleCalendarView, renderCalendarInvitePage } = require("../views/calendars_view");
|
||||||
const { projectsView, singleProjectView, clearnetProjectView } = require("../views/projects_view")
|
const { projectsView, singleProjectView, clearnetProjectView } = require("../views/projects_view")
|
||||||
const { industryView, singleFacilityView, singleBuildView, singleBlueprintView, blueprintEditView, buildEditView } = require("../views/industry_view")
|
const { industryView, singleFacilityView, singleBuildView, singleBlueprintView, blueprintEditView, buildEditView } = require("../views/industry_view")
|
||||||
|
|
@ -3690,89 +3636,6 @@ router
|
||||||
}
|
}
|
||||||
ctx.body = await mentionsView({ messages: combined, myFeedId });
|
ctx.body = await mentionsView({ messages: combined, myFeedId });
|
||||||
})
|
})
|
||||||
// ===== FORK_IA_UX: módulo KARVAN (mensajes temporales RAM + WebRTC data-channel) =====
|
|
||||||
.get('/karvan', async (ctx) => {
|
|
||||||
if (!checkMod(ctx, 'karvanMod')) { ctx.redirect('/modules'); return; }
|
|
||||||
let selfId = ''; try { selfId = getViewerId(); } catch (e) {}
|
|
||||||
ctx.body = karvanView(karvanModel.listRooms(), { selfId });
|
|
||||||
})
|
|
||||||
.post('/karvan/create', koaBody(), async (ctx) => {
|
|
||||||
if (!checkMod(ctx, 'karvanMod')) { ctx.redirect('/modules'); return; }
|
|
||||||
const b = ctx.request.body || {};
|
|
||||||
const room = karvanModel.createRoom({
|
|
||||||
title: stripDangerousTags(String(b.title || '').trim()).slice(0, 80),
|
|
||||||
ttlMinutes: b.ttl,
|
|
||||||
});
|
|
||||||
ctx.redirect('/karvan/' + room.id);
|
|
||||||
})
|
|
||||||
.get('/karvan/:id', async (ctx) => {
|
|
||||||
if (!checkMod(ctx, 'karvanMod')) { ctx.redirect('/modules'); return; }
|
|
||||||
let room = karvanModel.getRoom(ctx.params.id);
|
|
||||||
// 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 }); 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 || '' });
|
|
||||||
})
|
|
||||||
.post('/karvan/:id/invite', koaBody(), async (ctx) => {
|
|
||||||
if (!checkMod(ctx, 'karvanMod')) { ctx.status = 403; ctx.body = { error: 'off' }; return; }
|
|
||||||
const id = ctx.params.id;
|
|
||||||
const room = karvanModel.getRoom(id);
|
|
||||||
if (!room) { ctx.redirect('/karvan'); return; }
|
|
||||||
const to = String((ctx.request.body || {}).to || '').trim();
|
|
||||||
if (!/^@[A-Za-z0-9+/=]{20,}\.ed25519$/.test(to)) { ctx.redirect('/karvan/' + id + '?invite=bad'); return; }
|
|
||||||
try {
|
|
||||||
// reutiliza el patrón de invitación por mensaje privado de Oasis (como el módulo industry):
|
|
||||||
// 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'); }
|
|
||||||
})
|
|
||||||
.post('/karvan/:id/msg', koaBody(), async (ctx) => {
|
|
||||||
if (!checkMod(ctx, 'karvanMod')) { ctx.status = 403; ctx.body = { error: 'off' }; return; }
|
|
||||||
const b = ctx.request.body || {};
|
|
||||||
let selfId = ''; try { selfId = getViewerId(); } catch (e) {}
|
|
||||||
const msg = karvanModel.postMessage(ctx.params.id, {
|
|
||||||
from: String(b.from || selfId || '?').slice(0, 80),
|
|
||||||
text: stripDangerousTags(String(b.text || '')).slice(0, 2000),
|
|
||||||
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 };
|
|
||||||
else ctx.redirect('/karvan/' + ctx.params.id);
|
|
||||||
})
|
|
||||||
.get('/karvan/:id/msgs', async (ctx) => {
|
|
||||||
if (!checkMod(ctx, 'karvanMod')) { ctx.status = 403; ctx.body = { error: 'off' }; return; }
|
|
||||||
const msgs = karvanModel.listMessages(ctx.params.id, ctx.query.since || 0);
|
|
||||||
ctx.body = { messages: msgs || [] };
|
|
||||||
})
|
|
||||||
.post('/karvan/:id/signal', koaBody(), async (ctx) => {
|
|
||||||
if (!checkMod(ctx, 'karvanMod')) { ctx.status = 403; ctx.body = { error: 'off' }; return; }
|
|
||||||
const b = ctx.request.body || {};
|
|
||||||
karvanModel.postSignal(ctx.params.id, { from: b.from, to: b.to, payload: b.payload });
|
|
||||||
ctx.body = { ok: true };
|
|
||||||
})
|
|
||||||
.get('/karvan/:id/signal', async (ctx) => {
|
|
||||||
if (!checkMod(ctx, 'karvanMod')) { ctx.status = 403; ctx.body = { error: 'off' }; return; }
|
|
||||||
const r = karvanModel.getSignals(ctx.params.id, { forId: ctx.query.for || '', since: ctx.query.since || 0 });
|
|
||||||
ctx.body = r || { signals: [], members: [] };
|
|
||||||
})
|
|
||||||
.get('/opinions', async (ctx) => {
|
.get('/opinions', async (ctx) => {
|
||||||
const filter = qf(ctx, 'TOP');
|
const filter = qf(ctx, 'TOP');
|
||||||
let opinions = await opinionsModel.listOpinions(filter);
|
let opinions = await opinionsModel.listOpinions(filter);
|
||||||
|
|
@ -8827,6 +8690,8 @@ const middleware = [
|
||||||
}
|
}
|
||||||
await next();
|
await next();
|
||||||
},
|
},
|
||||||
|
// rutas propias de la rama (Karvan): fuera de la cadena de upstream
|
||||||
|
require('./fork_routes')({ cooler, pull, pmModel, checkMod, getViewerId }),
|
||||||
routes,
|
routes,
|
||||||
];
|
];
|
||||||
const app = http({ host, port, middleware, allowHost: config.allowHost });
|
const app = http({ host, port, middleware, allowHost: config.allowHost });
|
||||||
|
|
|
||||||
161
src/backend/fork_routes.js
Normal file
161
src/backend/fork_routes.js
Normal file
|
|
@ -0,0 +1,161 @@
|
||||||
|
"use strict";
|
||||||
|
// Rutas propias de la rama experimental (modulo Karvan: salas efimeras en RAM,
|
||||||
|
// mensajes temporales y senalizacion WebRTC).
|
||||||
|
//
|
||||||
|
// Viven fuera de backend.js a proposito: ese fichero concentra las 577 rutas de
|
||||||
|
// upstream en una sola cadena fluida y se mueve +905/-298 lineas por release, asi
|
||||||
|
// que cualquier ruta anadida en medio entra en conflicto en cada integracion.
|
||||||
|
// Aqui se montan en un router propio que se inserta en la cadena de middleware
|
||||||
|
// antes del router de upstream; lo que no casa cae a next() y sigue su curso.
|
||||||
|
//
|
||||||
|
// Las dependencias se inyectan en lugar de reconstruirse: cooler abre la conexion
|
||||||
|
// al sbot y duplicarla abriria un segundo cliente muxrpc.
|
||||||
|
|
||||||
|
const koaRouter = require("../server/node_modules/@koa/router");
|
||||||
|
const { koaBody } = require("../server/node_modules/koa-body");
|
||||||
|
const { stripDangerousTags } = require("./sanitizeHtml");
|
||||||
|
const karvanModel = require('../models/karvan_model')({}); // FORK_IA_UX: salas efímeras en RAM (mensajes temporales + señalización WebRTC)
|
||||||
|
const { karvanView, karvanRoomView } = require("../views/karvan_view");
|
||||||
|
|
||||||
|
module.exports = ({ cooler, pull, pmModel, checkMod, getViewerId }) => {
|
||||||
|
// 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 forkRouter = new koaRouter();
|
||||||
|
forkRouter
|
||||||
|
// ===== FORK_IA_UX: módulo KARVAN (mensajes temporales RAM + WebRTC data-channel) =====
|
||||||
|
.get('/karvan', async (ctx) => {
|
||||||
|
if (!checkMod(ctx, 'karvanMod')) { ctx.redirect('/modules'); return; }
|
||||||
|
let selfId = ''; try { selfId = getViewerId(); } catch (e) {}
|
||||||
|
ctx.body = karvanView(karvanModel.listRooms(), { selfId });
|
||||||
|
})
|
||||||
|
.post('/karvan/create', koaBody(), async (ctx) => {
|
||||||
|
if (!checkMod(ctx, 'karvanMod')) { ctx.redirect('/modules'); return; }
|
||||||
|
const b = ctx.request.body || {};
|
||||||
|
const room = karvanModel.createRoom({
|
||||||
|
title: stripDangerousTags(String(b.title || '').trim()).slice(0, 80),
|
||||||
|
ttlMinutes: b.ttl,
|
||||||
|
});
|
||||||
|
ctx.redirect('/karvan/' + room.id);
|
||||||
|
})
|
||||||
|
.get('/karvan/:id', async (ctx) => {
|
||||||
|
if (!checkMod(ctx, 'karvanMod')) { ctx.redirect('/modules'); return; }
|
||||||
|
let room = karvanModel.getRoom(ctx.params.id);
|
||||||
|
// 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 }); 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 || '' });
|
||||||
|
})
|
||||||
|
.post('/karvan/:id/invite', koaBody(), async (ctx) => {
|
||||||
|
if (!checkMod(ctx, 'karvanMod')) { ctx.status = 403; ctx.body = { error: 'off' }; return; }
|
||||||
|
const id = ctx.params.id;
|
||||||
|
const room = karvanModel.getRoom(id);
|
||||||
|
if (!room) { ctx.redirect('/karvan'); return; }
|
||||||
|
const to = String((ctx.request.body || {}).to || '').trim();
|
||||||
|
if (!/^@[A-Za-z0-9+/=]{20,}\.ed25519$/.test(to)) { ctx.redirect('/karvan/' + id + '?invite=bad'); return; }
|
||||||
|
try {
|
||||||
|
// reutiliza el patrón de invitación por mensaje privado de Oasis (como el módulo industry):
|
||||||
|
// 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'); }
|
||||||
|
})
|
||||||
|
.post('/karvan/:id/msg', koaBody(), async (ctx) => {
|
||||||
|
if (!checkMod(ctx, 'karvanMod')) { ctx.status = 403; ctx.body = { error: 'off' }; return; }
|
||||||
|
const b = ctx.request.body || {};
|
||||||
|
let selfId = ''; try { selfId = getViewerId(); } catch (e) {}
|
||||||
|
const msg = karvanModel.postMessage(ctx.params.id, {
|
||||||
|
from: String(b.from || selfId || '?').slice(0, 80),
|
||||||
|
text: stripDangerousTags(String(b.text || '')).slice(0, 2000),
|
||||||
|
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 };
|
||||||
|
else ctx.redirect('/karvan/' + ctx.params.id);
|
||||||
|
})
|
||||||
|
.get('/karvan/:id/msgs', async (ctx) => {
|
||||||
|
if (!checkMod(ctx, 'karvanMod')) { ctx.status = 403; ctx.body = { error: 'off' }; return; }
|
||||||
|
const msgs = karvanModel.listMessages(ctx.params.id, ctx.query.since || 0);
|
||||||
|
ctx.body = { messages: msgs || [] };
|
||||||
|
})
|
||||||
|
.post('/karvan/:id/signal', koaBody(), async (ctx) => {
|
||||||
|
if (!checkMod(ctx, 'karvanMod')) { ctx.status = 403; ctx.body = { error: 'off' }; return; }
|
||||||
|
const b = ctx.request.body || {};
|
||||||
|
karvanModel.postSignal(ctx.params.id, { from: b.from, to: b.to, payload: b.payload });
|
||||||
|
ctx.body = { ok: true };
|
||||||
|
})
|
||||||
|
.get('/karvan/:id/signal', async (ctx) => {
|
||||||
|
if (!checkMod(ctx, 'karvanMod')) { ctx.status = 403; ctx.body = { error: 'off' }; return; }
|
||||||
|
const r = karvanModel.getSignals(ctx.params.id, { forId: ctx.query.for || '', since: ctx.query.since || 0 });
|
||||||
|
ctx.body = r || { signals: [], members: [] };
|
||||||
|
});
|
||||||
|
|
||||||
|
return forkRouter.routes();
|
||||||
|
};
|
||||||
Loading…
Add table
Add a link
Reference in a new issue