From 1148e6a472bc296de174064eeb78bc9a9a6a029f Mon Sep 17 00:00:00 2001 From: hacklab Date: Mon, 19 Oct 2026 13:31:24 +0200 Subject: [PATCH] backend: lo del movil sale de backend.js a mobile_layer.js MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit De 20 trozos nuestros en backend.js quedan 9 (21 lineas). Pasan a src/backend/mobile_layer.js, que monta el router propio delante de las rutas del dev: - blockexplorer, actualizar con git y presets de modulos, cortados en el movil - los modulos que no van en el movil, apagados al arrancar y al guardar - avatar de la barra de arriba, filtro y lanzador - fotograma en los videos y pestaña actual marcada - descargas de la copia de seguridad por GET El cambio de tema del movil lo atiende karvan_routes.js. --- GUIA_DE_DESARROLLO.md | 11 ++- src/backend/backend.js | 89 ++--------------------- src/backend/karvan_routes.js | 20 +++++- src/backend/mobile_layer.js | 133 +++++++++++++++++++++++++++++++++++ 4 files changed, 164 insertions(+), 89 deletions(-) create mode 100644 src/backend/mobile_layer.js diff --git a/GUIA_DE_DESARROLLO.md b/GUIA_DE_DESARROLLO.md index 76dcf1f..3434333 100644 --- a/GUIA_DE_DESARROLLO.md +++ b/GUIA_DE_DESARROLLO.md @@ -69,6 +69,9 @@ Aquí se puede hacer lo que haga falta. - `src/views/hive_views.js`: lanzador y barras de arriba y abajo. - `src/views/mobile_icons.js`: los iconos. - `src/backend/karvan_routes.js`: nuestro router. Va justo delante del del dev. +- `src/backend/mobile_layer.js`: lo que el móvil necesita del backend (rutas que se + cortan, módulos que no van, avatar y lanzador, retoques del HTML, descargas de la + copia de seguridad). La monta `karvan_routes.js`. - `src/models/fork_workflows.js`: nuestros workflows. - `src/client/assets/translations/i18n_fork.js`: nuestros textos. - `src/client/assets/images/ux-*.svg`: nuestras imágenes. @@ -172,8 +175,12 @@ inspeccionar la WebView desde `chrome://inspect`. Cosas de antes de estas normas que aún no las cumplen, por orden de lo que más duele: -1. Quedan 21 trozos nuestros en `backend.js` (copia de seguridad, avatar de la barra - de arriba, vídeos, filtro del lanzador...). Tienen que irse al router propio. +1. En `backend.js` quedan 9 trozos pequeños (21 líneas) que no se pueden sacar desde + fuera: tres expresiones regulares que con el Node 18 de Android tumbaban el + arranque (esto habría que contárselo al dev), no abrir el navegador al arrancar, + no repetir fotos ya limpiadas, el modo chats, Karvan en la lista de Módulos de + escritorio y la línea que monta nuestro router. Lo demás ya está en + `src/backend/mobile_layer.js`. 2. En `workflows_model.js` queda la logica de skins del movil (`themeOf`, `sameTheme`): `settings_view.js` coge `currentWorkflow` al cargar y no se puede envolver desde fuera sin tocar esa vista. Es poco, pero es del dev. diff --git a/src/backend/backend.js b/src/backend/backend.js index 2b9ac3a..5c20d34 100644 --- a/src/backend/backend.js +++ b/src/backend/backend.js @@ -1644,17 +1644,6 @@ 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") { @@ -5114,7 +5103,6 @@ router }) .get('/blockexplorer', async (ctx) => { - if (process.env.OASIS_MOBILE === '1') { ctx.redirect('/'); return; } const userId = getViewerId(); const query = ctx.query || {}; const search = { @@ -5165,7 +5153,6 @@ 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 = { @@ -8487,7 +8474,7 @@ router profile: true, ux: true, workflow: isLoopbackRequest(ctx), - theme: true, // en el movil ofrece los skins de movil (WIZARD_THEMES) + theme: true, community: checkMod(ctx, 'invitesMod'), greeting: checkMod(ctx, 'feedMod') }; @@ -11522,40 +11509,6 @@ 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; @@ -14648,7 +14601,6 @@ 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 }); @@ -14669,8 +14621,6 @@ router }) .post("/settings/theme", koaBody(), async (ctx) => { const theme = String(ctx.request.body.theme || "").trim(), cfg = getConfig(); - // en el movil solo los skins de movil: un tema de escritorio deja la app sin maqueta - if (process.env.OASIS_MOBILE === '1' && !require('../views/main_views').MOBILE_THEMES.some(([value]) => value === theme)) { ctx.redirect("/settings#theme"); return; } cfg.themes.current = theme || "Dark-SNH"; saveConfig(cfg); ctx.cookies.set("theme", cfg.themes.current, { httpOnly: true, sameSite: 'strict', secure: ctx.secure }); @@ -15177,7 +15127,6 @@ 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 || ''); @@ -15189,14 +15138,10 @@ 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`); }) @@ -15347,19 +15292,6 @@ const folderBytes = async (dir) => { return bytes; }; const refreshAll = async (now) => { - try { - // avatar propio para el circulo de la topbar movil (lo lee renderMobileTopbar) - const meId = getViewerId(); - const selfImg = meId ? await about.image(meId) : null; - const NULL_IMG = '&' + '0'.repeat(43) + '=.sha256'; - if (typeof selfImg === 'string' && selfImg[0] === '&' && selfImg !== NULL_IMG) { - global.__OASIS_SELF_AVATAR__ = `/image/256/${encodeURIComponent(selfImg)}`; - } else if (typeof selfImg === 'string' && selfImg[0] === '/') { - global.__OASIS_SELF_AVATAR__ = selfImg; - } else { - global.__OASIS_SELF_AVATAR__ = null; - } - } catch (_) {} try { await bankingModel.ensureSelfAddressPublished(); } catch (_) {} try { await bankingModel.getUserEngagementScore(getViewerId()); } catch (_) {} try { @@ -15479,14 +15411,6 @@ 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(/]*>/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('
'); - if (tab && headerEnd > 0) ctx.body = ctx.body.slice(0, headerEnd).replace(` { 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(); @@ -15586,18 +15510,13 @@ const middleware = [ }, async (ctx, next) => { if (!ctx.path.startsWith('/assets/') && !ctx.path.startsWith('/image/') && !ctx.path.startsWith('/blob/') && !ctx.path.startsWith('/qr') && !ctx.path.startsWith('/c/')) { - // filtro de la home (Personal/Community); lo leen la topbar y el lanzador - global.__OASIS_HIVE_FILTER__ = (ctx.path === '/activity' && (ctx.query.hive === 'personal' || ctx.query.hive === 'community')) ? ctx.query.hive : ''; - // el lanzador de modulos SOLO en la home; en el resto de paginas el top queda limpio - global.__OASIS_SHOW_HIVE__ = (ctx.path === '/activity' || ctx.path === '/'); if (Date.now() - sharedState.getLastRefresh() > 60000) kickRefresh(); } await next(); }, - // rutas propias de la rama (Karvan, barra inferior, exportacion): fuera de la cadena de upstream - require('./karvan_routes')({ cooler, pull, pmModel, checkMod, getViewerId, - getConfig, saveConfig, - isLoopbackRequest, sendErrorPage }), + // la rama movil: su router y su capa de backend (ver GUIA_DE_DESARROLLO.md) + require('./karvan_routes')({ cooler, pull, pmModel, checkMod, getViewerId, getConfig, saveConfig, + isLoopbackRequest, sendErrorPage, about, backupModel, onboardingModel }), routes, async (ctx) => { if (ctx.body != null || (ctx.method !== 'GET' && ctx.method !== 'HEAD')) return; diff --git a/src/backend/karvan_routes.js b/src/backend/karvan_routes.js index 8f64450..6deda6b 100644 --- a/src/backend/karvan_routes.js +++ b/src/backend/karvan_routes.js @@ -60,7 +60,10 @@ const wrapUpstream = async (ctx, next) => { }; module.exports = ({ cooler, pull, pmModel, checkMod, getViewerId, - getConfig, saveConfig, isLoopbackRequest, sendErrorPage }) => { + getConfig, saveConfig, isLoopbackRequest, sendErrorPage, + about, backupModel, onboardingModel }) => { +const mobileLayer = require('./mobile_layer')({ getConfig, saveConfig, getViewerId, about, backupModel, onboardingModel, + checkMod, isLoopbackRequest, sendErrorPage }); // 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. @@ -116,6 +119,19 @@ async function karvanInviteAuthor(roomId) { const forkRouter = new koaRouter(); forkRouter + // tema en el movil: solo los skins de movil (uno de escritorio deja la app sin + // maqueta). Hace lo mismo que la ruta del dev, con esa comprobacion delante + .post('/settings/theme', koaBody(), async (ctx, next) => { + if (!isMobile()) return next(); + const theme = String((ctx.request.body || {}).theme || '').trim(); + const { MOBILE_THEMES } = require('../views/main_views'); + if (!MOBILE_THEMES.some(([value]) => value === theme)) { ctx.redirect('/settings#theme'); return; } + const cfg = getConfig(); + cfg.themes.current = theme; + saveConfig(cfg); + ctx.cookies.set('theme', theme, { httpOnly: true, sameSite: 'strict', secure: ctx.secure }); + ctx.redirect('/settings#theme'); + }) // Modulos en el movil: la vista propia (mobile_modules_view.js); en escritorio, la de upstream .get('/modules', async (ctx, next) => { if (!isMobile()) return next(); @@ -287,5 +303,5 @@ async function karvanInviteAuthor(roomId) { setTimeout(() => process.exit(0), 500); }); - return compose([wrapUpstream, forkRouter.routes()]); + return compose([mobileLayer, wrapUpstream, forkRouter.routes()]); }; diff --git a/src/backend/mobile_layer.js b/src/backend/mobile_layer.js new file mode 100644 index 0000000..f234da3 --- /dev/null +++ b/src/backend/mobile_layer.js @@ -0,0 +1,133 @@ +"use strict"; +// Lo que el movil 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 (karvan_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 karvan_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. + +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) + let avatarAt = 0; + const refreshAvatar = async () => { + avatarAt = Date.now(); + try { + const me = getViewerId(); + const img = me ? await about.image(me) : null; + if (typeof img === 'string' && img[0] === '&' && img !== NULL_IMG) global.__OASIS_SELF_AVATAR__ = `/image/256/${encodeURIComponent(img)}`; + else if (typeof img === 'string' && img[0] === '/') global.__OASIS_SELF_AVATAR__ = img; + else global.__OASIS_SELF_AVATAR__ = null; + } 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 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(/]*>/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('
'); + if (tab && headerEnd > 0) ctx.body = ctx.body.slice(0, headerEnd).replace(` { + 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]); +};