From 6b26c12bfa1f28d40834478b50d2f7b09c9f54fb Mon Sep 17 00:00:00 2001 From: V <vendicated@riseup.net> Date: Tue, 4 Apr 2023 01:16:29 +0200 Subject: Add additional build flavours for Vencord Desktop (#765) --- src/Vencord.ts | 12 +- src/components/VencordSettings/Updater.tsx | 3 +- src/components/VencordSettings/VencordTab.tsx | 4 +- src/globals.d.ts | 3 + src/ipcMain/constants.ts | 35 ----- src/ipcMain/crxToZip.ts | 57 -------- src/ipcMain/extensions.ts | 85 ----------- src/ipcMain/index.ts | 99 ------------- src/ipcMain/simpleGet.ts | 37 ----- src/ipcMain/updater/common.ts | 59 -------- src/ipcMain/updater/git.ts | 83 ----------- src/ipcMain/updater/http.ts | 88 ----------- src/ipcMain/updater/index.ts | 19 --- src/main/index.ts | 108 ++++++++++++++ src/main/ipcMain.ts | 107 ++++++++++++++ src/main/patchWin32Updater.ts | 99 +++++++++++++ src/main/patcher.ts | 120 +++++++++++++++ src/main/updater/common.ts | 59 ++++++++ src/main/updater/git.ts | 83 +++++++++++ src/main/updater/http.ts | 104 +++++++++++++ src/main/updater/index.ts | 19 +++ src/main/utils/constants.ts | 37 +++++ src/main/utils/crxToZip.ts | 57 ++++++++ src/main/utils/extensions.ts | 85 +++++++++++ src/main/utils/simpleGet.ts | 37 +++++ src/patchWin32Updater.ts | 99 ------------- src/patcher.ts | 202 -------------------------- src/plugins/consoleShortcuts.ts | 3 +- src/plugins/settings.tsx | 1 + src/preload.ts | 4 +- src/utils/native.ts | 24 +++ src/utils/settingsSync.ts | 42 +++--- src/utils/updater.ts | 7 +- src/webpack/webpack.ts | 2 +- 34 files changed, 988 insertions(+), 895 deletions(-) delete mode 100644 src/ipcMain/constants.ts delete mode 100644 src/ipcMain/crxToZip.ts delete mode 100644 src/ipcMain/extensions.ts delete mode 100644 src/ipcMain/index.ts delete mode 100644 src/ipcMain/simpleGet.ts delete mode 100644 src/ipcMain/updater/common.ts delete mode 100644 src/ipcMain/updater/git.ts delete mode 100644 src/ipcMain/updater/http.ts delete mode 100644 src/ipcMain/updater/index.ts create mode 100644 src/main/index.ts create mode 100644 src/main/ipcMain.ts create mode 100644 src/main/patchWin32Updater.ts create mode 100644 src/main/patcher.ts create mode 100644 src/main/updater/common.ts create mode 100644 src/main/updater/git.ts create mode 100644 src/main/updater/http.ts create mode 100644 src/main/updater/index.ts create mode 100644 src/main/utils/constants.ts create mode 100644 src/main/utils/crxToZip.ts create mode 100644 src/main/utils/extensions.ts create mode 100644 src/main/utils/simpleGet.ts delete mode 100644 src/patchWin32Updater.ts delete mode 100644 src/patcher.ts create mode 100644 src/utils/native.ts (limited to 'src') diff --git a/src/Vencord.ts b/src/Vencord.ts index 00f8a58..73b53e8 100644 --- a/src/Vencord.ts +++ b/src/Vencord.ts @@ -30,7 +30,7 @@ import "./webpack/patchWebpack"; import { showNotification } from "./api/Notifications"; import { PlainSettings, Settings } from "./api/settings"; import { patches, PMLogger, startAllPlugins } from "./plugins"; -import { checkForUpdates, rebuild, update, UpdateLogger } from "./utils/updater"; +import { checkForUpdates, rebuild, update,UpdateLogger } from "./utils/updater"; import { onceReady } from "./webpack"; import { SettingsRouter } from "./webpack/common"; @@ -56,8 +56,12 @@ async function init() { permanent: true, noPersist: true, onClick() { - if (needsFullRestart) - window.DiscordNative.app.relaunch(); + if (needsFullRestart) { + if (IS_DISCORD_DESKTOP) + window.DiscordNative.app.relaunch(); + else + window.VencordDesktop.app.relaunch(); + } else location.reload(); } @@ -96,7 +100,7 @@ async function init() { init(); -if (!IS_WEB && Settings.winNativeTitleBar && navigator.platform.toLowerCase().startsWith("win")) { +if (IS_DISCORD_DESKTOP && Settings.winNativeTitleBar && navigator.platform.toLowerCase().startsWith("win")) { document.addEventListener("DOMContentLoaded", () => { document.head.append(Object.assign(document.createElement("style"), { id: "vencord-native-titlebar-style", diff --git a/src/components/VencordSettings/Updater.tsx b/src/components/VencordSettings/Updater.tsx index 3c3eb91..15a8c87 100644 --- a/src/components/VencordSettings/Updater.tsx +++ b/src/components/VencordSettings/Updater.tsx @@ -24,6 +24,7 @@ import { handleComponentFailed } from "@components/handleComponentFailed"; import { Link } from "@components/Link"; import { Margins } from "@utils/margins"; import { classes, useAwaiter } from "@utils/misc"; +import { relaunch } from "@utils/native"; import { changes, checkForUpdates, getRepo, isNewer, rebuild, update, updateError, UpdateLogger } from "@utils/updater"; import { Alerts, Button, Card, Forms, Parser, React, Switch, Toasts } from "@webpack/common"; @@ -133,7 +134,7 @@ function Updatable(props: CommonProps) { cancelText: "Not now!", onConfirm() { if (needFullRestart) - window.DiscordNative.app.relaunch(); + relaunch(); else location.reload(); r(); diff --git a/src/components/VencordSettings/VencordTab.tsx b/src/components/VencordSettings/VencordTab.tsx index 7113421..8b86968 100644 --- a/src/components/VencordSettings/VencordTab.tsx +++ b/src/components/VencordSettings/VencordTab.tsx @@ -26,6 +26,7 @@ import { ErrorCard } from "@components/ErrorCard"; import IpcEvents from "@utils/IpcEvents"; import { Margins } from "@utils/margins"; import { identity, useAwaiter } from "@utils/misc"; +import { relaunch } from "@utils/native"; import { Button, Card, Forms, React, Select, Slider, Switch } from "@webpack/common"; const cl = classNameFactory("vc-settings-"); @@ -100,7 +101,7 @@ function VencordSettings() { ) : ( <React.Fragment> <Button - onClick={() => window.DiscordNative.app.relaunch()} + onClick={relaunch} size={Button.Sizes.SMALL}> Restart Client </Button> @@ -111,6 +112,7 @@ function VencordSettings() { Open QuickCSS File </Button> <Button + // FIXME: Vencord Desktop support onClick={() => window.DiscordNative.fileManager.showItemInFolder(settingsDir)} size={Button.Sizes.SMALL} disabled={settingsDirPending}> diff --git a/src/globals.d.ts b/src/globals.d.ts index 7c494e2..98eccde 100644 --- a/src/globals.d.ts +++ b/src/globals.d.ts @@ -35,6 +35,8 @@ declare global { export var IS_WEB: boolean; export var IS_DEV: boolean; export var IS_STANDALONE: boolean; + export var IS_DISCORD_DESKTOP: boolean; + export var IS_VENCORD_DESKTOP: boolean; export var VencordNative: typeof import("./VencordNative").default; export var Vencord: typeof import("./Vencord"); @@ -54,6 +56,7 @@ declare global { * If you really must use it, mark your plugin as Desktop App only by naming it Foo.desktop.ts(x) */ export var DiscordNative: any; + export var VencordDesktop: any; interface Window { webpackChunkdiscord_app: { diff --git a/src/ipcMain/constants.ts b/src/ipcMain/constants.ts deleted file mode 100644 index 7133757..0000000 --- a/src/ipcMain/constants.ts +++ /dev/null @@ -1,35 +0,0 @@ -/* - * Vencord, a modification for Discord's desktop app - * Copyright (c) 2022 Vendicated and contributors - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see <https://www.gnu.org/licenses/>. -*/ - -import { app } from "electron"; -import { join } from "path"; - -export const DATA_DIR = process.env.VENCORD_USER_DATA_DIR ?? ( - process.env.DISCORD_USER_DATA_DIR - ? join(process.env.DISCORD_USER_DATA_DIR, "..", "VencordData") - : join(app.getPath("userData"), "..", "Vencord") -); -export const SETTINGS_DIR = join(DATA_DIR, "settings"); -export const QUICKCSS_PATH = join(SETTINGS_DIR, "quickCss.css"); -export const SETTINGS_FILE = join(SETTINGS_DIR, "settings.json"); -export const ALLOWED_PROTOCOLS = [ - "https:", - "http:", - "steam:", - "spotify:" -]; diff --git a/src/ipcMain/crxToZip.ts b/src/ipcMain/crxToZip.ts deleted file mode 100644 index ca43890..0000000 --- a/src/ipcMain/crxToZip.ts +++ /dev/null @@ -1,57 +0,0 @@ -/* eslint-disable header/header */ - -/*! - * crxToZip - * Copyright (c) 2013 Rob Wu <rob@robwu.nl> - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. - */ - -export function crxToZip(buf: Buffer) { - function calcLength(a: number, b: number, c: number, d: number) { - let length = 0; - - length += a << 0; - length += b << 8; - length += c << 16; - length += d << 24 >>> 0; - return length; - } - - // 50 4b 03 04 - // This is actually a zip file - if (buf[0] === 80 && buf[1] === 75 && buf[2] === 3 && buf[3] === 4) { - return buf; - } - - // 43 72 32 34 (Cr24) - if (buf[0] !== 67 || buf[1] !== 114 || buf[2] !== 50 || buf[3] !== 52) { - throw new Error("Invalid header: Does not start with Cr24"); - } - - // 02 00 00 00 - // or - // 03 00 00 00 - const isV3 = buf[4] === 3; - const isV2 = buf[4] === 2; - - if ((!isV2 && !isV3) || buf[5] || buf[6] || buf[7]) { - throw new Error("Unexpected crx format version number."); - } - - if (isV2) { - const publicKeyLength = calcLength(buf[8], buf[9], buf[10], buf[11]); - const signatureLength = calcLength(buf[12], buf[13], buf[14], buf[15]); - - // 16 = Magic number (4), CRX format version (4), lengths (2x4) - const zipStartOffset = 16 + publicKeyLength + signatureLength; - - return buf.subarray(zipStartOffset, buf.length); - } - // v3 format has header size and then header - const headerSize = calcLength(buf[8], buf[9], buf[10], buf[11]); - const zipStartOffset = 12 + headerSize; - - return buf.subarray(zipStartOffset, buf.length); -} diff --git a/src/ipcMain/extensions.ts b/src/ipcMain/extensions.ts deleted file mode 100644 index d8f8437..0000000 --- a/src/ipcMain/extensions.ts +++ /dev/null @@ -1,85 +0,0 @@ -/* - * Vencord, a modification for Discord's desktop app - * Copyright (c) 2022 Vendicated and contributors - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see <https://www.gnu.org/licenses/>. -*/ - -import { session } from "electron"; -import { unzip } from "fflate"; -import { constants as fsConstants } from "fs"; -import { access, mkdir, rm, writeFile } from "fs/promises"; -import { join } from "path"; - -import { DATA_DIR } from "./constants"; -import { crxToZip } from "./crxToZip"; -import { get } from "./simpleGet"; - -const extensionCacheDir = join(DATA_DIR, "ExtensionCache"); - -async function extract(data: Buffer, outDir: string) { - await mkdir(outDir, { recursive: true }); - return new Promise<void>((resolve, reject) => { - unzip(data, (err, files) => { - if (err) return void reject(err); - Promise.all(Object.keys(files).map(async f => { - // Signature stuff - // 'Cannot load extension with file or directory name - // _metadata. Filenames starting with "_" are reserved for use by the system.'; - if (f.startsWith("_metadata/")) return; - - if (f.endsWith("/")) return void mkdir(join(outDir, f), { recursive: true }); - - const pathElements = f.split("/"); - const name = pathElements.pop()!; - const directories = pathElements.join("/"); - const dir = join(outDir, directories); - - if (directories) { - await mkdir(dir, { recursive: true }); - } - - await writeFile(join(dir, name), files[f]); - })) - .then(() => resolve()) - .catch(err => { - rm(outDir, { recursive: true, force: true }); - reject(err); - }); - }); - }); -} - -export async function installExt(id: string) { - const extDir = join(extensionCacheDir, `${id}`); - - try { - await access(extDir, fsConstants.F_OK); - } catch (err) { - const url = id === "fmkadmapgofadopljbjfkapdkoienihi" - // React Devtools v4.25 - // v4.27 is broken in Electron, see https://github.com/facebook/react/issues/25843 - // Unfortunately, Google does not serve old versions, so this is the only way - ? "https://raw.githubusercontent.com/Vendicated/random-files/f6f550e4c58ac5f2012095a130406c2ab25b984d/fmkadmapgofadopljbjfkapdkoienihi.zip" - : `https://clients2.google.com/service/update2/crx?response=redirect&acceptformat=crx2,crx3&x=id%3D${id}%26uc&prodversion=32`; - const buf = await get(url, { - headers: { - "User-Agent": "Vencord (https://github.com/Vendicated/Vencord)" - } - }); - await extract(crxToZip(buf), extDir).catch(console.error); - } - - session.defaultSession.loadExtension(extDir); -} diff --git a/src/ipcMain/index.ts b/src/ipcMain/index.ts deleted file mode 100644 index 6fb31d1..0000000 --- a/src/ipcMain/index.ts +++ /dev/null @@ -1,99 +0,0 @@ -/* - * Vencord, a modification for Discord's desktop app - * Copyright (c) 2022 Vendicated and contributors - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see <https://www.gnu.org/licenses/>. -*/ - -import "./updater"; - -import { debounce } from "@utils/debounce"; -import IpcEvents from "@utils/IpcEvents"; -import { Queue } from "@utils/Queue"; -import { BrowserWindow, ipcMain, shell } from "electron"; -import { mkdirSync, readFileSync, watch } from "fs"; -import { open, readFile, writeFile } from "fs/promises"; -import { join } from "path"; - -import monacoHtml from "~fileContent/../components/monacoWin.html;base64"; - -import { ALLOWED_PROTOCOLS, QUICKCSS_PATH, SETTINGS_DIR, SETTINGS_FILE } from "./constants"; - -mkdirSync(SETTINGS_DIR, { recursive: true }); - -function readCss() { - return readFile(QUICKCSS_PATH, "utf-8").catch(() => ""); -} - -export function readSettings() { - try { - return readFileSync(SETTINGS_FILE, "utf-8"); - } catch { - return "{}"; - } -} - -ipcMain.handle(IpcEvents.OPEN_QUICKCSS, () => shell.openPath(QUICKCSS_PATH)); - -ipcMain.handle(IpcEvents.OPEN_EXTERNAL, (_, url) => { - try { - var { protocol } = new URL(url); - } catch { - throw "Malformed URL"; - } - if (!ALLOWED_PROTOCOLS.includes(protocol)) - throw "Disallowed protocol."; - - shell.openExternal(url); -}); - -const cssWriteQueue = new Queue(); -const settingsWriteQueue = new Queue(); - -ipcMain.handle(IpcEvents.GET_QUICK_CSS, () => readCss()); -ipcMain.handle(IpcEvents.SET_QUICK_CSS, (_, css) => - cssWriteQueue.push(() => writeFile(QUICKCSS_PATH, css)) -); - -ipcMain.handle(IpcEvents.GET_SETTINGS_DIR, () => SETTINGS_DIR); -ipcMain.on(IpcEvents.GET_SETTINGS, e => e.returnValue = readSettings()); - -ipcMain.handle(IpcEvents.SET_SETTINGS, (_, s) => { - settingsWriteQueue.push(() => writeFile(SETTINGS_FILE, s)); -}); - - -export function initIpc(mainWindow: BrowserWindow) { - open(QUICKCSS_PATH, "a+").then(fd => { - fd.close(); - watch(QUICKCSS_PATH, { persistent: false }, debounce(async () => { - mainWindow.webContents.postMessage(IpcEvents.QUICK_CSS_UPDATE, await readCss()); - }, 50)); - }); -} - -ipcMain.handle(IpcEvents.OPEN_MONACO_EDITOR, async () => { - const win = new BrowserWindow({ - title: "QuickCss Editor", - autoHideMenuBar: true, - darkTheme: true, - webPreferences: { - preload: join(__dirname, "preload.js"), - contextIsolation: true, - nodeIntegration: false, - sandbox: false - } - }); - await win.loadURL(`data:text/html;base64,${monacoHtml}`); -}); diff --git a/src/ipcMain/simpleGet.ts b/src/ipcMain/simpleGet.ts deleted file mode 100644 index 1a8302c..0000000 --- a/src/ipcMain/simpleGet.ts +++ /dev/null @@ -1,37 +0,0 @@ -/* - * Vencord, a modification for Discord's desktop app - * Copyright (c) 2022 Vendicated and contributors - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see <https://www.gnu.org/licenses/>. -*/ - -import https from "https"; - -export function get(url: string, options: https.RequestOptions = {}) { - return new Promise<Buffer>((resolve, reject) => { - https.get(url, options, res => { - const { statusCode, statusMessage, headers } = res; - if (statusCode! >= 400) - return void reject(`${statusCode}: ${statusMessage} - ${url}`); - if (statusCode! >= 300) - return void resolve(get(headers.location!, options)); - - const chunks = [] as Buffer[]; - res.on("error", reject); - - res.on("data", chunk => chunks.push(chunk)); - res.once("end", () => resolve(Buffer.concat(chunks))); - }); - }); -} diff --git a/src/ipcMain/updater/common.ts b/src/ipcMain/updater/common.ts deleted file mode 100644 index 3729c6d..0000000 --- a/src/ipcMain/updater/common.ts +++ /dev/null @@ -1,59 +0,0 @@ -/* - * Vencord, a modification for Discord's desktop app - * Copyright (c) 2022 Vendicated and contributors - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see <https://www.gnu.org/licenses/>. -*/ - -import { createHash } from "crypto"; -import { createReadStream } from "fs"; -import { join } from "path"; - -export async function calculateHashes() { - const hashes = {} as Record<string, string>; - - await Promise.all( - ["patcher.js", "preload.js", "renderer.js", "renderer.css"].map(file => new Promise<void>(r => { - const fis = createReadStream(join(__dirname, file)); - const hash = createHash("sha1", { encoding: "hex" }); - fis.once("end", () => { - hash.end(); - hashes[file] = hash.read(); - r(); - }); - fis.pipe(hash); - })) - ); - - return hashes; -} - -export function serializeErrors(func: (...args: any[]) => any) { - return async function () { - try { - return { - ok: true, - value: await func(...arguments) - }; - } catch (e: any) { - return { - ok: false, - error: e instanceof Error ? { - // prototypes get lost, so turn error into plain object - ...e - } : e - }; - } - }; -} diff --git a/src/ipcMain/updater/git.ts b/src/ipcMain/updater/git.ts deleted file mode 100644 index 89c2d3c..0000000 --- a/src/ipcMain/updater/git.ts +++ /dev/null @@ -1,83 +0,0 @@ -/* - * Vencord, a modification for Discord's desktop app - * Copyright (c) 2022 Vendicated and contributors - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see <https://www.gnu.org/licenses/>. -*/ - -import IpcEvents from "@utils/IpcEvents"; -import { execFile as cpExecFile } from "child_process"; -import { ipcMain } from "electron"; -import { join } from "path"; -import { promisify } from "util"; - -import { calculateHashes, serializeErrors } from "./common"; - -const VENCORD_SRC_DIR = join(__dirname, ".."); - -const execFile = promisify(cpExecFile); - -const isFlatpak = process.platform === "linux" && Boolean(process.env.FLATPAK_ID?.includes("discordapp") || process.env.FLATPAK_ID?.includes("Discord")); - -if (process.platform === "darwin") process.env.PATH = `/usr/local/bin:${process.env.PATH}`; - -function git(...args: string[]) { - const opts = { cwd: VENCORD_SRC_DIR }; - - if (isFlatpak) return execFile("flatpak-spawn", ["--host", "git", ...args], opts); - else return execFile("git", args, opts); -} - -async function getRepo() { - const res = await git("remote", "get-url", "origin"); - return res.stdout.trim() - .replace(/git@(.+):/, "https://$1/") - .replace(/\.git$/, ""); -} - -async function calculateGitChanges() { - await git("fetch"); - - const res = await git("log", "HEAD...origin/main", "--pretty=format:%an/%h/%s"); - - const commits = res.stdout.trim(); - return commits ? commits.split("\n").map(line => { - const [author, hash, ...rest] = line.split("/"); - return { - hash, author, message: rest.join("/") - }; - }) : []; -} - -async function pull() { - const res = await git("pull"); - return res.stdout.includes("Fast-forward"); -} - -async function build() { - const opts = { cwd: VENCORD_SRC_DIR }; - - const command = isFlatpak ? "flatpak-spawn" : "node"; - const args = isFlatpak ? ["--host", "node", "scripts/build/build.mjs"] : ["scripts/build/build.mjs"]; - - const res = await execFile(command, args, opts); - - return !res.stderr.includes("Build failed"); -} - -ipcMain.handle(IpcEvents.GET_HASHES, serializeErrors(calculateHashes)); -ipcMain.handle(IpcEvents.GET_REPO, serializeErrors(getRepo)); -ipcMain.handle(IpcEvents.GET_UPDATES, serializeErrors(calculateGitChanges)); -ipcMain.handle(IpcEvents.UPDATE, serializeErrors(pull)); -ipcMain.handle(IpcEvents.BUILD, serializeErrors(build)); diff --git a/src/ipcMain/updater/http.ts b/src/ipcMain/updater/http.ts deleted file mode 100644 index cc10631..0000000 --- a/src/ipcMain/updater/http.ts +++ /dev/null @@ -1,88 +0,0 @@ -/* - * Vencord, a modification for Discord's desktop app - * Copyright (c) 2022 Vendicated and contributors - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see <https://www.gnu.org/licenses/>. -*/ - -import { VENCORD_USER_AGENT } from "@utils/constants"; -import IpcEvents from "@utils/IpcEvents"; -import { ipcMain } from "electron"; -import { writeFile } from "fs/promises"; -import { join } from "path"; - -import gitHash from "~git-hash"; -import gitRemote from "~git-remote"; - -import { get } from "../simpleGet"; -import { calculateHashes, serializeErrors } from "./common"; - -const API_BASE = `https://api.github.com/repos/${gitRemote}`; -let PendingUpdates = [] as [string, string][]; - -async function githubGet(endpoint: string) { - return get(API_BASE + endpoint, { - headers: { - Accept: "application/vnd.github+json", - // "All API requests MUST include a valid User-Agent header. - // Requests with no User-Agent header will be rejected." - "User-Agent": VENCORD_USER_AGENT - } - }); -} - -async function calculateGitChanges() { - const isOutdated = await fetchUpdates(); - if (!isOutdated) return []; - - const res = await githubGet(`/compare/${gitHash}...HEAD`); - - const data = JSON.parse(res.toString("utf-8")); - return data.commits.map((c: any) => ({ - // github api only sends the long sha - hash: c.sha.slice(0, 7), - author: c.author.login, - message: c.commit.message - })); -} - -async function fetchUpdates() { - const release = await githubGet("/releases/latest"); - - const data = JSON.parse(release.toString()); - const hash = data.name.slice(data.name.lastIndexOf(" ") + 1); - if (hash === gitHash) - return false; - - data.assets.forEach(({ name, browser_download_url }) => { - if (["patcher.js", "preload.js", "renderer.js", "renderer.css"].some(s => name.startsWith(s))) { - PendingUpdates.push([name, browser_download_url]); - } - }); - return true; -} - -async function applyUpdates() { - await Promise.all(PendingUpdates.map( - async ([name, data]) => writeFile(join(__dirname, name), await get(data))) - ); - PendingUpdates = []; - return true; -} - -ipcMain.handle(IpcEvents.GET_HASHES, serializeErrors(calculateHashes)); -ipcMain.handle(IpcEvents.GET_REPO, serializeErrors(() => `https://github.com/${gitRemote}`)); -ipcMain.handle(IpcEvents.GET_UPDATES, serializeErrors(calculateGitChanges)); -ipcMain.handle(IpcEvents.UPDATE, serializeErrors(fetchUpdates)); -ipcMain.handle(IpcEvents.BUILD, serializeErrors(applyUpdates)); diff --git a/src/ipcMain/updater/index.ts b/src/ipcMain/updater/index.ts deleted file mode 100644 index 7036112..0000000 --- a/src/ipcMain/updater/index.ts +++ /dev/null @@ -1,19 +0,0 @@ -/* - * Vencord, a modification for Discord's desktop app - * Copyright (c) 2022 Vendicated and contributors - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see <https://www.gnu.org/licenses/>. -*/ - -import(IS_STANDALONE ? "./http" : "./git"); diff --git a/src/main/index.ts b/src/main/index.ts new file mode 100644 index 0000000..c35635e --- /dev/null +++ b/src/main/index.ts @@ -0,0 +1,108 @@ +/* + * Vencord, a modification for Discord's desktop app + * Copyright (c) 2023 Vendicated and contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. +*/ + +import { app, protocol, session } from "electron"; +import { join } from "path"; + +import { getSettings } from "./ipcMain"; +import { IS_VANILLA } from "./utils/constants"; +import { installExt } from "./utils/extensions"; + +if (IS_VENCORD_DESKTOP || !IS_VANILLA) { + app.whenReady().then(() => { + // Source Maps! Maybe there's a better way but since the renderer is executed + // from a string I don't think any other form of sourcemaps would work + protocol.registerFileProtocol("vencord", ({ url: unsafeUrl }, cb) => { + let url = unsafeUrl.slice("vencord://".length); + if (url.endsWith("/")) url = url.slice(0, -1); + switch (url) { + case "renderer.js.map": + case "preload.js.map": + case "patcher.js.map": // doubt + cb(join(__dirname, url)); + break; + default: + cb({ statusCode: 403 }); + } + }); + + try { + if (getSettings().enableReactDevtools) + installExt("fmkadmapgofadopljbjfkapdkoienihi") + .then(() => console.info("[Vencord] Installed React Developer Tools")) + .catch(err => console.error("[Vencord] Failed to install React Developer Tools", err)); + } catch { } + + + // Remove CSP + type PolicyResult = Record<string, string[]>; + + const parsePolicy = (policy: string): PolicyResult => { + const result: PolicyResult = {}; + policy.split(";").forEach(directive => { + const [directiveKey, ...directiveValue] = directive.trim().split(/\s+/g); + if (directiveKey && !Object.prototype.hasOwnProperty.call(result, directiveKey)) { + result[directiveKey] = directiveValue; + } + }); + return result; + }; + const stringifyPolicy = (policy: PolicyResult): string => + Object.entries(policy) + .filter(([, values]) => values?.length) + .map(directive => directive.flat().join(" ")) + .join("; "); + + function patchCsp(headers: Record<string, string[]>, header: string) { + if (header in headers) { + const csp = parsePolicy(headers[header][0]); + + for (const directive of ["style-src", "connect-src", "img-src", "font-src", "media-src", "worker-src"]) { + csp[directive] = ["*", "blob:", "data:", "'unsafe-inline'"]; + } + // TODO: Restrict this to only imported packages with fixed version. + // Perhaps auto generate with esbuild + csp["script-src"] ??= []; + csp["script-src"].push("'unsafe-eval'", "https://unpkg.com", "https://cdnjs.cloudflare.com"); + headers[header] = [stringifyPolicy(csp)]; + } + } + + session.defaultSession.webRequest.onHeadersReceived(({ responseHeaders, resourceType }, cb) => { + if (responseHeaders) { + if (resourceType === "mainFrame") + patchCsp(responseHeaders, "content-security-policy"); + + // Fix hosts that don't properly set the css content type, such as + // raw.githubusercontent.com + if (resourceType === "stylesheet") + responseHeaders["content-type"] = ["text/css"]; + } + cb({ cancel: false, responseHeaders }); + }); + + // assign a noop to onHeadersReceived to prevent other mods from adding their own incompatible ones. + // For instance, OpenAsar adds their own that doesn't fix content-type for stylesheets which makes it + // impossible to load css from github raw despite our fix above + session.defaultSession.webRequest.onHeadersReceived = () => { }; + }); +} + +if (IS_DISCORD_DESKTOP) { + require("./patcher"); +} diff --git a/src/main/ipcMain.ts b/src/main/ipcMain.ts new file mode 100644 index 0000000..b0f8fc3 --- /dev/null +++ b/src/main/ipcMain.ts @@ -0,0 +1,107 @@ +/* + * Vencord, a modification for Discord's desktop app + * Copyright (c) 2022 Vendicated and contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. +*/ + +import "./updater"; + +import { debounce } from "@utils/debounce"; +import IpcEvents from "@utils/IpcEvents"; +import { Queue } from "@utils/Queue"; +import { BrowserWindow, ipcMain, shell } from "electron"; +import { mkdirSync, readFileSync, watch } from "fs"; +import { open, readFile, writeFile } from "fs/promises"; +import { join } from "path"; + +import monacoHtml from "~fileContent/../components/monacoWin.html;base64"; + +import { ALLOWED_PROTOCOLS, QUICKCSS_PATH, SETTINGS_DIR, SETTINGS_FILE } from "./utils/constants"; + +mkdirSync(SETTINGS_DIR, { recursive: true }); + +function readCss() { + return readFile(QUICKCSS_PATH, "utf-8").catch(() => ""); +} + +export function readSettings() { + try { + return readFileSync(SETTINGS_FILE, "utf-8"); + } catch { + return "{}"; + } +} + +export function getSettings(): typeof import("@api/settings").Settings { + try { + return JSON.parse(readSettings()); + } catch { + return {} as any; + } +} + +ipcMain.handle(IpcEvents.OPEN_QUICKCSS, () => shell.openPath(QUICKCSS_PATH)); + +ipcMain.handle(IpcEvents.OPEN_EXTERNAL, (_, url) => { + try { + var { protocol } = new URL(url); + } catch { + throw "Malformed URL"; + } + if (!ALLOWED_PROTOCOLS.includes(protocol)) + throw "Disallowed protocol."; + + shell.openExternal(url); +}); + +const cssWriteQueue = new Queue(); +const settingsWriteQueue = new Queue(); + +ipcMain.handle(IpcEvents.GET_QUICK_CSS, () => readCss()); +ipcMain.handle(IpcEvents.SET_QUICK_CSS, (_, css) => + cssWriteQueue.push(() => writeFile(QUICKCSS_PATH, css)) +); + +ipcMain.handle(IpcEvents.GET_SETTINGS_DIR, () => SETTINGS_DIR); +ipcMain.on(IpcEvents.GET_SETTINGS, e => e.returnValue = readSettings()); + +ipcMain.handle(IpcEvents.SET_SETTINGS, (_, s) => { + settingsWriteQueue.push(() => writeFile(SETTINGS_FILE, s)); +}); + + +export function initIpc(mainWindow: BrowserWindow) { + open(QUICKCSS_PATH, "a+").then(fd => { + fd.close(); + watch(QUICKCSS_PATH, { persistent: false }, debounce(async () => { + mainWindow.webContents.postMessage(IpcEvents.QUICK_CSS_UPDATE, await readCss()); + }, 50)); + }); +} + +ipcMain.handle(IpcEvents.OPEN_MONACO_EDITOR, async () => { + const win = new BrowserWindow({ + title: "QuickCss Editor", + autoHideMenuBar: true, + darkTheme: true, + webPreferences: { + preload: join(__dirname, "preload.js"), + contextIsolation: true, + nodeIntegration: false, + sandbox: false + } + }); + await win.loadURL(`data:text/html;base64,${monacoHtml}`); +}); diff --git a/src/main/patchWin32Updater.ts b/src/main/patchWin32Updater.ts new file mode 100644 index 0000000..e08e37d --- /dev/null +++ b/src/main/patchWin32Updater.ts @@ -0,0 +1,99 @@ +/* + * Vencord, a modification for Discord's desktop app + * Copyright (c) 2022 Vendicated and contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. +*/ + +import { app, autoUpdater } from "electron"; +import { existsSync, mkdirSync, readdirSync, renameSync, statSync, writeFileSync } from "fs"; +import { basename, dirname, join } from "path"; + +const { setAppUserModelId } = app; + +// Apparently requiring Discords updater too early leads into issues, +// copied this workaround from powerCord +app.setAppUserModelId = function (id: string) { + app.setAppUserModelId = setAppUserModelId; + + setAppUserModelId.call(this, id); + + patchUpdater(); +}; + +function isNewer($new: string, old: string) { + const newParts = $new.slice(4).split(".").map(Number); + const oldParts = old.slice(4).split(".").map(Number); + + for (let i = 0; i < oldParts.length; i++) { + if (newParts[i] > oldParts[i]) return true; + if (newParts[i] < oldParts[i]) return false; + } + return false; +} + +function patchLatest() { + try { + const currentAppPath = dirname(process.execPath); + const currentVersion = basename(currentAppPath); + const discordPath = join(currentAppPath, ".."); + + const latestVersion = readdirSync(discordPath).reduce((prev, curr) => { + return (curr.startsWith("app-") && isNewer(curr, prev)) + ? curr + : prev; + }, currentVersion as string); + + if (latestVersion === currentVersion) return; + + const resources = join(discordPath, latestVersion, "resources"); + const app = join(resources, "app.asar"); + const _app = join(resources, "_app.asar"); + + if (!existsSync(app) || statSync(app).isDirectory()) return; + + console.info("[Vencord] Detected Host Update. Repatching..."); + + renameSync(app, _app); + mkdirSync(app); + writeFileSync(join(app, "package.json"), JSON.stringify({ + name: "discord", + main: "index.js" + })); + writeFileSync(join(app, "index.js"), `require(${JSON.stringify(join(__dirname, "patcher.js"))});`); + } catch (err) { + console.error("[Vencord] Failed to repatch latest host update", err); + } +} + +// Windows Host Updates install to a new folder app-{HOST_VERSION}, so we +// need to reinject +function patchUpdater() { + try { + const autoStartScript = join(require.main!.filename, "..", "autoStart", "win32.js"); + const { update } = require(autoStartScript); + + require.cache[autoStartScript]!.exports.update = function () { + update.apply(this, arguments); + patchLatest(); + }; + } catch { + // OpenAsar uses electrons autoUpdater on Windows + const { quitAndInstall } = autoUpdater; + autoUpdater.quitAndInstall = function () { + patchLatest(); + quitAndInstall.call(this); + }; + } +} diff --git a/src/main/patcher.ts b/src/main/patcher.ts new file mode 100644 index 0000000..c45a299 --- /dev/null +++ b/src/main/patcher.ts @@ -0,0 +1,120 @@ +/* + * Vencord, a modification for Discord's desktop app + * Copyright (c) 2022 Vendicated and contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. +*/ + +import { onceDefined } from "@utils/onceDefined"; +import electron, { app, BrowserWindowConstructorOptions, Menu } from "electron"; +import { dirname, join } from "path"; + +import { getSettings, initIpc } from "./ipcMain"; +import { IS_VANILLA } from "./utils/constants"; + +console.log("[Vencord] Starting up..."); + +// Our injector file at app/index.js +const injectorPath = require.main!.filename; + +// special discord_arch_electron injection method +const asarName = require.main!.path.endsWith("app.asar") ? "_app.asar" : "app.asar"; + +// The original app.asar +const asarPath = join(dirname(injectorPath), "..", asarName); + +const discordPkg = require(join(asarPath, "package.json")); +require.main!.filename = join(asarPath, discordPkg.main); + +// @ts-ignore Untyped method? Dies from cringe +app.setAppPath(asarPath); + +if (!IS_VANILLA) { + const settings = getSettings(); + + // Repatch after host updates on Windows + if (process.platform === "win32") { + require("./patchWin32Updater"); + + if (settings.winCtrlQ) { + const originalBuild = Menu.buildFromTemplate; + Menu.buildFromTemplate = function (template) { + if (template[0]?.label === "&File") { + const { submenu } = template[0]; + if (Array.isArray(submenu)) { + submenu.push({ + label: "Quit (Hidden)", + visible: false, + acceleratorWorksWhenHidden: true, + accelerator: "Control+Q", + click: () => app.quit() + }); + } + } + return originalBuild.call(this, template); + }; + } + } + + class BrowserWindow extends electron.BrowserWindow { + constructor(options: BrowserWindowConstructorOptions) { + if (options?.webPreferences?.preload && options.title) { + const original = options.webPreferences.preload; + options.webPreferences.preload = join(__dirname, "preload.js"); + options.webPreferences.sandbox = false; + if (settings.frameless) { + options.frame = false; + } else if (process.platform === "win32" && settings.winNativeTitleBar) { + delete options.frame; + } + + // This causes electron to freeze / white screen for some people + if ((settings as any).transparentUNSAFE_USE_AT_OWN_RISK) { + options.transparent = true; + options.backgroundColor = "#00000000"; + } + + process.env.DISCORD_PRELOAD = original; + + super(options); + initIpc(this); + } else super(options); + } + } + Object.assign(BrowserWindow, electron.BrowserWindow); + // esbuild may rename our BrowserWindow, which leads to it being excluded + // from getFocusedWindow(), so this is necessary + // https://github.com/discord/electron/blob/13-x-y/lib/browser/api/browser-window.ts#L60-L62 + Object.defineProperty(BrowserWindow, "name", { value: "BrowserWindow", configurable: true }); + + // Replace electrons exports with our custom BrowserWindow + const electronPath = require.resolve("electron"); + delete require.cache[electronPath]!.exports; + require.cache[electronPath]!.exports = { + ...electron, + BrowserWindow + }; + + // Patch appSettings to force enable devtools + onceDefined(global, "appSettings", s => + s.set("DANGEROUS_ENABLE_DEVTOOLS_ONLY_ENABLE_IF_YOU_KNOW_WHAT_YOURE_DOING", true) + ); + + process.env.DATA_DIR = join(app.getPath("userData"), "..", "Vencord"); +} else { + console.log("[Vencord] Running in vanilla mode. Not loading Vencord"); +} + +console.log("[Vencord] Loading original Discord app.asar"); +require(require.main!.filename); diff --git a/src/main/updater/common.ts b/src/main/updater/common.ts new file mode 100644 index 0000000..3729c6d --- /dev/null +++ b/src/main/updater/common.ts @@ -0,0 +1,59 @@ +/* + * Vencord, a modification for Discord's desktop app + * Copyright (c) 2022 Vendicated and contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. +*/ + +import { createHash } from "crypto"; +import { createReadStream } from "fs"; +import { join } from "path"; + +export async function calculateHashes() { + const hashes = {} as Record<string, string>; + + await Promise.all( + ["patcher.js", "preload.js", "renderer.js", "renderer.css"].map(file => new Promise<void>(r => { + const fis = createReadStream(join(__dirname, file)); + const hash = createHash("sha1", { encoding: "hex" }); + fis.once("end", () => { + hash.end(); + hashes[file] = hash.read(); + r(); + }); + fis.pipe(hash); + })) + ); + + return hashes; +} + +export function serializeErrors(func: (...args: any[]) => any) { + return async function () { + try { + return { + ok: true, + value: await func(...arguments) + }; + } catch (e: any) { + return { + ok: false, + error: e instanceof Error ? { + // prototypes get lost, so turn error into plain object + ...e + } : e + }; + } + }; +} diff --git a/src/main/updater/git.ts b/src/main/updater/git.ts new file mode 100644 index 0000000..89c2d3c --- /dev/null +++ b/src/main/updater/git.ts @@ -0,0 +1,83 @@ +/* + * Vencord, a modification for Discord's desktop app + * Copyright (c) 2022 Vendicated and contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. +*/ + +import IpcEvents from "@utils/IpcEvents"; +import { execFile as cpExecFile } from "child_process"; +import { ipcMain } from "electron"; +import { join } from "path"; +import { promisify } from "util"; + +import { calculateHashes, serializeErrors } from "./common"; + +const VENCORD_SRC_DIR = join(__dirname, ".."); + +const execFile = promisify(cpExecFile); + +const isFlatpak = process.platform === "linux" && Boolean(process.env.FLATPAK_ID?.includes("discordapp") || process.env.FLATPAK_ID?.includes("Discord")); + +if (process.platform === "darwin") process.env.PATH = `/usr/local/bin:${process.env.PATH}`; + +function git(...args: string[]) { + const opts = { cwd: VENCORD_SRC_DIR }; + + if (isFlatpak) return execFile("flatpak-spawn", ["--host", "git", ...args], opts); + else return execFile("git", args, opts); +} + +async function getRepo() { + const res = await git("remote", "get-url", "origin"); + return res.stdout.trim() + .replace(/git@(.+):/, "https://$1/") + .replace(/\.git$/, ""); +} + +async function calculateGitChanges() { + await git("fetch"); + + const res = await git("log", "HEAD...origin/main", "--pretty=format:%an/%h/%s"); + + const commits = res.stdout.trim(); + return commits ? commits.split("\n").map(line => { + const [author, hash, ...rest] = line.split("/"); + return { + hash, author, message: rest.join("/") + }; + }) : []; +} + +async function pull() { + const res = await git("pull"); + return res.stdout.includes("Fast-forward"); +} + +async function build() { + const opts = { cwd: VENCORD_SRC_DIR }; + + const command = isFlatpak ? "flatpak-spawn" : "node"; + const args = isFlatpak ? ["--host", "node", "scripts/build/build.mjs"] : ["scripts/build/build.mjs"]; + + const res = await execFile(command, args, opts); + + return !res.stderr.includes("Build failed"); +} + +ipcMain.handle(IpcEvents.GET_HASHES, serializeErrors(calculateHashes)); +ipcMain.handle(IpcEvents.GET_REPO, serializeErrors(getRepo)); +ipcMain.handle(IpcEvents.GET_UPDATES, serializeErrors(calculateGitChanges)); +ipcMain.handle(IpcEvents.UPDATE, serializeErrors(pull)); +ipcMain.handle(IpcEvents.BUILD, serializeErrors(build)); diff --git a/src/main/updater/http.ts b/src/main/updater/http.ts new file mode 100644 index 0000000..a2e2aad --- /dev/null +++ b/src/main/updater/http.ts @@ -0,0 +1,104 @@ +/* + * Vencord, a modification for Discord's desktop app + * Copyright (c) 2022 Vendicated and contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. +*/ + +import { VENCORD_USER_AGENT } from "@utils/constants"; +import IpcEvents from "@utils/IpcEvents"; +import { ipcMain } from "electron"; +import { writeFile } from "fs/promises"; +import { join } from "path"; + +import gitHash from "~git-hash"; +import gitRemote from "~git-remote"; + +import { get } from "../utils/simpleGet"; +import { calculateHashes, serializeErrors } from "./common"; + +const API_BASE = `https://api.github.com/repos/${gitRemote}`; +let PendingUpdates = [] as [string, string][]; + +async function githubGet(endpoint: string) { + return get(API_BASE + endpoint, { + headers: { + Accept: "application/vnd.github+json", + // "All API requests MUST include a valid User-Agent header. + // Requests with no User-Agent header will be rejected." + "User-Agent": VENCORD_USER_AGENT + } + }); +} + +async function calculateGitChanges() { + const isOutdated = await fetchUpdates(); + if (!isOutdated) return []; + + const res = await githubGet(`/compare/${gitHash}...HEAD`); + + const data = JSON.parse(res.toString("utf-8")); + return data.commits.map((c: any) => ({ + // github api only sends the long sha + hash: c.sha.slice(0, 7), + author: c.author.login, + message: c.commit.message + })); +} + +const FILES_TO_DOWNLOAD = [ + IS_DISCORD_DESKTOP ? "patcher.js" : "vencordDesktopMain.js", + "preload.js", + IS_DISCORD_DESKTOP ? "renderer.js" : "vencordDesktopRenderer.js", + "renderer.css" +]; + +async function fetchUpdates() { + const release = await githubGet("/releases/latest"); + + const data = JSON.parse(release.toString()); + const hash = data.name.slice(data.name.lastIndexOf(" ") + 1); + if (hash === gitHash) + return false; + + data.assets.forEach(({ name, browser_download_url }) => { + if (FILES_TO_DOWNLOAD.some(s => name.startsWith(s))) { + PendingUpdates.push([name, browser_download_url]); + } + }); + return true; +} + +async function applyUpdates() { + await Promise.all(PendingUpdates.map( + async ([name, data]) => writeFile( + join( + __dirname, + IS_VENCORD_DESKTOP + // vencordDesktopRenderer.js -> renderer.js + ? name.replace(/vencordDesktop(\w)/, (_, c) => c.toLowerCase()) + : name + ), + await get(data) + ) + )); + PendingUpdates = []; + return true; +} + +ipcMain.handle(IpcEvents.GET_HASHES, serializeErrors(calculateHashes)); +ipcMain.handle(IpcEvents.GET_REPO, serializeErrors(() => `https://github.com/${gitRemote}`)); +ipcMain.handle(IpcEvents.GET_UPDATES, serializeErrors(calculateGitChanges)); +ipcMain.handle(IpcEvents.UPDATE, serializeErrors(fetchUpdates)); +ipcMain.handle(IpcEvents.BUILD, serializeErrors(applyUpdates)); diff --git a/src/main/updater/index.ts b/src/main/updater/index.ts new file mode 100644 index 0000000..7036112 --- /dev/null +++ b/src/main/updater/index.ts @@ -0,0 +1,19 @@ +/* + * Vencord, a modification for Discord's desktop app + * Copyright (c) 2022 Vendicated and contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. +*/ + +import(IS_STANDALONE ? "./http" : "./git"); diff --git a/src/main/utils/constants.ts b/src/main/utils/constants.ts new file mode 100644 index 0000000..cc9f459 --- /dev/null +++ b/src/main/utils/constants.ts @@ -0,0 +1,37 @@ +/* + * Vencord, a modification for Discord's desktop app + * Copyright (c) 2022 Vendicated and contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. +*/ + +import { app } from "electron"; +import { join } from "path"; + +export const DATA_DIR = process.env.VENCORD_USER_DATA_DIR ?? ( + process.env.DISCORD_USER_DATA_DIR + ? join(process.env.DISCORD_USER_DATA_DIR, "..", "VencordData") + : join(app.getPath("userData"), "..", "Vencord") +); +export const SETTINGS_DIR = join(DATA_DIR, "settings"); +export const QUICKCSS_PATH = join(SETTINGS_DIR, "quickCss.css"); +export const SETTINGS_FILE = join(SETTINGS_DIR, "settings.json"); +export const ALLOWED_PROTOCOLS = [ + "https:", + "http:", + "steam:", + "spotify:" +]; + +export const IS_VANILLA = /* @__PURE__ */ process.argv.includes("--vanilla"); diff --git a/src/main/utils/crxToZip.ts b/src/main/utils/crxToZip.ts new file mode 100644 index 0000000..ca43890 --- /dev/null +++ b/src/main/utils/crxToZip.ts @@ -0,0 +1,57 @@ +/* eslint-disable header/header */ + +/*! + * crxToZip + * Copyright (c) 2013 Rob Wu <rob@robwu.nl> + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +export function crxToZip(buf: Buffer) { + function calcLength(a: number, b: number, c: number, d: number) { + let length = 0; + + length += a << 0; + length += b << 8; + length += c << 16; + length += d << 24 >>> 0; + return length; + } + + // 50 4b 03 04 + // This is actually a zip file + if (buf[0] === 80 && buf[1] === 75 && buf[2] === 3 && buf[3] === 4) { + return buf; + } + + // 43 72 32 34 (Cr24) + if (buf[0] !== 67 || buf[1] !== 114 || buf[2] !== 50 || buf[3] !== 52) { + throw new Error("Invalid header: Does not start with Cr24"); + } + + // 02 00 00 00 + // or + // 03 00 00 00 + const isV3 = buf[4] === 3; + const isV2 = buf[4] === 2; + + if ((!isV2 && !isV3) || buf[5] || buf[6] || buf[7]) { + throw new Error("Unexpected crx format version number."); + } + + if (isV2) { + const publicKeyLength = calcLength(buf[8], buf[9], buf[10], buf[11]); + const signatureLength = calcLength(buf[12], buf[13], buf[14], buf[15]); + + // 16 = Magic number (4), CRX format version (4), lengths (2x4) + const zipStartOffset = 16 + publicKeyLength + signatureLength; + + return buf.subarray(zipStartOffset, buf.length); + } + // v3 format has header size and then header + const headerSize = calcLength(buf[8], buf[9], buf[10], buf[11]); + const zipStartOffset = 12 + headerSize; + + return buf.subarray(zipStartOffset, buf.length); +} diff --git a/src/main/utils/extensions.ts b/src/main/utils/extensions.ts new file mode 100644 index 0000000..d8f8437 --- /dev/null +++ b/src/main/utils/extensions.ts @@ -0,0 +1,85 @@ +/* + * Vencord, a modification for Discord's desktop app + * Copyright (c) 2022 Vendicated and contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. +*/ + +import { session } from "electron"; +import { unzip } from "fflate"; +import { constants as fsConstants } from "fs"; +import { access, mkdir, rm, writeFile } from "fs/promises"; +import { join } from "path"; + +import { DATA_DIR } from "./constants"; +import { crxToZip } from "./crxToZip"; +import { get } from "./simpleGet"; + +const extensionCacheDir = join(DATA_DIR, "ExtensionCache"); + +async function extract(data: Buffer, outDir: string) { + await mkdir(outDir, { recursive: true }); + return new Promise<void>((resolve, reject) => { + unzip(data, (err, files) => { + if (err) return void reject(err); + Promise.all(Object.keys(files).map(async f => { + // Signature stuff + // 'Cannot load extension with file or directory name + // _metadata. Filenames starting with "_" are reserved for use by the system.'; + if (f.startsWith("_metadata/")) return; + + if (f.endsWith("/")) return void mkdir(join(outDir, f), { recursive: true }); + + const pathElements = f.split("/"); + const name = pathElements.pop()!; + const directories = pathElements.join("/"); + const dir = join(outDir, directories); + + if (directories) { + await mkdir(dir, { recursive: true }); + } + + await writeFile(join(dir, name), files[f]); + })) + .then(() => resolve()) + .catch(err => { + rm(outDir, { recursive: true, force: true }); + reject(err); + }); + }); + }); +} + +export async function installExt(id: string) { + const extDir = join(extensionCacheDir, `${id}`); + + try { + await access(extDir, fsConstants.F_OK); + } catch (err) { + const url = id === "fmkadmapgofadopljbjfkapdkoienihi" + // React Devtools v4.25 + // v4.27 is broken in Electron, see https://github.com/facebook/react/issues/25843 + // Unfortunately, Google does not serve old versions, so this is the only way + ? "https://raw.githubusercontent.com/Vendicated/random-files/f6f550e4c58ac5f2012095a130406c2ab25b984d/fmkadmapgofadopljbjfkapdkoienihi.zip" + : `https://clients2.google.com/service/update2/crx?response=redirect&acceptformat=crx2,crx3&x=id%3D${id}%26uc&prodversion=32`; + const buf = await get(url, { + headers: { + "User-Agent": "Vencord (https://github.com/Vendicated/Vencord)" + } + }); + await extract(crxToZip(buf), extDir).catch(console.error); + } + + session.defaultSession.loadExtension(extDir); +} diff --git a/src/main/utils/simpleGet.ts b/src/main/utils/simpleGet.ts new file mode 100644 index 0000000..1a8302c --- /dev/null +++ b/src/main/utils/simpleGet.ts @@ -0,0 +1,37 @@ +/* + * Vencord, a modification for Discord's desktop app + * Copyright (c) 2022 Vendicated and contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. +*/ + +import https from "https"; + +export function get(url: string, options: https.RequestOptions = {}) { + return new Promise<Buffer>((resolve, reject) => { + https.get(url, options, res => { + const { statusCode, statusMessage, headers } = res; + if (statusCode! >= 400) + return void reject(`${statusCode}: ${statusMessage} - ${url}`); + if (statusCode! >= 300) + return void resolve(get(headers.location!, options)); + + const chunks = [] as Buffer[]; + res.on("error", reject); + + res.on("data", chunk => chunks.push(chunk)); + res.once("end", () => resolve(Buffer.concat(chunks))); + }); + }); +} diff --git a/src/patchWin32Updater.ts b/src/patchWin32Updater.ts deleted file mode 100644 index e08e37d..0000000 --- a/src/patchWin32Updater.ts +++ /dev/null @@ -1,99 +0,0 @@ -/* - * Vencord, a modification for Discord's desktop app - * Copyright (c) 2022 Vendicated and contributors - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see <https://www.gnu.org/licenses/>. -*/ - -import { app, autoUpdater } from "electron"; -import { existsSync, mkdirSync, readdirSync, renameSync, statSync, writeFileSync } from "fs"; -import { basename, dirname, join } from "path"; - -const { setAppUserModelId } = app; - -// Apparently requiring Discords updater too early leads into issues, -// copied this workaround from powerCord -app.setAppUserModelId = function (id: string) { - app.setAppUserModelId = setAppUserModelId; - - setAppUserModelId.call(this, id); - - patchUpdater(); -}; - -function isNewer($new: string, old: string) { - const newParts = $new.slice(4).split(".").map(Number); - const oldParts = old.slice(4).split(".").map(Number); - - for (let i = 0; i < oldParts.length; i++) { - if (newParts[i] > oldParts[i]) return true; - if (newParts[i] < oldParts[i]) return false; - } - return false; -} - -function patchLatest() { - try { - const currentAppPath = dirname(process.execPath); - const currentVersion = basename(currentAppPath); - const discordPath = join(currentAppPath, ".."); - - const latestVersion = readdirSync(discordPath).reduce((prev, curr) => { - return (curr.startsWith("app-") && isNewer(curr, prev)) - ? curr - : prev; - }, currentVersion as string); - - if (latestVersion === currentVersion) return; - - const resources = join(discordPath, latestVersion, "resources"); - const app = join(resources, "app.asar"); - const _app = join(resources, "_app.asar"); - - if (!existsSync(app) || statSync(app).isDirectory()) return; - - console.info("[Vencord] Detected Host Update. Repatching..."); - - renameSync(app, _app); - mkdirSync(app); - writeFileSync(join(app, "package.json"), JSON.stringify({ - name: "discord", - main: "index.js" - })); - writeFileSync(join(app, "index.js"), `require(${JSON.stringify(join(__dirname, "patcher.js"))});`); - } catch (err) { - console.error("[Vencord] Failed to repatch latest host update", err); - } -} - -// Windows Host Updates install to a new folder app-{HOST_VERSION}, so we -// need to reinject -function patchUpdater() { - try { - const autoStartScript = join(require.main!.filename, "..", "autoStart", "win32.js"); - const { update } = require(autoStartScript); - - require.cache[autoStartScript]!.exports.update = function () { - update.apply(this, arguments); - patchLatest(); - }; - } catch { - // OpenAsar uses electrons autoUpdater on Windows - const { quitAndInstall } = autoUpdater; - autoUpdater.quitAndInstall = function () { - patchLatest(); - quitAndInstall.call(this); - }; - } -} diff --git a/src/patcher.ts b/src/patcher.ts deleted file mode 100644 index b1f47b0..0000000 --- a/src/patcher.ts +++ /dev/null @@ -1,202 +0,0 @@ -/* - * Vencord, a modification for Discord's desktop app - * Copyright (c) 2022 Vendicated and contributors - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see <https://www.gnu.org/licenses/>. -*/ - -import { onceDefined } from "@utils/onceDefined"; -import electron, { app, BrowserWindowConstructorOptions, Menu, protocol, session } from "electron"; -import { dirname, join } from "path"; - -import { initIpc } from "./ipcMain"; -import { installExt } from "./ipcMain/extensions"; -import { readSettings } from "./ipcMain/index"; - -console.log("[Vencord] Starting up..."); - -// Our injector file at app/index.js -const injectorPath = require.main!.filename; - -// special discord_arch_electron injection method -const asarName = require.main!.path.endsWith("app.asar") ? "_app.asar" : "app.asar"; - -// The original app.asar -const asarPath = join(dirname(injectorPath), "..", asarName); - -const discordPkg = require(join(asarPath, "package.json")); -require.main!.filename = join(asarPath, discordPkg.main); - -// @ts-ignore Untyped method? Dies from cringe -app.setAppPath(asarPath); - -if (!process.argv.includes("--vanilla")) { - let settings: typeof import("@api/settings").Settings = {} as any; - try { - settings = JSON.parse(readSettings()); - } catch { } - - // Repatch after host updates on Windows - if (process.platform === "win32") { - require("./patchWin32Updater"); - - if (settings.winCtrlQ) { - const originalBuild = Menu.buildFromTemplate; - Menu.buildFromTemplate = function (template) { - if (template[0]?.label === "&File") { - const { submenu } = template[0]; - if (Array.isArray(submenu)) { - submenu.push({ - label: "Quit (Hidden)", - visible: false, - acceleratorWorksWhenHidden: true, - accelerator: "Control+Q", - click: () => app.quit() - }); - } - } - return originalBuild.call(this, template); - }; - } - } - - class BrowserWindow extends electron.BrowserWindow { - constructor(options: BrowserWindowConstructorOptions) { - if (options?.webPreferences?.preload && options.title) { - const original = options.webPreferences.preload; - options.webPreferences.preload = join(__dirname, "preload.js"); - options.webPreferences.sandbox = false; - if (settings.frameless) { - options.frame = false; - } else if (process.platform === "win32" && settings.winNativeTitleBar) { - delete options.frame; - } - - // This causes electron to freeze / white screen for some people - if ((settings as any).transparentUNSAFE_USE_AT_OWN_RISK) { - options.transparent = true; - options.backgroundColor = "#00000000"; - } - - process.env.DISCORD_PRELOAD = original; - - super(options); - initIpc(this); - } else super(options); - } - } - Object.assign(BrowserWindow, electron.BrowserWindow); - // esbuild may rename our BrowserWindow, which leads to it being excluded - // from getFocusedWindow(), so this is necessary - // https://github.com/discord/electron/blob/13-x-y/lib/browser/api/browser-window.ts#L60-L62 - Object.defineProperty(BrowserWindow, "name", { value: "BrowserWindow", configurable: true }); - - // Replace electrons exports with our custom BrowserWindow - const electronPath = require.resolve("electron"); - delete require.cache[electronPath]!.exports; - require.cache[electronPath]!.exports = { - ...electron, - BrowserWindow - }; - - // Patch appSettings to force enable devtools - onceDefined(global, "appSettings", s => - s.set("DANGEROUS_ENABLE_DEVTOOLS_ONLY_ENABLE_IF_YOU_KNOW_WHAT_YOURE_DOING", true) - ); - - process.env.DATA_DIR = join(app.getPath("userData"), "..", "Vencord"); - - app.whenReady().then(() => { - // Source Maps! Maybe there's a better way but since the renderer is executed - // from a string I don't think any other form of sourcemaps would work - protocol.registerFileProtocol("vencord", ({ url: unsafeUrl }, cb) => { - let url = unsafeUrl.slice("vencord://".length); - if (url.endsWith("/")) url = url.slice(0, -1); - switch (url) { - case "renderer.js.map": - case "preload.js.map": - case "patcher.js.map": // doubt - cb(join(__dirname, url)); - break; - default: - cb({ statusCode: 403 }); - } - }); - - try { - if (settings?.enableReactDevtools) - installExt("fmkadmapgofadopljbjfkapdkoienihi") - .then(() => console.info("[Vencord] Installed React Developer Tools")) - .catch(err => console.error("[Vencord] Failed to install React Developer Tools", err)); - } catch { } - - - // Remove CSP - type PolicyResult = Record<string, string[]>; - - const parsePolicy = (policy: string): PolicyResult => { - const result: PolicyResult = {}; - policy.split(";").forEach(directive => { - const [directiveKey, ...directiveValue] = directive.trim().split(/\s+/g); - if (directiveKey && !Object.prototype.hasOwnProperty.call(result, directiveKey)) { - result[directiveKey] = directiveValue; - } - }); - return result; - }; - const stringifyPolicy = (policy: PolicyResult): string => - Object.entries(policy) - .filter(([, values]) => values?.length) - .map(directive => directive.flat().join(" ")) - .join("; "); - - function patchCsp(headers: Record<string, string[]>, header: string) { - if (header in headers) { - const csp = parsePolicy(headers[header][0]); - - for (const directive of ["style-src", "connect-src", "img-src", "font-src", "media-src", "worker-src"]) { - csp[directive] = ["*", "blob:", "data:", "'unsafe-inline'"]; - } - // TODO: Restrict this to only imported packages with fixed version. - // Perhaps auto generate with esbuild - csp["script-src"] ??= []; - csp["script-src"].push("'unsafe-eval'", "https://unpkg.com", "https://cdnjs.cloudflare.com"); - headers[header] = [stringifyPolicy(csp)]; - } - } - - session.defaultSession.webRequest.onHeadersReceived(({ responseHeaders, resourceType }, cb) => { - if (responseHeaders) { - if (resourceType === "mainFrame") - patchCsp(responseHeaders, "content-security-policy"); - - // Fix hosts that don't properly set the css content type, such as - // raw.githubusercontent.com - if (resourceType === "stylesheet") - responseHeaders["content-type"] = ["text/css"]; - } - cb({ cancel: false, responseHeaders }); - }); - - // assign a noop to onHeadersReceived to prevent other mods from adding their own incompatible ones. - // For instance, OpenAsar adds their own that doesn't fix content-type for stylesheets which makes it - // impossible to load css from github raw despite our fix above - session.defaultSession.webRequest.onHeadersReceived = () => { }; - }); -} else { - console.log("[Vencord] Running in vanilla mode. Not loading Vencord"); -} - -console.log("[Vencord] Loading original Discord app.asar"); -require(require.main!.filename); diff --git a/src/plugins/consoleShortcuts.ts b/src/plugins/consoleShortcuts.ts index 70a9875..056d246 100644 --- a/src/plugins/consoleShortcuts.ts +++ b/src/plugins/consoleShortcuts.ts @@ -17,6 +17,7 @@ */ import { Devs } from "@utils/constants"; +import { relaunch } from "@utils/native"; import definePlugin from "@utils/types"; import * as Webpack from "@webpack"; import { extract, filters, findAll, search } from "@webpack"; @@ -77,7 +78,7 @@ export default definePlugin({ Settings: Vencord.Settings, Api: Vencord.Api, reload: () => location.reload(), - restart: IS_WEB ? WEB_ONLY("restart") : window.DiscordNative.app.relaunch + restart: IS_WEB ? WEB_ONLY("restart") : relaunch }; }, diff --git a/src/plugins/settings.tsx b/src/plugins/settings.tsx index 67d1f8d..8db0a4e 100644 --- a/src/plugins/settings.tsx +++ b/src/plugins/settings.tsx @@ -168,6 +168,7 @@ export default definePlugin({ get additionalInfo() { if (IS_DEV) return " (Dev)"; if (IS_WEB) return " (Web)"; + if (IS_VENCORD_DESKTOP) return " (Vencord Desktop)"; if (IS_STANDALONE) return " (Standalone)"; return ""; }, diff --git a/src/preload.ts b/src/preload.ts index 820b655..b75574a 100644 --- a/src/preload.ts +++ b/src/preload.ts @@ -54,7 +54,9 @@ if (location.protocol !== "data:") { document.getElementById("vencord-css-core")!.textContent = readFileSync(rendererCss, "utf-8"); }); } - require(process.env.DISCORD_PRELOAD!); + + if (process.env.DISCORD_PRELOAD) + require(process.env.DISCORD_PRELOAD); } else { // Monaco Popout contextBridge.exposeInMainWorld("setCss", debounce(s => VencordNative.ipc.invoke(IpcEvents.SET_QUICK_CSS, s))); diff --git a/src/utils/native.ts b/src/utils/native.ts new file mode 100644 index 0000000..70e4c0e --- /dev/null +++ b/src/utils/native.ts @@ -0,0 +1,24 @@ +/* + * Vencord, a modification for Discord's desktop app + * Copyright (c) 2023 Vendicated and contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. +*/ + +export function relaunch() { + if (IS_DISCORD_DESKTOP) + window.DiscordNative.app.relaunch(); + else + window.VencordDesktop.app.relaunch(); +} diff --git a/src/utils/settingsSync.ts b/src/utils/settingsSync.ts index 18e1854..781899f 100644 --- a/src/utils/settingsSync.ts +++ b/src/utils/settingsSync.ts @@ -47,7 +47,9 @@ export async function downloadSettingsBackup() { const backup = await exportSettings(); const data = new TextEncoder().encode(backup); - if (IS_WEB) { + if (IS_DISCORD_DESKTOP) { + DiscordNative.fileManager.saveWithDialog(data, filename); + } else { const file = new File([data], filename, { type: "application/json" }); const a = document.createElement("a"); a.href = URL.createObjectURL(file); @@ -59,8 +61,6 @@ export async function downloadSettingsBackup() { URL.revokeObjectURL(a.href); document.body.removeChild(a); }); - } else { - DiscordNative.fileManager.saveWithDialog(data, filename); } } @@ -77,7 +77,24 @@ const toastFailure = (err: any) => Toasts.show({ }); export async function uploadSettingsBackup(showToast = true): Promise<void> { - if (IS_WEB) { + if (IS_DISCORD_DESKTOP) { + const [file] = await DiscordNative.fileManager.openFiles({ + filters: [ + { name: "Vencord Settings Backup", extensions: ["json"] }, + { name: "all", extensions: ["*"] } + ] + }); + + if (file) { + try { + await importSettings(new TextDecoder().decode(file.data)); + if (showToast) toastSuccess(); + } catch (err) { + new Logger("SettingsSync").error(err); + if (showToast) toastFailure(err); + } + } + } else { const input = document.createElement("input"); input.type = "file"; input.style.display = "none"; @@ -102,22 +119,5 @@ export async function uploadSettingsBackup(showToast = true): Promise<void> { document.body.appendChild(input); input.click(); setImmediate(() => document.body.removeChild(input)); - } else { - const [file] = await DiscordNative.fileManager.openFiles({ - filters: [ - { name: "Vencord Settings Backup", extensions: ["json"] }, - { name: "all", extensions: ["*"] } - ] - }); - - if (file) { - try { - await importSettings(new TextDecoder().decode(file.data)); - if (showToast) toastSuccess(); - } catch (err) { - new Logger("SettingsSync").error(err); - if (showToast) toastFailure(err); - } - } } } diff --git a/src/utils/updater.ts b/src/utils/updater.ts index e13f5cf..d0b1fdc 100644 --- a/src/utils/updater.ts +++ b/src/utils/updater.ts @@ -20,6 +20,7 @@ import gitHash from "~git-hash"; import IpcEvents from "./IpcEvents"; import Logger from "./Logger"; +import { relaunch } from "./native"; import { IpcRes } from "./types"; export const UpdateLogger = /* #__PURE__*/ new Logger("Updater", "white"); @@ -90,8 +91,10 @@ export async function maybePromptToUpdate(confirmMessage: string, checkForDev = if (wantsUpdate) { await update(); const needFullRestart = await rebuild(); - if (needFullRestart) DiscordNative.app.relaunch(); - else location.reload(); + if (needFullRestart) + relaunch(); + else + location.reload(); } } } catch (err) { diff --git a/src/webpack/webpack.ts b/src/webpack/webpack.ts index 0d95587..a37fe6d 100644 --- a/src/webpack/webpack.ts +++ b/src/webpack/webpack.ts @@ -67,7 +67,7 @@ export function _initWebpack(instance: typeof window.webpackChunkdiscord_app) { instance.pop(); } -if (IS_DEV && !IS_WEB) { +if (IS_DEV && IS_DISCORD_DESKTOP) { var devToolsOpen = false; // At this point in time, DiscordNative has not been exposed yet, so setImmediate is needed setTimeout(() => { -- cgit