Ctrl K
ComunidadGuías…

electron/companion-settings.mjs

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
import fs from "node:fs";
import os from "node:os";
import path from "node:path";

/** `companion-settings.json` in the app's user data: each way into this computer and whether the person turned it on.
 * `wifi` is Same Wi-Fi (the device port on the LAN, and Bonjour), `relay` is Anywhere, through MausBot, `tailscale`
 * the tailnet address (with `computers` when the person's other computers may open this one's server over it), `https`
 * the HTTPS address (the managed tunnel). `notices` lists routes kept on by the update that the person has not seen yet. */
export const COMPANION_SETTINGS_VERSION = 2;
export const ROUTES = Object.freeze(["wifi", "relay", "tailscale", "https"]);
/** A device seen this recently counts as using its routes. */
export const RECENT_DEVICE_MS = 30 * 24 * 60 * 60 * 1_000;

const off = () => ({ on: false, since: null });

export function defaultCompanionSettings() {
  return { version: COMPANION_SETTINGS_VERSION, keepAwake: false, routes: Object.fromEntries(ROUTES.map((route) => [route, off()])), notices: [] };
}

function readRoute(value, route) {
  if (!value || typeof value !== "object" || value.on !== true) return off();
  const since = Number.isSafeInteger(value.since) ? value.since : null;
  if (route === "tailscale") return { on: true, since, name: typeof value.name === "string" && /\.ts\.net$/i.test(value.name) ? value.name : null, ...(value.computers === true ? { computers: true } : {}) };
  return { on: true, since };
}

/**
 * Settings before version 2 had one flag that started everything at launch. The update keeps a route on only where a
 * paired device seen in the last 30 days could be using it, and lists each kept route as a notice: nothing paired keeps
 * nothing on. The sidecar records no route per device, so this goes by what a device could use: Same Wi-Fi for any
 * device; the relay for an end-to-end device not limited to home routes; the HTTPS address for a device not limited
 * to home routes while this computer has its managed address; Tailscale only while this computer has a tailnet address.
 * Anything unreadable is "off": these flags open network listeners.
 */
export function readCompanionSettings(raw, facts = {}) {
  const parsed = raw && typeof raw === "object" ? raw : {};
  const keepAwake = parsed.keepAwake === true;
  if (parsed.version === COMPANION_SETTINGS_VERSION) {
    const routes = Object.fromEntries(ROUTES.map((route) => [route, readRoute(parsed.routes?.[route], route)]));
    const notices = Array.isArray(parsed.notices) ? ROUTES.filter((route) => parsed.notices.includes(route) && routes[route].on) : [];
    return { version: COMPANION_SETTINGS_VERSION, keepAwake, routes, notices };
  }
  const settings = { ...defaultCompanionSettings(), keepAwake };
  if (parsed.enabled !== true) return settings;
  const now = facts.now ?? Date.now();
  const recent = (Array.isArray(facts.devices) ? facts.devices : []).filter((device) => Number.isFinite(device?.lastSeenAt) && now - device.lastSeenAt <= RECENT_DEVICE_MS);
  if (recent.length === 0) return settings;
  const away = recent.filter((device) => device.routes !== "local");
  const kept = {
    wifi: true,
    relay: away.some((device) => Number.isSafeInteger(device.e2eAt)),
    tailscale: facts.tailnetAddress === true,
    https: away.length > 0 && facts.managedAddress === true,
  };
  for (const route of ROUTES) if (kept[route]) settings.routes[route] = route === "tailscale" ? { on: true, since: now, name: null } : { on: true, since: now };
  settings.notices = ROUTES.filter((route) => kept[route]);
  return settings;
}

export const anyRouteOn = (settings) => ROUTES.some((route) => settings?.routes?.[route]?.on === true);

/** Same Wi-Fi and Tailscale are what the sidecar itself listens for; the relay and the HTTPS address reach it otherwise. */
export const directRoutesOn = (settings) => ({ wifi: settings?.routes?.wifi?.on === true, tailscale: settings?.routes?.tailscale?.on === true });

export function setRoute(settings, route, on, { now = Date.now(), name, computers } = {}) {
  if (!ROUTES.includes(route)) return settings;
  const current = settings.routes[route];
  const next = on ? { on: true, since: current.on ? current.since : now, ...(route === "tailscale" ? { name: name !== undefined ? name : current.name ?? null, ...((computers ?? current.computers) === true ? { computers: true } : {}) } : {}) } : off();
  return { ...settings, routes: { ...settings.routes, [route]: next }, notices: on ? settings.notices : settings.notices.filter((notice) => notice !== route) };
}

/**
 * The phone link's sidecar runs only for a computer that runs its own server, has a way in turned on and has a device
 * paired with it (or a pairing code open, which the dialog asks for). Never for a desktop that is only a client of
 * another computer.
 */
export function phoneLinkStartsAtLaunch({ remoteClient, serverReady, settings, pairedPhones }) {
  return !remoteClient && serverReady === true && anyRouteOn(settings) && (pairedPhones ?? 0) > 0;
}

/** A running sidecar with nothing paired and no code open goes back to sleep; its routes stay as set. */
export const phoneLinkShouldSleep = (state) =>
  state?.enabled === true && !state.error && state.devicesUnreadable !== true && Array.isArray(state.devices) && state.devices.length === 0 && !state.pairing;

export const companionDevicesFile = (env = process.env, home = os.homedir()) =>
  path.join(env.OMB_COMPANION_DIR ?? path.join(home, ".openmausbot-companion"), "devices.json");

/** Paired devices in the companion's own store; null when the file exists but cannot be read. */
export function readPairedDevices(file = companionDevicesFile()) {
  let text;
  try {
    text = fs.readFileSync(file, "utf8");
  } catch (error) {
    return error?.code === "ENOENT" ? [] : null;
  }
  try {
    const devices = JSON.parse(text)?.devices;
    return Array.isArray(devices) ? devices : null;
  } catch {
    return null;
  }
}

export const countPairedPhones = (file) => readPairedDevices(file)?.length ?? null;

/** Looks at this computer's interfaces only: Tailscale's own interface (`tailscale0`, "Tailscale", or the utun that
 * carries a fd7a:115c:a1e0::/48 address). A 100.64/10 address alone is not Tailscale: carrier NAT and other VPNs use it. */
export function hasTailnetAddress(interfaces = os.networkInterfaces()) {
  return Object.entries(interfaces).some(([name, entries]) =>
    (entries ?? []).length > 0 && (/^tailscale/i.test(name) || (entries ?? []).some((entry) => /^fd7a:115c:a1e0:/i.test(entry?.address ?? ""))));
}