From 88542b9ede71fd89b1edef41175131aa0dc5027f Mon Sep 17 00:00:00 2001 From: megumin Date: Tue, 4 Oct 2022 21:07:34 +0100 Subject: feat(installer): Implement cross-platform patcher. (#39) * megu cute --- scripts/patcher/common.js | 297 +++++++++++++++++++++++++++++++++++++++++++ scripts/patcher/install.js | 111 ++++++++++++++++ scripts/patcher/uninstall.js | 59 +++++++++ 3 files changed, 467 insertions(+) create mode 100644 scripts/patcher/common.js create mode 100644 scripts/patcher/install.js create mode 100644 scripts/patcher/uninstall.js (limited to 'scripts/patcher') diff --git a/scripts/patcher/common.js b/scripts/patcher/common.js new file mode 100644 index 0000000..94cb383 --- /dev/null +++ b/scripts/patcher/common.js @@ -0,0 +1,297 @@ +const path = require("path"); +const readline = require("readline"); +const fs = require("fs"); +const menu = require("console-menu"); + +const BRANCH_NAMES = [ + "Discord", + "DiscordPTB", + "DiscordCanary", + "DiscordDevelopment", + "discord", + "discordptb", + "discordcanary", + "discorddevelopment", + "discord-ptb", + "discord-canary", + "discord-development", + // Flatpak + "com.discordapp.Discord", + "com.discordapp.DiscordPTB", + "com.discordapp.DiscordCanary", + "com.discordapp.DiscordDevelopment", +]; + +const MACOS_DISCORD_DIRS = [ + "Discord.app", + "Discord PTB.app", + "Discord Canary.app", + "Discord Development.app", +]; + +if (process.platform === "linux" && process.env.SUDO_USER) { + process.env.HOME = fs + .readFileSync("/etc/passwd", "utf-8") + .match(new RegExp(`^${process.env.SUDO_USER}.+$`, "m"))[0] + .split(":")[5]; +} + +const LINUX_DISCORD_DIRS = [ + "/usr/share", + "/usr/lib64", + "/opt", + `${process.env.HOME}/.local/share`, + "/var/lib/flatpak/app", + `${process.env.HOME}/.local/share/flatpak/app`, +]; + +const FLATPAK_NAME_MAPPING = { + DiscordCanary: "discord-canary", + DiscordPTB: "discord-ptb", + DiscordDevelopment: "discord-development", + Discord: "discord", +}; + +const ENTRYPOINT = path + .join(process.cwd(), "dist", "patcher.js") + .replace(/\\/g, "/"); + +function question(question) { + const rl = readline.createInterface({ + input: process.stdin, + output: process.stdout, + terminal: false, + }); + + return new Promise((resolve) => { + rl.question(question, (answer) => { + rl.close(); + resolve(answer); + }); + }); +} + +async function getMenuItem(installations) { + let menuItems = installations.map((info) => ({ + title: info.patched ? "[MODIFIED] " + info.location : info.location, + info, + })); + + if (menuItems.length === 0) { + console.log("No Discord installations found."); + process.exit(1); + } + + const result = await menu( + [...menuItems, { title: "Exit without patching", exit: true }], + { + header: "Select a Discord installation to patch:", + border: true, + helpMessage: + "Use the up/down arrow keys to select an option. " + + "Press ENTER to confirm.", + } + ); + + if (!result || !result.info || result.exit) { + console.log("No installation selected."); + process.exit(0); + } + + if (result.info.patched) { + const answer = await question( + "This installation has already been modified. Overwrite? [Y/n]: " + ); + + if (!["y", "yes", "yeah", ""].includes(answer.toLowerCase())) { + console.log("Not patching."); + process.exit(0); + } + } + + return result.info; +} + +function getWindowsDirs() { + const dirs = []; + for (const dir of fs.readdirSync(process.env.LOCALAPPDATA)) { + if (!BRANCH_NAMES.includes(dir)) continue; + + const location = path.join(process.env.LOCALAPPDATA, dir); + if (!fs.statSync(location).isDirectory()) continue; + + const appDirs = fs + .readdirSync(location, { withFileTypes: true }) + .filter((file) => file.isDirectory()) + .filter((file) => file.name.startsWith("app-")) + .map((file) => path.join(location, file.name)); + + let versions = []; + let patched = false; + + for (const fqAppDir of appDirs) { + const resourceDir = path.join(fqAppDir, "resources"); + if (!fs.existsSync(path.join(resourceDir, "app.asar"))) { + continue; + } + const appDir = path.join(resourceDir, "app"); + if (fs.existsSync(appDir)) { + patched = true; + } + versions.push({ + path: appDir, + name: /app-([0-9\.]+)/.exec(fqAppDir)[1], + }); + } + + if (appDirs.length) { + dirs.push({ + branch: dir, + patched, + location, + versions, + arch: "win32", + flatpak: false, + }); + } + } + return dirs; +} + +function getDarwinDirs() { + const dirs = []; + for (const dir of fs.readdirSync("/Applications")) { + if (!MACOS_DISCORD_DIRS.includes(dir)) continue; + + const location = path.join("/Applications", dir, "Contents"); + if (!fs.existsSync(location)) continue; + if (!fs.statSync(location).isDirectory()) continue; + + const appDirs = fs + .readdirSync(location, { withFileTypes: true }) + .filter((file) => file.isDirectory()) + .filter((file) => file.name.startsWith("Resources")) + .map((file) => path.join(location, file.name)); + + let versions = []; + let patched = false; + + for (const resourceDir of appDirs) { + if (!fs.existsSync(path.join(resourceDir, "app.asar"))) { + continue; + } + const appDir = path.join(resourceDir, "app"); + if (fs.existsSync(appDir)) { + patched = true; + } + + versions.push({ + path: appDir, + name: null, // MacOS installs have no version number + }); + } + + if (appDirs.length) { + dirs.push({ + branch: dir, + patched, + location, + versions, + arch: "win32", + }); + } + } + return dirs; +} + +function getLinuxDirs() { + const dirs = []; + for (const dir of LINUX_DISCORD_DIRS) { + if (!fs.existsSync(dir)) continue; + for (const branch of fs.readdirSync(dir)) { + if (!BRANCH_NAMES.includes(branch)) continue; + + const location = path.join(dir, branch); + if (!fs.statSync(location).isDirectory()) continue; + + const isFlatpak = location.includes("/flatpak/"); + + let appDirs = []; + + if (isFlatpak) { + const fqDir = path.join(location, "current", "active", "files"); + if (!/com\.discordapp\.(\w+)\//.test(fqDir)) continue; + const branchName = /com\.discordapp\.(\w+)\//.exec(fqDir)[1]; + if (!Object.keys(FLATPAK_NAME_MAPPING).includes(branchName)) { + continue; + } + const appDir = path.join( + fqDir, + FLATPAK_NAME_MAPPING[branchName] + ); + + if (!fs.existsSync(appDir)) continue; + if (!fs.statSync(appDir).isDirectory()) continue; + + const resourceDir = path.join(appDir, "resources"); + + appDirs.push(resourceDir); + } else { + appDirs = fs + .readdirSync(location, { withFileTypes: true }) + .filter((file) => file.isDirectory()) + .filter( + (file) => + file.name.startsWith("app-") || + file.name === "resources" + ) + .map((file) => path.join(location, file.name)); + } + + let versions = []; + let patched = false; + + for (const resourceDir of appDirs) { + if (!fs.existsSync(path.join(resourceDir, "app.asar"))) { + continue; + } + const appDir = path.join(resourceDir, "app"); + if (fs.existsSync(appDir)) { + patched = true; + } + + const version = /app-([0-9\.]+)/.exec(resourceDir); + + versions.push({ + path: appDir, + name: version && version.length > 1 ? version[1] : null, + }); + } + + if (appDirs.length) { + dirs.push({ + branch, + patched, + location, + versions, + arch: "linux", + isFlatpak, + }); + } + } + } + return dirs; +} + +module.exports = { + BRANCH_NAMES, + MACOS_DISCORD_DIRS, + LINUX_DISCORD_DIRS, + FLATPAK_NAME_MAPPING, + ENTRYPOINT, + question, + getMenuItem, + getWindowsDirs, + getDarwinDirs, + getLinuxDirs, +}; diff --git a/scripts/patcher/install.js b/scripts/patcher/install.js new file mode 100644 index 0000000..42ba47f --- /dev/null +++ b/scripts/patcher/install.js @@ -0,0 +1,111 @@ +const path = require("path"); +const fs = require("fs"); +const { execSync } = require("child_process"); + +console.log("\nVencord Installer\n"); + +if (!fs.existsSync(path.join(process.cwd(), "node_modules"))) { + console.log("You need to install dependencies first. Run:", "pnpm install"); + process.exit(1); +} + +if (!fs.existsSync(path.join(process.cwd(), "dist", "patcher.js"))) { + console.log("You need to build the project first. Run:", "pnpm build"); + process.exit(1); +} + +const { + getMenuItem, + getWindowsDirs, + getDarwinDirs, + getLinuxDirs, + ENTRYPOINT, +} = require("./common"); + +switch (process.platform) { + case "win32": + install(getWindowsDirs()); + break; + case "darwin": + install(getDarwinDirs()); + break; + case "linux": + install(getLinuxDirs()); + break; + default: + console.log("Unknown OS"); + break; +} + +async function install(installations) { + const selected = await getMenuItem(installations); + + // Attempt to give flatpak perms + if (selected.isFlatpak) { + try { + const branch = selected.branch; + const cwd = process.cwd(); + const globalCmd = `flatpak override ${branch} --filesystem=${cwd}`; + const userCmd = `flatpak override --user ${branch} --filesystem=${cwd}`; + const cmd = selected.location.startsWith("/home") + ? userCmd + : globalCmd; + execSync(cmd); + console.log("Successfully gave write perms to Discord Flatpak."); + } catch (e) { + console.log("Failed to give write perms to Discord Flatpak."); + console.log( + "Try running this script as an administrator:", + "sudo pnpm patch" + ); + process.exit(1); + } + } + + for (const version of selected.versions) { + const dir = version.path; + // Check if we have write perms to the install directory... + try { + fs.accessSync(selected.location, fs.constants.W_OK); + } catch (e) { + console.error("No write access to", selected.location); + console.error( + "Try running this script as an administrator:", + "sudo pnpm patch" + ); + process.exit(1); + } + if (fs.existsSync(dir) && fs.lstatSync(dir).isDirectory()) { + fs.rmSync(dir, { recursive: true }); + } + if (!fs.existsSync(dir)) { + fs.mkdirSync(dir, { recursive: true }); + } + + fs.writeFileSync( + path.join(dir, "index.js"), + `require("${ENTRYPOINT}"); require("../app.asar");` + ); + fs.writeFileSync( + path.join(dir, "package.json"), + JSON.stringify({ + name: "discord", + main: "index.js", + }) + ); + + const requiredFiles = ["index.js", "package.json"]; + + if (requiredFiles.every((f) => fs.existsSync(path.join(dir, f)))) { + console.log( + "Successfully patched", + version.name + ? `${selected.branch} ${version.name}` + : selected.branch + ); + } else { + console.log("Failed to patch", dir); + console.log("Files in directory:", fs.readdirSync(dir)); + } + } +} diff --git a/scripts/patcher/uninstall.js b/scripts/patcher/uninstall.js new file mode 100644 index 0000000..c470c64 --- /dev/null +++ b/scripts/patcher/uninstall.js @@ -0,0 +1,59 @@ +const path = require("path"); +const fs = require("fs"); + +console.log("\nVencord Uninstaller\n"); + +if (!fs.existsSync(path.join(process.cwd(), "node_modules"))) { + console.log("You need to install dependencies first. Run:", "pnpm install"); + process.exit(1); +} + +const { + getMenuItem, + getWindowsDirs, + getDarwinDirs, + getLinuxDirs, +} = require("./common"); + +switch (process.platform) { + case "win32": + uninstall(getWindowsDirs()); + break; + case "darwin": + uninstall(getDarwinDirs()); + break; + case "linux": + uninstall(getLinuxDirs()); + break; + default: + console.log("Unknown OS"); + break; +} + +async function uninstall(installations) { + const selected = await getMenuItem(installations); + + for (const version of selected.versions) { + const dir = version.path; + // Check if we have write perms to the install directory... + try { + fs.accessSync(selected.location, fs.constants.W_OK); + } catch (e) { + console.error("No write access to", selected.location); + console.error( + "Try running this script as an administrator:", + "sudo pnpm unpatch" + ); + process.exit(1); + } + if (fs.existsSync(dir)) { + fs.rmSync(dir, { recursive: true }); + } + console.log( + "Successfully unpatched", + version.name + ? `${selected.branch} ${version.name}` + : selected.branch + ); + } +} -- cgit