import fs from "node:fs"; import path from "node:path"; import {app, dialog} from "@electron/remote"; import * as Sentry from "@sentry/electron/renderer"; import {JsonDB} from "node-json-db"; import {DataError} from "node-json-db/dist/lib/Errors"; import {z} from "zod"; import * as EnterpriseUtil from "../../../common/enterprise-util.js"; import Logger from "../../../common/logger-util.js"; import * as Messages from "../../../common/messages.js"; import * as t from "../../../common/translation-util.js"; import type {ServerConf} from "../../../common/types.js"; import defaultIcon from "../../img/icon.png"; import {ipcRenderer} from "../typed-ipc-renderer.js"; const logger = new Logger({ file: "domain-util.log", }); // For historical reasons, we store this string in domain.json to denote a // missing icon; it does not change with the actual icon location. export const defaultIconSentinel = "../renderer/img/icon.png"; const serverConfSchema = z.object({ url: z.string().url(), alias: z.string(), icon: z.string(), zulipVersion: z.string().default("unknown"), zulipFeatureLevel: z.number().default(0), }); let db!: JsonDB; reloadDb(); // Migrate from old schema try { const oldDomain = db.getObject("/domain"); if (typeof oldDomain === "string") { (async () => { await addDomain({ alias: "Zulip", url: oldDomain, }); db.delete("/domain"); })(); } } catch (error: unknown) { if (!(error instanceof DataError)) throw error; } export function getDomains(): ServerConf[] { reloadDb(); try { return serverConfSchema.array().parse(db.getObject("/domains")); } catch (error: unknown) { if (!(error instanceof DataError)) throw error; return []; } } export function getDomain(index: number): ServerConf { reloadDb(); return serverConfSchema.parse(db.getObject(`/domains[${index}]`)); } export function updateDomain(index: number, server: ServerConf): void { reloadDb(); serverConfSchema.parse(server); db.push(`/domains[${index}]`, server, true); } export async function addDomain(server: { url: string; alias: string; icon?: string; }): Promise { if (server.icon) { const localIconUrl = await saveServerIcon(server.icon); server.icon = localIconUrl; serverConfSchema.parse(server); db.push("/domains[]", server, true); reloadDb(); } else { server.icon = defaultIconSentinel; serverConfSchema.parse(server); db.push("/domains[]", server, true); reloadDb(); } } export function removeDomains(): void { db.delete("/domains"); reloadDb(); } export function removeDomain(index: number): boolean { if (EnterpriseUtil.isPresetOrg(getDomain(index).url)) { return false; } db.delete(`/domains[${index}]`); reloadDb(); return true; } // Check if domain is already added export function duplicateDomain(domain: string): boolean { domain = formatUrl(domain); return getDomains().some((server) => server.url === domain); } export async function checkDomain( domain: string, silent = false, ): Promise { if (!silent && duplicateDomain(domain)) { // Do not check duplicate in silent mode throw new Error("This server has been added."); } domain = formatUrl(domain); try { return await getServerSettings(domain); } catch { throw new Error(Messages.invalidZulipServerError(domain)); } } async function getServerSettings(domain: string): Promise { return ipcRenderer.invoke("get-server-settings", domain); } export async function saveServerIcon(iconURL: string): Promise { return ( (await ipcRenderer.invoke("save-server-icon", iconURL)) ?? defaultIconSentinel ); } export async function updateSavedServer( url: string, index: number, ): Promise { // Does not promise successful update const serverConf = getDomain(index); const oldIcon = serverConf.icon; try { const newServerConf = await checkDomain(url, true); const localIconUrl = await saveServerIcon(newServerConf.icon); if (!oldIcon || localIconUrl !== defaultIconSentinel) { newServerConf.icon = localIconUrl; updateDomain(index, newServerConf); reloadDb(); } return newServerConf; } catch (error: unknown) { logger.log("Could not update server icon."); logger.log(error); Sentry.captureException(error); return serverConf; } } function reloadDb(): void { const domainJsonPath = path.join( app.getPath("userData"), "config/domain.json", ); try { const file = fs.readFileSync(domainJsonPath, "utf8"); JSON.parse(file); } catch (error: unknown) { if (fs.existsSync(domainJsonPath)) { fs.unlinkSync(domainJsonPath); dialog.showErrorBox( "Error saving new organization", "There seems to be error while saving new organization, " + "you may have to re-add your previous organizations back.", ); logger.error("Error while JSON parsing domain.json: "); logger.error(error); Sentry.captureException(error); } } db = new JsonDB(domainJsonPath, true, true); } export function formatUrl(domain: string): string { if (domain.startsWith("http://") || domain.startsWith("https://")) { return domain; } if (domain.startsWith("localhost:")) { return `http://${domain}`; } return `https://${domain}`; } export function getUnsupportedMessage(server: ServerConf): string | undefined { if (server.zulipFeatureLevel < 65 /* Zulip Server 4.0 */) { const realm = new URL(server.url).hostname; return t.__( "{{{server}}} runs an outdated Zulip Server version {{{version}}}. It may not fully work in this app.", {server: realm, version: server.zulipVersion}, ); } return undefined; } export function iconAsUrl(iconPath: string): string { if (iconPath === defaultIconSentinel) return defaultIcon; try { return `data:application/octet-stream;base64,${fs.readFileSync( iconPath, "base64", )}`; } catch { return defaultIcon; } }