backend: lo del movil sale de backend.js a mobile_layer.js
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.
This commit is contained in:
parent
e7892c4bf9
commit
1148e6a472
4 changed files with 164 additions and 89 deletions
|
|
@ -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/hive_views.js`: lanzador y barras de arriba y abajo.
|
||||||
- `src/views/mobile_icons.js`: los iconos.
|
- `src/views/mobile_icons.js`: los iconos.
|
||||||
- `src/backend/karvan_routes.js`: nuestro router. Va justo delante del del dev.
|
- `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/models/fork_workflows.js`: nuestros workflows.
|
||||||
- `src/client/assets/translations/i18n_fork.js`: nuestros textos.
|
- `src/client/assets/translations/i18n_fork.js`: nuestros textos.
|
||||||
- `src/client/assets/images/ux-*.svg`: nuestras imágenes.
|
- `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
|
Cosas de antes de estas normas que aún no las cumplen, por orden de lo que más
|
||||||
duele:
|
duele:
|
||||||
|
|
||||||
1. Quedan 21 trozos nuestros en `backend.js` (copia de seguridad, avatar de la barra
|
1. En `backend.js` quedan 9 trozos pequeños (21 líneas) que no se pueden sacar desde
|
||||||
de arriba, vídeos, filtro del lanzador...). Tienen que irse al router propio.
|
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`):
|
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
|
`settings_view.js` coge `currentWorkflow` al cargar y no se puede envolver desde
|
||||||
fuera sin tocar esa vista. Es poco, pero es del dev.
|
fuera sin tocar esa vista. Es poco, pero es del dev.
|
||||||
|
|
|
||||||
|
|
@ -1644,17 +1644,6 @@ debug("Current configuration: %O", config);
|
||||||
debug(`You can save the above to ${defaultConfigFile} to make \
|
debug(`You can save the above to ${defaultConfigFile} to make \
|
||||||
these settings the default. See the readme for details.`);
|
these settings the default. See the readme for details.`);
|
||||||
const { saveConfig, getConfig } = require('../configs/config-manager');
|
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";
|
const oasisCheckPath = "/.well-known/oasis";
|
||||||
process.on("uncaughtException", function (err) {
|
process.on("uncaughtException", function (err) {
|
||||||
if (err["code"] === "EADDRINUSE") {
|
if (err["code"] === "EADDRINUSE") {
|
||||||
|
|
@ -5114,7 +5103,6 @@ router
|
||||||
})
|
})
|
||||||
|
|
||||||
.get('/blockexplorer', async (ctx) => {
|
.get('/blockexplorer', async (ctx) => {
|
||||||
if (process.env.OASIS_MOBILE === '1') { ctx.redirect('/'); return; }
|
|
||||||
const userId = getViewerId();
|
const userId = getViewerId();
|
||||||
const query = ctx.query || {};
|
const query = ctx.query || {};
|
||||||
const search = {
|
const search = {
|
||||||
|
|
@ -5165,7 +5153,6 @@ router
|
||||||
ctx.body = renderBlockchainView(blockchainData, filter, userId, search, { inspect, censusBlocks });
|
ctx.body = renderBlockchainView(blockchainData, filter, userId, search, { inspect, censusBlocks });
|
||||||
})
|
})
|
||||||
.get('/blockexplorer/block/:id', async (ctx) => {
|
.get('/blockexplorer/block/:id', async (ctx) => {
|
||||||
if (process.env.OASIS_MOBILE === '1') { ctx.redirect('/'); return; }
|
|
||||||
const userId = getViewerId();
|
const userId = getViewerId();
|
||||||
const query = ctx.query || {};
|
const query = ctx.query || {};
|
||||||
const search = {
|
const search = {
|
||||||
|
|
@ -8487,7 +8474,7 @@ router
|
||||||
profile: true,
|
profile: true,
|
||||||
ux: true,
|
ux: true,
|
||||||
workflow: isLoopbackRequest(ctx),
|
workflow: isLoopbackRequest(ctx),
|
||||||
theme: true, // en el movil ofrece los skins de movil (WIZARD_THEMES)
|
theme: true,
|
||||||
community: checkMod(ctx, 'invitesMod'),
|
community: checkMod(ctx, 'invitesMod'),
|
||||||
greeting: checkMod(ctx, 'feedMod')
|
greeting: checkMod(ctx, 'feedMod')
|
||||||
};
|
};
|
||||||
|
|
@ -11522,40 +11509,6 @@ router
|
||||||
try { await forumModel.removeOpenInvite(ctx.params.id); } catch (_) { actionFail(ctx); return; }
|
try { await forumModel.removeOpenInvite(ctx.params.id); } catch (_) { actionFail(ctx); return; }
|
||||||
ctx.redirect(safeReturnTo(ctx, `/forum/${encodeURIComponent(ctx.params.id)}`, ['/forum']));
|
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) => {
|
.post('/backup/keys/export', koaBody(), async (ctx) => {
|
||||||
if (!isLoopbackRequest(ctx)) { ctx.status = 403; ctx.body = ''; return; }
|
if (!isLoopbackRequest(ctx)) { ctx.status = 403; ctx.body = ''; return; }
|
||||||
const pw = ctx.request.body.password;
|
const pw = ctx.request.body.password;
|
||||||
|
|
@ -14648,7 +14601,6 @@ router
|
||||||
})
|
})
|
||||||
.post("/update", koaBody(), async (ctx) => {
|
.post("/update", koaBody(), async (ctx) => {
|
||||||
if (!isLoopbackRequest(ctx)) { ctx.status = 403; ctx.body = ''; return; }
|
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 exec = require("node:util").promisify(require("node:child_process").exec);
|
||||||
const repoRoot = path.resolve(__dirname, '..', '..');
|
const repoRoot = path.resolve(__dirname, '..', '..');
|
||||||
const { stdout, stderr } = await exec("git reset --hard && git pull", { cwd: repoRoot });
|
const { stdout, stderr } = await exec("git reset --hard && git pull", { cwd: repoRoot });
|
||||||
|
|
@ -14669,8 +14621,6 @@ router
|
||||||
})
|
})
|
||||||
.post("/settings/theme", koaBody(), async (ctx) => {
|
.post("/settings/theme", koaBody(), async (ctx) => {
|
||||||
const theme = String(ctx.request.body.theme || "").trim(), cfg = getConfig();
|
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";
|
cfg.themes.current = theme || "Dark-SNH";
|
||||||
saveConfig(cfg);
|
saveConfig(cfg);
|
||||||
ctx.cookies.set("theme", cfg.themes.current, { httpOnly: true, sameSite: 'strict', secure: ctx.secure });
|
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) => {
|
.post("/modules/preset", koaBody(), async (ctx) => {
|
||||||
if (!isLoopbackRequest(ctx)) { ctx.status = 403; ctx.body = ''; return; }
|
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 ALL_MODULES = workflowsModel.ALL_MODULES;
|
||||||
const PRESETS = workflowsModel.PRESETS;
|
const PRESETS = workflowsModel.PRESETS;
|
||||||
const preset = String(ctx.request.body.preset || '');
|
const preset = String(ctx.request.body.preset || '');
|
||||||
|
|
@ -15189,14 +15138,10 @@ router
|
||||||
ctx.redirect('/modules');
|
ctx.redirect('/modules');
|
||||||
})
|
})
|
||||||
.post("/save-modules", koaBody(), async (ctx) => {
|
.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; }
|
if (!isLoopbackRequest(ctx)) { ctx.status = 403; ctx.body = ''; return; }
|
||||||
const modules = workflowsModel.ALL_MODULES;
|
const modules = workflowsModel.ALL_MODULES;
|
||||||
const cfg = getConfig();
|
const cfg = getConfig();
|
||||||
modules.forEach(mod => cfg.modules[`${mod}Mod`] = ctx.request.body[`${mod}Form`] === 'on' ? 'on' : 'off');
|
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);
|
saveConfig(cfg);
|
||||||
ctx.redirect(`/modules`);
|
ctx.redirect(`/modules`);
|
||||||
})
|
})
|
||||||
|
|
@ -15347,19 +15292,6 @@ const folderBytes = async (dir) => {
|
||||||
return bytes;
|
return bytes;
|
||||||
};
|
};
|
||||||
const refreshAll = async (now) => {
|
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.ensureSelfAddressPublished(); } catch (_) {}
|
||||||
try { await bankingModel.getUserEngagementScore(getViewerId()); } catch (_) {}
|
try { await bankingModel.getUserEngagementScore(getViewerId()); } catch (_) {}
|
||||||
try {
|
try {
|
||||||
|
|
@ -15479,14 +15411,6 @@ const kickRefresh = () => {
|
||||||
};
|
};
|
||||||
setTimeout(() => { if (!sharedState.getLastRefresh()) kickRefresh(); }, 5000);
|
setTimeout(() => { if (!sharedState.getLastRefresh()) kickRefresh(); }, 5000);
|
||||||
const middleware = [
|
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) => {
|
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' };
|
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();
|
await next();
|
||||||
|
|
@ -15586,18 +15510,13 @@ const middleware = [
|
||||||
},
|
},
|
||||||
async (ctx, next) => {
|
async (ctx, next) => {
|
||||||
if (!ctx.path.startsWith('/assets/') && !ctx.path.startsWith('/image/') && !ctx.path.startsWith('/blob/') && !ctx.path.startsWith('/qr') && !ctx.path.startsWith('/c/')) {
|
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();
|
if (Date.now() - sharedState.getLastRefresh() > 60000) kickRefresh();
|
||||||
}
|
}
|
||||||
await next();
|
await next();
|
||||||
},
|
},
|
||||||
// rutas propias de la rama (Karvan, barra inferior, exportacion): fuera de la cadena de upstream
|
// la rama movil: su router y su capa de backend (ver GUIA_DE_DESARROLLO.md)
|
||||||
require('./karvan_routes')({ cooler, pull, pmModel, checkMod, getViewerId,
|
require('./karvan_routes')({ cooler, pull, pmModel, checkMod, getViewerId, getConfig, saveConfig,
|
||||||
getConfig, saveConfig,
|
isLoopbackRequest, sendErrorPage, about, backupModel, onboardingModel }),
|
||||||
isLoopbackRequest, sendErrorPage }),
|
|
||||||
routes,
|
routes,
|
||||||
async (ctx) => {
|
async (ctx) => {
|
||||||
if (ctx.body != null || (ctx.method !== 'GET' && ctx.method !== 'HEAD')) return;
|
if (ctx.body != null || (ctx.method !== 'GET' && ctx.method !== 'HEAD')) return;
|
||||||
|
|
|
||||||
|
|
@ -60,7 +60,10 @@ const wrapUpstream = async (ctx, next) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
module.exports = ({ cooler, pull, pmModel, checkMod, getViewerId,
|
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
|
// 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).
|
// (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.
|
// 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();
|
const forkRouter = new koaRouter();
|
||||||
forkRouter
|
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
|
// Modulos en el movil: la vista propia (mobile_modules_view.js); en escritorio, la de upstream
|
||||||
.get('/modules', async (ctx, next) => {
|
.get('/modules', async (ctx, next) => {
|
||||||
if (!isMobile()) return next();
|
if (!isMobile()) return next();
|
||||||
|
|
@ -287,5 +303,5 @@ async function karvanInviteAuthor(roomId) {
|
||||||
setTimeout(() => process.exit(0), 500);
|
setTimeout(() => process.exit(0), 500);
|
||||||
});
|
});
|
||||||
|
|
||||||
return compose([wrapUpstream, forkRouter.routes()]);
|
return compose([mobileLayer, wrapUpstream, forkRouter.routes()]);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
133
src/backend/mobile_layer.js
Normal file
133
src/backend/mobile_layer.js
Normal file
|
|
@ -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(/<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]);
|
||||||
|
};
|
||||||
Loading…
Add table
Add a link
Reference in a new issue