OASIS_MOBILE/src/configs/config-manager.js
oasis-bot c332afd6ea 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.
2026-10-18 02:52:17 +02:00

209 lines
8.3 KiB
JavaScript

const fs = require('fs');
const path = require('path');
const os = require('os');
const legacyConfigPath = path.join(__dirname, 'oasis-config.json');
const stateDir = process.env.OASIS_STATE_DIR || process.env.ssb_path || path.join(os.homedir(), '.ssb');
const oasisDir = path.join(stateDir, 'oasis');
const configFilePath = path.join(oasisDir, 'oasis-config.json');
const defaultServerConfigPath = path.join(__dirname, 'server-config.json');
const serverConfigFilePath = path.join(oasisDir, 'oasis-server-config.json');
for (const [from, to] of [[path.join(stateDir, 'oasis-config.json'), configFilePath], [path.join(stateDir, 'oasis-server-config.json'), serverConfigFilePath]]) {
try {
if (!fs.existsSync(to) && fs.existsSync(from)) {
fs.mkdirSync(oasisDir, { recursive: true, mode: 0o700 });
fs.renameSync(from, to);
}
} catch (_) {}
}
const writeStateFile = (file, data) => {
fs.mkdirSync(path.dirname(file), { recursive: true, mode: 0o700 });
const tmp = `${file}.tmp-${process.pid}-${Date.now()}`;
fs.writeFileSync(tmp, data, { encoding: 'utf8', mode: 0o600 });
fs.renameSync(tmp, file);
try { fs.chmodSync(file, 0o600); } catch (_) {}
};
let configCache = { at: 0, cfg: null };
const CONFIG_TTL_MS = 2000;
const writeConfigFile = (data) => { configCache = { at: 0, cfg: null }; return writeStateFile(configFilePath, data); };
const readJsonFile = (file) => { try { return JSON.parse(fs.readFileSync(file, 'utf8')); } catch (_) { return null; } };
const isPlainObject = (v) => !!v && typeof v === 'object' && !Array.isArray(v);
const mergeDeep = (base, over) => {
if (!isPlainObject(base) || !isPlainObject(over)) return over === undefined ? base : over;
const out = { ...base };
for (const [k, v] of Object.entries(over)) out[k] = isPlainObject(v) && isPlainObject(base[k]) ? mergeDeep(base[k], v) : v;
return out;
};
if (!fs.existsSync(serverConfigFilePath)) {
const shipped = readJsonFile(defaultServerConfigPath);
const hops = shipped && shipped.friends ? shipped.friends.hops : undefined;
try {
if (shipped && shipped.pub === true) writeStateFile(serverConfigFilePath, JSON.stringify(shipped, null, 2));
else if (Number.isFinite(hops) && hops !== 2) writeStateFile(serverConfigFilePath, JSON.stringify({ friends: { hops } }, null, 2));
} catch (_) {}
}
const readServerConfig = () => mergeDeep(readJsonFile(defaultServerConfigPath) || {}, readJsonFile(serverConfigFilePath) || {});
const saveServerConfig = (patch) => writeStateFile(serverConfigFilePath, JSON.stringify(mergeDeep(readJsonFile(serverConfigFilePath) || {}, patch), null, 2));
const inviteCodesPath = path.join(__dirname, 'invite-codes.json');
const readInviteCodes = () => {
const raw = readJsonFile(inviteCodesPath);
return (Array.isArray(raw) ? raw : raw ? [raw] : []).filter((e) => e && typeof e.code === 'string' && e.code.trim());
};
const defaultInvite = () => readInviteCodes()[0] || null;
if (!fs.existsSync(configFilePath) && fs.existsSync(legacyConfigPath)) {
try { writeConfigFile(fs.readFileSync(legacyConfigPath, 'utf8')); } catch (_) {}
}
if (!fs.existsSync(configFilePath)) {
const defaultConfig = {
"themes": {
"current": "Dark-SNH"
},
"ux": {
"current": "blocks"
},
"modules": {
"blogsMod": "on",
"housingMod": "off",
"karvanMod": "on",
"popularMod": "on",
"topicsMod": "on",
"summariesMod": "on",
"latestMod": "on",
"threadsMod": "on",
"multiverseMod": "on",
"pollsMod": "on",
"fediverseMod": "on",
"invitesMod": "on",
"walletMod": "on",
"backupMod": "on",
"devMod": "on",
"cipherMod": "on",
"bookmarksMod": "on",
"videosMod": "on",
"docsMod": "on",
"audiosMod": "on",
"tagsMod": "on",
"imagesMod": "on",
"trendingMod": "on",
"eventsMod": "on",
"tasksMod": "on",
"marketMod": "on",
"votesMod": "on",
"tribesMod": "on",
"reportsMod": "on",
"opinionsMod": "on",
"padsMod": "on",
"wikiMod": "on",
"emergenciesMod": "on",
"mailingMod": "on",
"roomsMod": "on",
"phoneMod": "on",
"logisticsMod": "on",
"podcastsMod": "on",
"campaignsMod": "on",
"calendarsMod": "on",
"transfersMod": "on",
"feedMod": "on",
"pixeliaMod": "on",
"melodyMod": "on",
"agendaMod": "on",
"aiMod": "on",
"aiNavMod": "on",
"forumMod": "on",
"gamesMod": "on",
"jobsMod": "on",
"shopsMod": "on",
"projectsMod": "on",
"industryMod": "on",
"bankingMod": "on",
"schoolMod": "on",
"parliamentMod": "on",
"courtsMod": "on",
"favoritesMod": "on",
"logsMod": "on",
"mapsMod": "on",
"chatsMod": "on",
"torrentsMod": "on",
"filesMod": "on",
"graphosMod": "on",
"larpMod": "on"
},
"wallet": {
"url": "http://localhost:7474",
"user": "",
"pass": "",
"fee": "5"
},
"ai": {
"prompt": "Provide an informative and precise response."
},
"ssbLogStream": {
"limit": 2000
},
"homePage": "activity",
"language": "en",
"wish": "whole",
"pmVisibility": "whole",
"phone": { "visibility": "mutuals", "dnd": false, "relay": true },
"lanBroadcasting": true
};
writeConfigFile(JSON.stringify(defaultConfig, null, 2));
}
const getConfig = () => {
const now = Date.now();
if (!configCache.cfg || now - configCache.at > CONFIG_TTL_MS) configCache = { at: now, cfg: readConfig() };
return structuredClone(configCache.cfg);
};
const readConfig = () => {
const configData = fs.readFileSync(configFilePath);
const cfg = JSON.parse(configData);
if (!['whole', 'mutuals', 'only-lan', 'local'].includes(cfg.wish)) cfg.wish = 'whole';
if (cfg.pmVisibility !== 'whole' && cfg.pmVisibility !== 'mutuals') cfg.pmVisibility = 'whole';
if (!cfg.phone || typeof cfg.phone !== 'object') cfg.phone = {};
cfg.phone = { visibility: cfg.phone.visibility === 'whole' ? 'whole' : 'mutuals', dnd: cfg.phone.dnd === true, relay: cfg.phone.relay !== false, video: cfg.phone.video !== false, ...(typeof cfg.phone.videoDevice === 'string' && cfg.phone.videoDevice.trim() ? { videoDevice: cfg.phone.videoDevice.trim() } : {}) };
if (typeof cfg.ux === 'string') cfg.ux = { current: cfg.ux };
if (!cfg.ux || typeof cfg.ux !== 'object') cfg.ux = { current: 'blocks' };
if (cfg.ux.current === 'menus') cfg.ux.current = 'blocks';
if (cfg.ux.current !== 'blocks' && cfg.ux.current !== 'ainav' && cfg.ux.current !== 'chats' && cfg.ux.current !== 'feed' && cfg.ux.current !== 'phone') cfg.ux.current = 'blocks';
if (cfg.ux.current === 'ainav' && cfg.modules && cfg.modules.aiNavMod !== 'on') cfg.ux.current = 'blocks';
if (cfg.ux.current === 'chats' && cfg.modules && cfg.modules.chatsMod !== 'on') cfg.ux.current = 'blocks';
let pins = Array.isArray(cfg.bottomBarPins)
? cfg.bottomBarPins
: (typeof cfg.bottomBarPin === 'string'
? (cfg.bottomBarPin ? [cfg.bottomBarPin] : [])
: ['search', 'inbox', 'publish']);
pins = pins.filter(k => typeof k === 'string' && k);
pins = pins.filter((k, i) => pins.indexOf(k) === i).slice(0, 4);
cfg.bottomBarPins = pins;
delete cfg.bottomBarPin;
if (cfg.ux.current === 'phone' && cfg.modules && cfg.modules.phoneMod === 'off') cfg.ux.current = 'blocks';
if (cfg.ux.current === 'feed' && cfg.modules && cfg.modules.feedMod === 'off') cfg.ux.current = 'blocks';
if (!cfg.blobCache || typeof cfg.blobCache !== 'object' || !Number.isFinite(Number(cfg.blobCache.maxMB))) cfg.blobCache = { maxMB: 0 };
if (!Number.isFinite(Number(cfg.blobCache.pubMaxMB))) cfg.blobCache.pubMaxMB = 0;
if (cfg.modules && typeof cfg.modules === 'object') {
if (cfg.modules.backupMod === undefined) cfg.modules.backupMod = cfg.modules.legacyMod === 'off' ? 'off' : 'on';
for (const mod of ['wikiMod', 'emergenciesMod', 'mailingMod', 'logisticsMod', 'podcastsMod', 'campaignsMod', 'filesMod', 'phoneMod', 'roomsMod']) {
if (cfg.modules[mod] === undefined) cfg.modules[mod] = 'on';
}
}
return cfg;
};
const saveConfig = (newConfig) => {
writeConfigFile(JSON.stringify(newConfig, null, 2));
};
module.exports = {
configFilePath,
serverConfigFilePath,
getConfig,
saveConfig,
readServerConfig,
saveServerConfig,
readInviteCodes,
defaultInvite,
};