electron/desktop-prefs.mjs
Rama main · réplica de lectura del repositorio original.
// What this desktop keeps whatever page its window shows, whichever port its own page is served on: the notices someone
// closed, its usage-analytics switch, the app's language, its answer to the product-updates email question, whether
// the person hid the window's rail of places and whether they turned the window's frame off.
import { randomBytes } from "node:crypto";
import fs from "node:fs";
import path from "node:path";
import nativeText from "./native-text.cjs";
import { onboardingHints } from "./onboarding-record.mjs";
import { DEFAULT_TALK_KEY, isTalkKey } from "./talk-keys.mjs";
import { createUpdatesConsent, parseUpdatesConsent, registerUpdatesConsent } from "./updates-consent.mjs";
import { accountFullName } from "./system-name.mjs";
export { DESKTOP_PORTS } from "./desktop-ports.mjs";
/** The only notices a page may record, so no page can write anything else into the file. */
export const DESKTOP_NOTICES = Object.freeze(["whats-new-2026-11", "boat-credit-2026-11", "grok-move-2026-11", "modelgo-offer-2026-11"]);
const WHATS_NEW = DESKTOP_NOTICES[0];
/** A locale code such as "de" or "pt-br", or "" for following the system. */
const LANGUAGE = /^(?:[a-z]{2,3}(?:-[a-z0-9]{2,8}){0,2})?$/i;
const isLanguage = value => typeof value === "string" && value.length <= 24 && LANGUAGE.test(value);
const CHAT_ID = /^[\w-]{1,128}$/;
const INSTALL_ID = /^[A-Za-z0-9_-]{22,64}$/;
/** Security: a write that fails (a full disk, a kill mid-write) leaves the last whole record, analytics opt-out included.
* A file another tool saved with a byte-order mark (Notepad on Windows) still reads. */
export function jsonFile(file, files = fs) {
return {
read: () => JSON.parse(files.readFileSync(file, "utf8").replace(/^\uFEFF/, "")),
write: value => {
const temporary = `${file}.${process.pid}.tmp`;
try {
files.writeFileSync(temporary, JSON.stringify(value), { mode: 0o600 });
files.renameSync(temporary, file);
} catch (error) {
try { files.rmSync(temporary, { force: true }); } catch {}
throw error;
}
},
};
}
const parseChat = value => value && typeof value === "object" && typeof value.botId === "string" && typeof value.threadId === "string"
&& CHAT_ID.test(value.botId) && CHAT_ID.test(value.threadId) ? { botId: value.botId, threadId: value.threadId } : null;
function parse(raw) {
const record = raw && typeof raw === "object" && !Array.isArray(raw) ? raw : {};
const seen = Array.isArray(record.seen) ? DESKTOP_NOTICES.filter(id => record.seen.includes(id)) : [];
const updatesConsent = parseUpdatesConsent(record.updatesConsent);
const quickAsks = record.quickAsks && typeof record.quickAsks === "object" && !Array.isArray(record.quickAsks)
? Object.fromEntries(Object.entries(record.quickAsks).flatMap(([server, chat]) => { const parsed = parseChat(chat); return parsed ? [[server, parsed]] : []; }))
: {};
return { seen, ...(typeof record.analytics === "boolean" ? { analytics: record.analytics } : {}),
...(isLanguage(record.language) ? { language: record.language } : {}),
...(typeof record.installId === "string" && INSTALL_ID.test(record.installId) ? { installId: record.installId } : {}),
...(updatesConsent ? { updatesConsent } : {}),
...(typeof record.talkShortcut === "boolean" ? { talkShortcut: record.talkShortcut } : {}),
...(isTalkKey(record.talkKey) ? { talkKey: record.talkKey } : {}),
...(typeof record.railHidden === "boolean" ? { railHidden: record.railHidden } : {}),
...(typeof record.windowFrame === "boolean" ? { windowFrame: record.windowFrame } : {}),
...(Object.keys(quickAsks).length ? { quickAsks } : {}) };
}
/** Main's own words in the language this desktop's record keeps, with that record as `prefs`. Electron knows the
* system's language only once the app is ready. */
export function createDesktopText(app, prefs = createDesktopPrefs(jsonFile(path.join(app.getPath("userData"), "desktop-prefs.json")))) {
return Object.assign(nativeText.createNativeText({ saved: prefs.language(), system: () => (app.isReady() ? app.getLocale() : "") }), { prefs });
}
export function relabelDesktop({ menu, trays, window, title, badge }) {
menu();
for (const tray of trays) tray?.relabel();
if (window && !window.isDestroyed()) window.setTitle(title());
badge();
}
/** A record that cannot be written is still kept for this launch. */
export function createDesktopPrefs({ read, write }) {
let record = null;
const current = () => {
if (record) return record;
let raw = null;
try { raw = read(); } catch {}
return (record = parse(raw));
};
const save = next => { record = next; try { write(next); } catch {} };
return {
seen: () => [...current().seen],
markSeen(id) {
const saved = current();
if (DESKTOP_NOTICES.includes(id) && !saved.seen.includes(id)) save({ ...saved, seen: [...saved.seen, id] });
},
/** null until a choice is saved. */
analytics: () => current().analytics ?? null,
setAnalytics(enabled) { save({ ...current(), analytics: enabled === true }); },
/** null until a language is picked; "" is a pick too: follow the system. */
language: () => current().language ?? null,
setLanguage(language) {
if (!isLanguage(language)) return false;
if (current().language !== language) save({ ...current(), language });
return true;
},
/** Random, made on first use: the only name the Admin's no-account consent endpoint counts this computer by. */
installId() {
const saved = current();
if (saved.installId) return saved.installId;
const installId = randomBytes(24).toString("base64url");
save({ ...saved, installId });
return installId;
},
updatesConsent: () => current().updatesConsent ?? null,
setUpdatesConsent(value) {
const updatesConsent = parseUpdatesConsent(value);
if (!updatesConsent) return false;
save({ ...current(), updatesConsent });
return true;
},
talkShortcut: () => current().talkShortcut ?? true,
setTalkShortcut(enabled) { save({ ...current(), talkShortcut: enabled === true }); },
talkKey: () => current().talkKey ?? DEFAULT_TALK_KEY,
setTalkKey(id) {
if (!isTalkKey(id)) return false;
if (current().talkKey !== id) save({ ...current(), talkKey: id });
return true;
},
quickAsksChat(server) {
const quickAsks = current().quickAsks;
return quickAsks && Object.hasOwn(quickAsks, server) ? { ...quickAsks[server] } : null;
},
setQuickAsksChat(server, chat) {
const parsed = typeof server === "string" ? parseChat(chat) : null;
if (parsed) save({ ...current(), quickAsks: { ...current().quickAsks, [server]: parsed } });
},
/** The window's rail of places is shown until the person hides it. */
railHidden: () => current().railHidden === true,
setRailHidden(hidden) {
const railHidden = hidden === true;
if ((current().railHidden === true) !== railHidden) save({ ...current(), railHidden });
},
/** The window draws its own frame until the person turns it off. */
windowFrame: () => current().windowFrame !== false,
setWindowFrame(on) {
const windowFrame = on !== false;
if ((current().windowFrame !== false) !== windowFrame) save({ ...current(), windowFrame });
},
};
}
/**
* `cloudAccount()`: the MausBot Cloud sign-in client, for the product-updates question (updates-consent.mjs), which is
* asked where Cloud offers are (`cloudOffered`); without it nothing is asked and null is returned.
* `page(event)`: "local" for this computer's page, "cloud" for My Cloud's own page as main verifies it, "server" for
* another saved server open in this desktop's window, null for anything else. `cloudOffered()`: this desktop sells
* MausBot Cloud, asked once its Cloud sign-in is restored. `localConfig()`: This computer's own workspace's config.json.
* `languageChanged(language, sender)`: tells this desktop's other pages the language picked on one of them;
* `onLanguage(language)`: tells main, which draws its menus, tray and dialogs in it. `onShown(language, sender)`: the
* language a page of the window shows, which main's words follow while nothing is picked. `prefs`: the record main reads too.
*/
export function registerDesktopPrefs({ prefs: given, ipcMain, read, write, page, cloudOffered, cloudAccount, localConfig = () => null, languageChanged = () => {}, onLanguage = () => {}, onShown = () => {}, frame = null, now = Date.now, systemName = accountFullName }) {
let prefs = given ?? null;
const store = () => prefs ??= createDesktopPrefs({ read, write });
const from = (channel, pages, handler) => (event, ...args) => {
const asking = page(event);
if (!pages.includes(asking)) throw new Error(`${channel} is only available in this app's window`);
return handler(asking, ...args);
};
// This computer's workspace record counts on every page, before This computer's page has loaded.
const state = async () => {
const cloud = await cloudOffered();
let hints = [];
try { hints = onboardingHints(localConfig()); } catch {}
const seen = store().seen(), closed = hints.includes(WHATS_NEW);
return { seen: closed && !seen.includes(WHATS_NEW) ? [...seen, WHATS_NEW] : seen, cloud };
};
const ANY = ["local", "cloud", "server"];
ipcMain.handle("desktop-notices:state", from("desktop-notices:state", ANY, state));
ipcMain.handle("desktop-notices:seen", from("desktop-notices:seen", ANY, (_, id) => store().markSeen(id)));
// A first run's default name for the profile: this computer's own page only, never a server's.
ipcMain.handle("desktop-profile:system-name", from("desktop-profile:system-name", ["local"], () => systemName()));
// Security: a server's page learns only that this desktop turned analytics off, never that it is on.
ipcMain.on("desktop-analytics:read", event => {
try {
const asking = page(event), saved = store().analytics();
event.returnValue = asking === "local" ? saved : asking && saved === false ? false : null;
} catch { event.returnValue = null; }
});
ipcMain.handle("desktop-analytics:set", from("desktop-analytics:set", ["local"], (_, enabled) => { store().setAnalytics(enabled === true); }));
ipcMain.on("desktop-language:read", event => {
try { event.returnValue = page(event) ? store().language() : null; } catch { event.returnValue = null; }
});
const setLanguage = from("desktop-language:set", ANY, (_, language) => {
const before = store().language();
return store().setLanguage(language) && before !== language;
});
ipcMain.handle("desktop-language:set", (event, language) => {
if (setLanguage(event, language)) {
try { languageChanged(language, event.sender); } catch {}
try { onLanguage(language); } catch {}
}
return store().language() === language;
});
ipcMain.on("desktop-language:shown", (event, language) => {
try { if (page(event) && isLanguage(language) && language) onShown(language, event.sender); } catch {}
});
// Every page of this desktop's window reads and sets the frame; a server's page sets it only from the person's click,
// which the preload checks.
const frameState = () => ({ on: store().windowFrame(), restart: Boolean(frame?.restarts) && frame.made() !== null && frame.made() !== store().windowFrame() });
ipcMain.handle("window-frame:state", from("window-frame:state", ANY, () => frameState()));
ipcMain.handle("window-frame:set", from("window-frame:set", ANY, (_, on) => {
store().setWindowFrame(on === true);
try { frame?.changed(); } catch {}
return frameState();
}));
if (!cloudAccount) return null;
const consent = createUpdatesConsent({ prefs: { installId: () => store().installId(), updatesConsent: () => store().updatesConsent(), setUpdatesConsent: value => store().setUpdatesConsent(value) },
account: cloudAccount, available: cloudOffered, now });
registerUpdatesConsent({ ipcMain, page, consent });
void cloudOffered().then(offered => offered && consent.sync()).catch(() => {});
return consent;
}