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 fs = require('fs');
const path = require('path');
const pull = require('../server/node_modules/pull-stream');
const STEPS = ['language', 'ux', 'profile', 'federation', 'larp', 'greeting', 'backup'];
const STEPS = ['profile', 'ux', 'workflow', 'theme', 'community', 'greeting'];
const MARKED_STEPS = ['profile', 'ux', 'workflow', 'theme', 'community'];
const FLAG = 'oasis-first-contact';
const PENDING = 'welcome=pending';
const DISMISSED = 'welcome=dismissed';
@ -13,7 +14,13 @@ const ssbPathOf = (given) => {
try { return require('../server/ssb_config').path; } catch (_) { return null; }
};
const flagPath = (dir) => path.join(dir, FLAG);
const flagPath = (dir) => {
try {
const state = require('../configs/state-manager');
if (!dir || path.resolve(dir) === state.ssbDir()) return state.statePath(FLAG);
} catch (_) {}
return path.join(dir, FLAG);
};
function readFlag(dir) {
if (!dir) return { exists: false, id: '', markers: new Set() };
@ -49,6 +56,33 @@ const isPending = (dir) => {
const bannerVisible = (ssbPath) => isPending(ssbPathOf(ssbPath));
const DEFAULT_LANGUAGE = 'en';
const browserLanguage = (header, supported) => {
const list = Array.isArray(supported) ? supported : [];
const wanted = String(header || '').slice(0, 1024).split(',').map((part, i) => {
const [tag, ...params] = part.trim().split(';');
const qParam = params.map(p => p.trim()).find(p => /^q=/i.test(p));
const q = qParam ? Number(qParam.slice(2)) : 1;
return { tag: String(tag || '').trim().toLowerCase().replace(/_/g, '-'), q: Number.isFinite(q) ? q : 0, i };
}).filter(w => w.tag && w.tag !== '*' && w.q > 0).sort((a, b) => b.q - a.q || a.i - b.i);
for (const w of wanted) {
if (list.includes(w.tag)) return w.tag;
const base = w.tag.split('-')[0];
if (list.includes(base)) return base;
}
return '';
};
const firstRunLanguage = ({ fresh, isPublic, configured, cookie, header, supported } = {}) => {
const list = Array.isArray(supported) ? supported : [];
if (!fresh || isPublic) return { settled: true, language: '' };
if (list.includes(configured) && configured !== DEFAULT_LANGUAGE) return { settled: true, language: '' };
const picked = (list.includes(cookie) ? cookie : '') || browserLanguage(header, list);
if (!picked && !String(header || '').trim()) return { settled: false, language: '' };
return { settled: true, language: picked };
};
module.exports = ({ cooler, ssbPath } = {}) => {
const dir = () => ssbPathOf(ssbPath);
@ -56,7 +90,7 @@ module.exports = ({ cooler, ssbPath } = {}) => {
const ssb = await cooler.open();
const mine = ssb.id;
const all = await new Promise((resolve, reject) =>
pull(ssb.createLogStream({ reverse: true, limit: 2000 }), pull.collect((err, msgs) => err ? reject(err) : resolve(msgs || [])))
pull(ssb.createUserStream({ id: mine, reverse: true, limit: 2000 }), pull.collect((err, msgs) => err ? reject(err) : resolve(msgs || [])))
);
return { mine, messages: all.filter(m => m && m.value && m.value.author === mine) };
};
@ -72,16 +106,6 @@ module.exports = ({ cooler, ssbPath } = {}) => {
return c && typeof c === 'object' && c.type === 'feed' && String(c.text || '').trim().length > 0;
});
const hasJoinedLarp = (messages) => {
for (const m of messages) {
const c = m.value.content;
if (!c || typeof c !== 'object') continue;
if (c.type === 'larpJoinHouse' && c.house) return true;
if (c.type === 'larpLeaveLarp') return false;
}
return false;
};
const hasFederated = (mine, messages) => {
try {
const gossip = JSON.parse(fs.readFileSync(path.join(dir(), 'gossip.json'), 'utf8'));
@ -120,9 +144,20 @@ module.exports = ({ cooler, ssbPath } = {}) => {
}
},
adopt(feedId) {
const target = dir();
if (!target || !feedId) return false;
try {
fs.mkdirSync(target, { recursive: true });
fs.writeFileSync(flagPath(target), `${String(feedId)}\n${new Date().toISOString()}\n${DONE}\n`);
return true;
} catch (_) {
return false;
}
},
markStep(step) {
if (!STEPS.includes(step)) return false;
if (step !== 'language' && step !== 'backup' && step !== 'ux') return true;
if (!MARKED_STEPS.includes(step)) return false;
if (!isPending(dir())) return true;
return appendMarker(dir(), `step=${step}`);
},
@ -143,12 +178,11 @@ module.exports = ({ cooler, ssbPath } = {}) => {
let messages = [];
try { ({ mine, messages } = await ownMessages()); } catch (_) { mine = ''; messages = []; }
const steps = {
language: flag.markers.has('step=language'),
profile: hasProfile(mine, messages),
federation: hasFederated(mine, messages),
larp: hasJoinedLarp(messages),
profile: hasProfile(mine, messages) || flag.markers.has('step=profile'),
ux: flag.markers.has('step=ux'),
backup: flag.markers.has('step=backup'),
workflow: flag.markers.has('step=workflow'),
theme: flag.markers.has('step=theme'),
community: hasFederated(mine, messages) || flag.markers.has('step=community'),
greeting: hasGreeted(messages)
};
const profile = { id: mine, name: '', description: '', image: '' };
@ -180,3 +214,5 @@ module.exports = ({ cooler, ssbPath } = {}) => {
module.exports.bannerVisible = bannerVisible;
module.exports.STEPS = STEPS;
module.exports.browserLanguage = browserLanguage;
module.exports.firstRunLanguage = firstRunLanguage;