upstream: integrar Oasis 1.3.1

Base del merge a tres bandas: git 0.9.5 de epsylon, esta rama y el src de la
APK oficial 1.3.1 (la variante movil: OASIS_MOBILE, regex sin \p{}, audio y
video de Android). Corre en el mismo nodejs-mobile 18.20.4 que ya usabamos.

Se conserva de la rama: Karvan, identidades y multicuenta, topbar y barra
inferior, borrado de EXIF sin sharp, filtro hot del foro, RTC sin STUN y los
modulos apagados en movil (wallet, banking, torrents).

Se toma de upstream lo que ya hace de serie: respuesta citada en chats, ssb-lan
en movil, configuracion en ~/.ssb/oasis (sobrevive a reinstalar el backend).
El asistente de bienvenida deja elegir workflow tambien en movil.
This commit is contained in:
oasis-bot 2026-10-18 02:52:17 +02:00
parent 8d838dd8b1
commit c332afd6ea
265 changed files with 80970 additions and 29167 deletions

View file

@ -2,7 +2,8 @@ const pull = require('../server/node_modules/pull-stream');
const { normalizeImages, normalizeVideo } = require('./media_gallery');
const { getConfig } = require('../configs/config-manager.js');
const { buildValidatedTombstoneSet } = require('./tombstone_validator');
const { dedupeBy, norm } = require('../backend/dedupe');
const { dedupeBy, norm, versionGroups } = require('../backend/dedupe');
const { readTyped, memoIndex } = require('./typed_log');
const logLimit = getConfig().ssbLogStream?.limit || 1000;
const normU = (v) => String(v || '').trim().toUpperCase();
@ -50,20 +51,13 @@ module.exports = ({ cooler }) => {
let ssb;
const openSsb = async () => { if (!ssb) ssb = await cooler.open(); return ssb; };
const getAllMessages = async (ssbClient) =>
new Promise((resolve, reject) => {
pull(
ssbClient.createLogStream({ limit: logLimit }),
pull.collect((err, msgs) => err ? reject(err) : resolve(msgs))
);
});
const REPORT_TYPES = ['report', 'reportConfirm', 'reportOpinion', 'tombstone'];
const getAllMessages = async (ssbClient) => readTyped(ssbClient, REPORT_TYPES, { limit: logLimit });
const buildIndex = (messages) => {
const tomb = buildValidatedTombstoneSet(messages);
const nodes = new Map();
const parent = new Map();
const child = new Map();
const strictChild = new Map();
const confirms = [];
const opinionMsgs = [];
@ -79,46 +73,24 @@ module.exports = ({ cooler }) => {
if (c.type !== 'report') continue;
const ts = v.timestamp || m.timestamp || 0;
nodes.set(k, { key: k, ts, c, author: v.author });
if (c.replaces) {
parent.set(k, c.replaces);
child.set(c.replaces, k);
}
nodes.set(k, { key: k, ts, seq: v.sequence || 0, c, author: v.author });
}
for (const [k, node] of nodes) {
const t = node.c.replaces;
if (t) { const orig = nodes.get(t); if (orig && orig.author === node.author) strictChild.set(t, k); }
}
const rootOf = (id) => {
let cur = id, g = 0;
while (parent.has(cur) && nodes.has(parent.get(cur)) && g++ < 100000) cur = parent.get(cur);
return cur;
};
const contentTipOf = (root) => {
let cur = root, g = 0;
while (strictChild.has(cur) && g++ < 100000) cur = strictChild.get(cur);
const n = nodes.get(cur), rn = nodes.get(root);
return (n && rn && n.author === rn.author) ? cur : root;
};
const roots = new Set();
for (const id of nodes.keys()) roots.add(rootOf(id));
const vg = versionGroups([...nodes.values()].map(n => ({ key: n.key, author: n.author, seq: n.seq, ts: n.ts, replaces: n.c.replaces || null, claimedAuthor: typeof n.c.author === 'string' ? n.c.author : null })));
const rootOf = vg.rootOf;
const contentTipOf = vg.tipOf;
const confirmsByRoot = new Map(), opinionsByRoot = new Map();
for (const cf of confirms) { if (!nodes.has(cf.target)) continue; const r = rootOf(cf.target); if (!confirmsByRoot.has(r)) confirmsByRoot.set(r, []); confirmsByRoot.get(r).push(cf); }
for (const op of opinionMsgs) { if (!nodes.has(op.target)) continue; const r = rootOf(op.target); if (!opinionsByRoot.has(r)) opinionsByRoot.set(r, []); opinionsByRoot.get(r).push(op); }
const tipByRoot = new Map();
for (const r of roots) tipByRoot.set(r, contentTipOf(r));
for (const r of vg.roots) tipByRoot.set(r, contentTipOf(r));
const resolveGroup = (root) => {
const contentTip = contentTipOf(root);
const contentNode = nodes.get(contentTip) || nodes.get(root);
if (!contentNode) return null;
if (!contentNode || !tipByRoot.has(root)) return null;
const rootAuthor = nodes.get(root) ? nodes.get(root).author : null;
const lc = contentNode.c || {};
@ -126,8 +98,7 @@ module.exports = ({ cooler }) => {
const voterSet = new Set(ensureArray(lc.opinions_inhabitants));
const confirmSet = new Set(ensureArray(lc.confirmations));
const groupKeys = []; { let x = root, g = 0; groupKeys.push(x); while (child.has(x) && g++ < 100000) { x = child.get(x); groupKeys.push(x); } }
for (const k of groupKeys) {
for (const k of (vg.groups.get(root) || [root])) {
const n = nodes.get(k); if (!n) continue; if (n.author !== rootAuthor) continue; const c = n.c || {};
for (const cb of ensureArray(c.confirmations)) confirmSet.add(cb);
for (const vv of ensureArray(c.opinions_inhabitants)) voterSet.add(vv);
@ -151,7 +122,7 @@ module.exports = ({ cooler }) => {
};
};
return { tomb, nodes, parent, child, rootOf, tipByRoot, resolveGroup };
return { tomb, nodes, rootOf, tipByRoot, resolveGroup };
};
const buildReport = (node, agg) => {
@ -304,7 +275,7 @@ module.exports = ({ cooler }) => {
const ssb = await openSsb();
const messages = await getAllMessages(ssb);
const idx = buildIndex(messages);
const idx = memoIndex('reports', messages, buildIndex);
const root = idx.rootOf(id);
const agg = idx.resolveGroup(root);
if (!agg || agg.tombstoned) throw new Error('Report not found');