coherencia con la guia: lo de movil del dev vuelve a ser suyo

Comparando con el nodejs-project de la APK oficial (la referencia buena para
el movil, no el git del dev) salia que parte de lo que se saco de backend.js
era codigo de movil del propio dev: apagar los modulos que no van, cortar
rutas, retocar el HTML, las descargas de la copia de seguridad y ocultar
tema y workflow en la bienvenida.

- backend.js vuelve a ser su version de movil mas dos lineas (montar nuestro
  router); modules_view.js, su version de movil
- mobile_layer.js se queda con lo nuestro: avatar, filtro y lanzador
- la guia lista las 17 diferencias que quedan con la APK oficial (561 lineas)
  y explica que hay que comparar con la APK, no con el git
This commit is contained in:
hacklab 2026-10-19 14:19:54 +02:00
parent b65dde1263
commit 6eafe5ee18
8 changed files with 124 additions and 147 deletions

View file

@ -1644,6 +1644,17 @@ debug("Current configuration: %O", config);
debug(`You can save the above to ${defaultConfigFile} to make \
these settings the default. See the readme for details.`);
const { saveConfig, getConfig } = require('../configs/config-manager');
if (process.env.OASIS_MOBILE === '1') {
try {
const cfg = getConfig();
const forced = ['ai', 'aiNav', 'dev', 'games', 'logs', 'maps', 'pixelia'].filter(m => cfg.modules && cfg.modules[`${m}Mod`] !== 'off');
let dirty = false;
if (forced.length) { forced.forEach(m => { cfg.modules[`${m}Mod`] = 'off'; }); dirty = true; }
const w = cfg.wallet || {};
if (w.url === 'http://127.0.0.1:17474' && w.user === 'oasis' && w.pass === 'testpass123') { cfg.wallet = { ...w, url: 'http://127.0.0.1:7474', user: '', pass: '' }; dirty = true; }
if (dirty) saveConfig(cfg);
} catch (_) {}
}
const oasisCheckPath = "/.well-known/oasis";
process.on("uncaughtException", function (err) {
if (err["code"] === "EADDRINUSE") {
@ -5103,6 +5114,7 @@ router
})
.get('/blockexplorer', async (ctx) => {
if (process.env.OASIS_MOBILE === '1') { ctx.redirect('/'); return; }
const userId = getViewerId();
const query = ctx.query || {};
const search = {
@ -5153,6 +5165,7 @@ router
ctx.body = renderBlockchainView(blockchainData, filter, userId, search, { inspect, censusBlocks });
})
.get('/blockexplorer/block/:id', async (ctx) => {
if (process.env.OASIS_MOBILE === '1') { ctx.redirect('/'); return; }
const userId = getViewerId();
const query = ctx.query || {};
const search = {
@ -8473,8 +8486,8 @@ router
const available = {
profile: true,
ux: true,
workflow: isLoopbackRequest(ctx),
theme: true,
workflow: process.env.OASIS_MOBILE !== '1' && isLoopbackRequest(ctx),
theme: process.env.OASIS_MOBILE !== '1',
community: checkMod(ctx, 'invitesMod'),
greeting: checkMod(ctx, 'feedMod')
};
@ -11509,6 +11522,40 @@ router
try { await forumModel.removeOpenInvite(ctx.params.id); } catch (_) { actionFail(ctx); return; }
ctx.redirect(safeReturnTo(ctx, `/forum/${encodeURIComponent(ctx.params.id)}`, ['/forum']));
})
.get('/backup/keys/export', async (ctx) => {
if (!isLoopbackRequest(ctx)) { ctx.status = 403; ctx.body = ''; return; }
if (process.env.OASIS_MOBILE !== '1') { ctx.redirect('/backup'); return; }
const pw = String(ctx.query.password || '');
if (!pw || pw.length < 32) return ctx.redirect('/backup');
try {
const { filename, data } = await backupModel.exportKeys({ password: pw });
ctx.set('Content-Type', 'application/octet-stream');
ctx.set('Content-Disposition', `attachment; filename="${filename}"`);
ctx.set('Content-Length', String(data.length));
ctx.body = data;
try { onboardingModel.markStep('backup'); } catch (_) {}
} catch (error) { sendErrorPage(ctx, error.message); }
})
.get('/backup/export', async (ctx) => {
if (!isLoopbackRequest(ctx)) { ctx.status = 403; ctx.body = ''; return; }
if (process.env.OASIS_MOBILE !== '1') { ctx.redirect('/backup'); return; }
if (!checkMod(ctx, 'backupMod')) return ctx.redirect('/modules');
const b = ctx.query || {};
const pw = String(b.password || '');
if (pw.length < 32) return ctx.redirect('/backup');
const rawMods = b.modules === undefined ? [] : (Array.isArray(b.modules) ? b.modules : [b.modules]);
const outPath = path.join(os.tmpdir(), `oasis-backup-${process.pid}-${Date.now()}.oasisbk`);
try {
const res = await backupModel.createBackup({ scope: b.scope, blobs: b.blobs, modules: rawMods, since: b.since }, pw, outPath);
ctx.set('Content-Type', 'application/octet-stream');
ctx.set('Content-Disposition', `attachment; filename="${res.filename}"`);
ctx.set('Content-Length', String(res.bytes));
const stream = fs.createReadStream(outPath);
stream.on('close', () => { try { fs.unlinkSync(outPath); } catch (_) {} });
ctx.body = stream;
try { onboardingModel.markStep('backup'); } catch (_) {}
} catch (error) { try { fs.unlinkSync(outPath); } catch (_) {} sendErrorPage(ctx, error.message); }
})
.post('/backup/keys/export', koaBody(), async (ctx) => {
if (!isLoopbackRequest(ctx)) { ctx.status = 403; ctx.body = ''; return; }
const pw = ctx.request.body.password;
@ -14601,6 +14648,7 @@ router
})
.post("/update", koaBody(), async (ctx) => {
if (!isLoopbackRequest(ctx)) { ctx.status = 403; ctx.body = ''; return; }
if (process.env.OASIS_MOBILE === '1') { ctx.redirect('/settings'); return; }
const exec = require("node:util").promisify(require("node:child_process").exec);
const repoRoot = path.resolve(__dirname, '..', '..');
const { stdout, stderr } = await exec("git reset --hard && git pull", { cwd: repoRoot });
@ -15127,6 +15175,7 @@ router
})
.post("/modules/preset", koaBody(), async (ctx) => {
if (!isLoopbackRequest(ctx)) { ctx.status = 403; ctx.body = ''; return; }
if (process.env.OASIS_MOBILE === '1') { ctx.redirect('/modules'); return; }
const ALL_MODULES = workflowsModel.ALL_MODULES;
const PRESETS = workflowsModel.PRESETS;
const preset = String(ctx.request.body.preset || '');
@ -15138,10 +15187,14 @@ router
ctx.redirect('/modules');
})
.post("/save-modules", koaBody(), async (ctx) => {
const MOBILE_UNAVAILABLE_MODS = ['ai', 'aiNav', 'dev', 'games', 'logs', 'maps', 'pixelia'];
if (!isLoopbackRequest(ctx)) { ctx.status = 403; ctx.body = ''; return; }
const modules = workflowsModel.ALL_MODULES;
const cfg = getConfig();
modules.forEach(mod => cfg.modules[`${mod}Mod`] = ctx.request.body[`${mod}Form`] === 'on' ? 'on' : 'off');
if (process.env.OASIS_MOBILE === '1') {
MOBILE_UNAVAILABLE_MODS.forEach(mod => cfg.modules[`${mod}Mod`] = 'off');
}
saveConfig(cfg);
ctx.redirect(`/modules`);
})
@ -15411,6 +15464,14 @@ const kickRefresh = () => {
};
setTimeout(() => { if (!sharedState.getLastRefresh()) kickRefresh(); }, 5000);
const middleware = [
async (ctx, next) => {
await next();
if (process.env.OASIS_MOBILE !== '1' || typeof ctx.body !== 'string' || !String(ctx.type || '').includes('html')) return;
ctx.body = ctx.body.replace(/<video\b[^>]*>/g, (tag) => tag.replace(/\bsrc="(\/blob\/[^"#]+)"/, 'src="$1#t=0.1"'));
const tab = ['/mentions', '/inbox', '/phone', '/data', '/tags', '/search', '/settings'].find((t) => ctx.path === t || ctx.path.startsWith(t + '/'));
const headerEnd = ctx.body.indexOf('<div class="main-content">');
if (tab && headerEnd > 0) ctx.body = ctx.body.slice(0, headerEnd).replace(`<a href="${tab}"`, `<a href="${tab}" aria-current="page"`) + ctx.body.slice(headerEnd);
},
async (ctx, next) => {
if (ctx.method === 'GET' && /^recent$/i.test(String(ctx.query.filter || '')) && ['q', 'search'].some(k => String(ctx.query[k] || '').trim())) ctx.query = { ...ctx.query, filter: ctx.query.filter === 'RECENT' ? 'ALL' : 'all' };
await next();
@ -15515,8 +15576,7 @@ const middleware = [
await next();
},
// la rama movil: su router y su capa de backend (ver GUIA_DE_DESARROLLO.md)
require('./fork_routes')({ checkMod, getViewerId, getConfig, saveConfig, isLoopbackRequest, sendErrorPage,
about, backupModel, onboardingModel }),
require('./fork_routes')({ checkMod, getViewerId, getConfig, saveConfig, isLoopbackRequest, sendErrorPage, about }),
routes,
async (ctx) => {
if (ctx.body != null || (ctx.method !== 'GET' && ctx.method !== 'HEAD')) return;

View file

@ -3,7 +3,7 @@
//
// Va en la cadena justo delante de las rutas del dev (backend.js lo monta con una
// sola linea). Lo que no atiende pasa al del dev con next(). Aqui viven:
// - mobile_layer.js: lo que el movil necesita del backend
// - mobile_layer.js: lanzador, filtro y avatar de la barra de arriba
// - wrapUpstream: mira lo que contesta el dev (seccion apagada, guardar modulos)
// - las rutas propias: tema y Modulos en el movil, barra de abajo, identidades
//
@ -51,10 +51,8 @@ const wrapUpstream = async (ctx, next) => {
}
};
module.exports = ({ checkMod, getViewerId, getConfig, saveConfig, isLoopbackRequest, sendErrorPage,
about, backupModel, onboardingModel }) => {
const mobileLayer = require('./mobile_layer')({ getConfig, saveConfig, getViewerId, about, backupModel, onboardingModel,
checkMod, isLoopbackRequest, sendErrorPage });
module.exports = ({ checkMod, getViewerId, getConfig, saveConfig, isLoopbackRequest, sendErrorPage, about }) => {
const mobileLayer = require('./mobile_layer')({ getViewerId, about });
const forkRouter = new koaRouter();
forkRouter

View file

@ -1,49 +1,20 @@
"use strict";
// Lo que el movil necesita del backend, fuera de backend.js.
// Lo que la interfaz de la rama necesita del backend, fuera de backend.js.
//
// Antes eran trozos metidos en backend.js (rutas, middleware y refreshAll), que es
// del dev y cambia en 34 de cada 39 versiones. Ahora va aqui y lo monta el router
// propio (fork_routes.js) delante de las rutas del dev. Nada de esto corre en
// escritorio. Ver GUIA_DE_DESARROLLO.md.
// Prepara lo que leen la barra de arriba y el lanzador de hive_views.js: el filtro
// Personal/Social, si el lanzador se ensena en esta pagina y el avatar propio. Lo
// monta el router propio (fork_routes.js) delante de las rutas del dev. Nada de
// esto corre en escritorio. Ver GUIA_DE_DESARROLLO.md.
//
// Hace cuatro cosas:
// - corta en el movil rutas que alli no tienen sentido (blockexplorer, actualizar
// con git, presets de modulos); el cambio de tema lo atiende fork_routes.js
// - mantiene apagados los modulos que no van en el movil, al arrancar y al guardar
// - prepara lo que leen la barra de arriba y el lanzador (avatar, filtro, lanzador)
// - retoca el HTML que sale (videos con fotograma, pestaña actual marcada)
// Y sirve las dos descargas de la copia de seguridad, que en el movil van por GET.
// Lo demas que el movil necesita del backend (modulos que no van, rutas que se
// cortan, retoques del HTML, descargas de la copia de seguridad) lo hace el propio
// dev en la version de movil de su backend.js, la que va dentro de su APK.
const fs = require('fs');
const os = require('os');
const path = require('path');
const compose = require('../server/node_modules/koa-compose');
const MOBILE_UNAVAILABLE_MODS = ['ai', 'aiNav', 'dev', 'games', 'logs', 'maps', 'pixelia'];
const NULL_IMG = '&' + '0'.repeat(43) + '=.sha256';
const TAB_PATHS = ['/mentions', '/inbox', '/phone', '/data', '/tags', '/search', '/settings'];
const isMobile = () => process.env.OASIS_MOBILE === '1';
const isPage = (p) => !/^\/(assets|image|blob|qr|c)(\/|$)/.test(p);
module.exports = ({ getConfig, saveConfig, getViewerId, about, backupModel, onboardingModel,
checkMod, isLoopbackRequest, sendErrorPage }) => {
// --- al arrancar: modulos que no van en el movil, apagados; y la cartera de
// pruebas que traia la config de serie, fuera
if (isMobile()) {
try {
const cfg = getConfig();
let dirty = false;
MOBILE_UNAVAILABLE_MODS.forEach(m => { if (cfg.modules && cfg.modules[`${m}Mod`] !== 'off') { cfg.modules[`${m}Mod`] = 'off'; dirty = true; } });
const w = cfg.wallet || {};
if (w.url === 'http://127.0.0.1:17474' && w.user === 'oasis' && w.pass === 'testpass123') {
cfg.wallet = { ...w, url: 'http://127.0.0.1:7474', user: '', pass: '' };
dirty = true;
}
if (dirty) saveConfig(cfg);
} catch (_) {}
}
// --- avatar propio para el circulo de la barra de arriba (lo lee hive_views)
module.exports = ({ getViewerId, about }) => {
let avatarAt = 0;
const refreshAvatar = async () => {
avatarAt = Date.now();
@ -56,78 +27,12 @@ module.exports = ({ getConfig, saveConfig, getViewerId, about, backupModel, onbo
} catch (_) {}
};
// --- rutas que en el movil se cortan antes de llegar al dev
const guards = async (ctx, next) => {
if (!isMobile()) return next();
const p = ctx.path;
if (ctx.method === 'GET' && (p === '/blockexplorer' || p.startsWith('/blockexplorer/block/'))) { ctx.redirect('/'); return; }
if (ctx.method === 'POST' && p === '/update') { ctx.redirect('/settings'); return; }
if (ctx.method === 'POST' && p === '/modules/preset') { ctx.redirect('/modules'); return; }
return async (ctx, next) => {
if (!isMobile() || !isPage(ctx.path)) return next();
global.__OASIS_HIVE_FILTER__ = (ctx.path === '/activity' && (ctx.query.hive === 'personal' || ctx.query.hive === 'community')) ? ctx.query.hive : '';
global.__OASIS_SHOW_HIVE__ = (ctx.path === '/activity' || ctx.path === '/');
if (!avatarAt) await refreshAvatar();
else if (Date.now() - avatarAt > 60000) refreshAvatar();
return next();
};
// --- antes y despues de las rutas del dev
const around = async (ctx, next) => {
if (!isMobile()) return next();
if (isPage(ctx.path)) {
// filtro de la home (Personal/Social) y lanzador solo en la portada
global.__OASIS_HIVE_FILTER__ = (ctx.path === '/activity' && (ctx.query.hive === 'personal' || ctx.query.hive === 'community')) ? ctx.query.hive : '';
global.__OASIS_SHOW_HIVE__ = (ctx.path === '/activity' || ctx.path === '/');
if (!avatarAt) await refreshAvatar();
else if (Date.now() - avatarAt > 60000) refreshAvatar();
}
await next();
// guardar modulos: los que no van en el movil siguen apagados
if (ctx.method === 'POST' && ctx.path === '/save-modules' && ctx.status >= 300 && ctx.status < 400) {
try {
const cfg = getConfig();
MOBILE_UNAVAILABLE_MODS.forEach(m => { cfg.modules[`${m}Mod`] = 'off'; });
saveConfig(cfg);
} catch (_) {}
}
if (typeof ctx.body !== 'string' || !String(ctx.type || '').includes('html')) return;
// los videos sin poster salen en negro en el WebView: se pide el fotograma 0.1
ctx.body = ctx.body.replace(/<video\b[^>]*>/g, (tag) => tag.replace(/\bsrc="(\/blob\/[^"#]+)"/, 'src="$1#t=0.1"'));
// la pestaña de la cabecera que corresponde a esta pagina
const tab = TAB_PATHS.find((t) => ctx.path === t || ctx.path.startsWith(t + '/'));
const headerEnd = ctx.body.indexOf('<div class="main-content">');
if (tab && headerEnd > 0) ctx.body = ctx.body.slice(0, headerEnd).replace(`<a href="${tab}"`, `<a href="${tab}" aria-current="page"`) + ctx.body.slice(headerEnd);
};
// --- copia de seguridad: en el movil se descarga por GET (el WebView no guarda
// la respuesta de un POST como fichero)
const backup = async (ctx, next) => {
if (ctx.method !== 'GET' || (ctx.path !== '/backup/keys/export' && ctx.path !== '/backup/export')) return next();
if (!isLoopbackRequest(ctx)) { ctx.status = 403; ctx.body = ''; return; }
if (!isMobile()) { ctx.redirect('/backup'); return; }
const q = ctx.query || {};
const pw = String(q.password || '');
if (!pw || pw.length < 32) { ctx.redirect('/backup'); return; }
if (ctx.path === '/backup/keys/export') {
try {
const { filename, data } = await backupModel.exportKeys({ password: pw });
ctx.set('Content-Type', 'application/octet-stream');
ctx.set('Content-Disposition', `attachment; filename="${filename}"`);
ctx.set('Content-Length', String(data.length));
ctx.body = data;
try { onboardingModel.markStep('backup'); } catch (_) {}
} catch (error) { sendErrorPage(ctx, error.message); }
return;
}
if (!checkMod(ctx, 'backupMod')) { ctx.redirect('/modules'); return; }
const rawMods = q.modules === undefined ? [] : (Array.isArray(q.modules) ? q.modules : [q.modules]);
const outPath = path.join(os.tmpdir(), `oasis-backup-${process.pid}-${Date.now()}.oasisbk`);
try {
const res = await backupModel.createBackup({ scope: q.scope, blobs: q.blobs, modules: rawMods, since: q.since }, pw, outPath);
ctx.set('Content-Type', 'application/octet-stream');
ctx.set('Content-Disposition', `attachment; filename="${res.filename}"`);
ctx.set('Content-Length', String(res.bytes));
const stream = fs.createReadStream(outPath);
stream.on('close', () => { try { fs.unlinkSync(outPath); } catch (_) {} });
ctx.body = stream;
try { onboardingModel.markStep('backup'); } catch (_) {}
} catch (error) { try { fs.unlinkSync(outPath); } catch (_) {} sendErrorPage(ctx, error.message); }
};
return compose([around, guards, backup]);
};

View file

@ -66,13 +66,17 @@ const modulesView = () => {
{ name: 'wiki', label: i18n.modulesWikiLabel, description: i18n.modulesWikiDescription }
].sort((x, y) => String(x.label || x.name).localeCompare(String(y.label || y.name), undefined, { sensitivity: 'base' }));
const moduleStates = modules.reduce((acc, mod) => {
const MOBILE_UNAVAILABLE = ['ai', 'aiNav', 'dev', 'games', 'logs', 'maps', 'pixelia'];
const isMobile = process.env.OASIS_MOBILE === '1';
const visibleModules = isMobile ? modules.filter(mod => !MOBILE_UNAVAILABLE.includes(mod.name)) : modules;
const moduleStates = visibleModules.reduce((acc, mod) => {
acc[`${mod.name}Mod`] = config[`${mod.name}Mod`] === 'on' ? 'on' : 'off';
return acc;
}, {});
const activeModulesCount = modules.filter(mod => moduleStates[`${mod.name}Mod`] === 'on').length;
const totalModulesCount = modules.length;
const activeModulesCount = visibleModules.filter(mod => moduleStates[`${mod.name}Mod`] === 'on').length;
const totalModulesCount = visibleModules.length;
const disabledModulesCount = totalModulesCount - activeModulesCount;
const header = [div({ class: 'tags-header module-header-line' },
@ -100,7 +104,7 @@ const modulesView = () => {
const presetButtons = div({ class: 'preset-group' },
Object.entries(PRESETS).map(([key, mods]) => {
const presetLabel = (i18n[`modulesPreset_${key}`] || key).toUpperCase();
const isActive = modules.every(m => mods.includes(m.name) === (moduleStates[`${m.name}Mod`] === 'on'));
const isActive = visibleModules.every(m => mods.includes(m.name) === (moduleStates[`${m.name}Mod`] === 'on'));
return form({ action: "/modules/preset", method: "post", class: "modules-preset-form" },
input({ type: "hidden", name: "preset", value: key }),
button({
@ -115,8 +119,7 @@ const modulesView = () => {
i18n.modules,
section(header),
section(
h2(i18n.modulesPresetTitle || "Common Configurations"),
presetButtons,
...(isMobile ? [] : [h2(i18n.modulesPresetTitle || "Common Configurations"), presetButtons]),
counts,
form(
{ action: "/save-modules", method: "post" },
@ -127,7 +130,7 @@ const modulesView = () => {
td(i18n.modulesModuleDescription),
td({ class: 'module-status-cell' }, i18n.modulesModuleStatus)
),
modules.map(mod =>
visibleModules.map(mod =>
tr(
td(a({ href: `/${mod.name}` }, mod.label)),
td(p(mod.description)),