diff options
Diffstat (limited to 'src/main')
-rw-r--r-- | src/main/index.ts | 108 | ||||
-rw-r--r-- | src/main/ipcMain.ts | 107 | ||||
-rw-r--r-- | src/main/patchWin32Updater.ts | 99 | ||||
-rw-r--r-- | src/main/patcher.ts | 120 | ||||
-rw-r--r-- | src/main/updater/common.ts | 59 | ||||
-rw-r--r-- | src/main/updater/git.ts | 83 | ||||
-rw-r--r-- | src/main/updater/http.ts | 104 | ||||
-rw-r--r-- | src/main/updater/index.ts | 19 | ||||
-rw-r--r-- | src/main/utils/constants.ts | 37 | ||||
-rw-r--r-- | src/main/utils/crxToZip.ts | 57 | ||||
-rw-r--r-- | src/main/utils/extensions.ts | 85 | ||||
-rw-r--r-- | src/main/utils/simpleGet.ts | 37 |
12 files changed, 915 insertions, 0 deletions
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))); + }); + }); +} |