From 88542b9ede71fd89b1edef41175131aa0dc5027f Mon Sep 17 00:00:00 2001 From: megumin Date: Tue, 4 Oct 2022 21:07:34 +0100 Subject: [PATCH] feat(installer): Implement cross-platform patcher. (#39) * megu cute --- README.md | 17 +- install.ps1 | 90 ----------- install.sh | 75 --------- package.json | 5 +- scripts/patcher/common.js | 297 +++++++++++++++++++++++++++++++++++ scripts/patcher/install.js | 111 +++++++++++++ scripts/patcher/uninstall.js | 59 +++++++ uninstall.ps1 | 73 --------- uninstall.sh | 9 -- 9 files changed, 481 insertions(+), 255 deletions(-) delete mode 100644 install.ps1 delete mode 100755 install.sh create mode 100644 scripts/patcher/common.js create mode 100644 scripts/patcher/install.js create mode 100644 scripts/patcher/uninstall.js delete mode 100644 uninstall.ps1 delete mode 100755 uninstall.sh diff --git a/README.md b/README.md index ef926827..10db1e67 100644 --- a/README.md +++ b/README.md @@ -15,11 +15,13 @@ A Discord client mod that does things differently ## Installing -If you can't follow the following instructions, please just use BetterDiscord. This was never meant to be a noob friendly mod. +If you can't follow the following instructions, please just use BetterDiscord. +This was never meant to be a noob friendly mod. Install [Node.js](https://nodejs.org/en/download/) and [git](https://git-scm.com/downloads) -Open a Terminal and run the following commands. If any of them failed, you didn't properly install Node.js and git (see above). +Open a Terminal and run the following commands. +If any of them failed, you didn't properly install Node.js and git (see above). > :warning: On Windows, DO NOT run the terminal as Administrator. If you open it and the path says system32, you opened it as Administrator. ```sh @@ -31,12 +33,11 @@ pnpm build ``` Don't close your terminal just yet! -The builds are now in the dist/ folder (Vencord/dist). Most importantly, you will need `dist/patcher.js` +Now to patch vencord into your Discord client, run the following command and follow the interactive prompt. -Now download [X1nto's installer](https://github.com/X1nto/VencordInstaller/releases/latest) for your platform. Download it to the Vencord folder. -Run it via terminal: `VencordInstaller.exe` on Windows or `chmod +x vencord_installer && ./vencord_installer` on Mac. - -Follow along with the prompts. Once you are prompted for the patcher, enter `dist/patcher.js`. +```sh +pnpm patch +``` Now fully close Discord. Start and confirm Vencord successfully installed by checking if you have a new Vencord section in Settings. @@ -44,6 +45,8 @@ If you ever need to get back to the Vencord folder, just open a new terminal and All plugins are disabled by default, so your first step should be opening Settings and enabling the plugins you want. +You can unpatch Vencord using `pnpm unpatch` + ## Installing on Browser diff --git a/install.ps1 b/install.ps1 deleted file mode 100644 index 88a8850c..00000000 --- a/install.ps1 +++ /dev/null @@ -1,90 +0,0 @@ -# Vencord Windows Installer - -$patcher = "$PWD\dist\patcher.js" -$patcher_safe = $patcher -replace '\\', '\\' - -$APP_PATCH = @" -require("$patcher_safe"); -require("../app.asar"); -"@ - -$PACKAGE_JSON = @" -{ - "main": "index.js", - "name": "discord" -} -"@ - -$branch_paths = Get-ChildItem -Directory -Path $env:LOCALAPPDATA | - Select-String -Pattern "Discord\w*" -AllMatches | - Select-String -Pattern "DiscordGames" -NotMatch # Ignore DiscordGames folder - -$branches = @() - -foreach ($branch in $branch_paths) { - $branch = $branch.Line.Split("\")[-1] - - if ($branch -eq "Discord") { - $branch = "Discord Stable" - } else { - $branch = $branch.Replace("Discord", "Discord ") - } - - $branches = $branches + $branch -} - -$branch_count = $branches.Count - -Write-Output "Found $branch_count Branches" -Write-Output "=====================================" -Write-Output "===== Select a Branch to patch ======" - -$i = 0 -foreach ($branch in $branches) { - Write-Output "=== $i. $branch" - $i++ -} - -Write-Output "=====================================" -$pos = Read-Host "Enter a number" - -if ($null -eq $branches[$pos]) { - Write-Output "Invalid branch selection" - exit -} - -$branch = $branches.Get($pos) -$discord_root = $branch_paths.Get($pos) - -Write-Output "`nPatching $branch" - -$app_folders = Get-ChildItem -Directory -Path $discord_root | - Select-String -Pattern "app-" - -foreach ($folder in $app_folders) -{ - $version = [regex]::match($folder, 'app-([\d\.]+)').Groups[1].Value - Write-Output "Patching Version $version" - - $resources = "$folder\resources" - if (-not(Test-Path -Path "$resources")) { - Write-Error "Resources folder does not exist. Outdated version?`n" - continue - } - if (-not(Test-Path -Path "$resources\app.asar")) { - Write-Error "Failed to find app.asar in $folder`n" - continue - } - - $app = "$resources\app" - if (Test-Path -Path $app) { - Write-Error "Are you already patched? App folder already exists at $resources`n" - continue - } - - $null = New-Item -Path $app -ItemType Directory - $null = Tee-Object -InputObject $APP_PATCH -FilePath "$app\index.js" - $null = Tee-Object -InputObject $PACKAGE_JSON -FilePath "$app\package.json" - - Write-Output "Patched $branch (version $version) successfully" -} diff --git a/install.sh b/install.sh deleted file mode 100755 index 945d2de6..00000000 --- a/install.sh +++ /dev/null @@ -1,75 +0,0 @@ -#!/bin/sh -# -# Super simple installer. You should probably run this as root. -# If you are getting permission issues, this is probably why. -# -# If this doesn't work for you, or you're not on Linux, just -# - locate your Discord folder -# - inside the resources folder, create a new folder "app" -# - inside app create the files index.js and package.json. -# See the two tee commands at the end of the file for their contents - -patcher="$PWD/dist/patcher.js" - -discord_bin="$(which discord)" -discord_actual="$(readlink "$discord_bin")" - -if [ -z "$discord_actual" ]; then - case "$(head -n1 "$discord_bin")" in - # has shebang? - \#!/*) - # Wrapper script, assume 2nd line has exec electron call and try to match asar path - path="$(tail -1 "$discord_bin" | grep -Eo "\S+/app.asar" | sed 's/${name}/discord/')" - if [ -z "$path" ]; then - echo "Unsupported Install. $discord_bin is wrapper script but last line isn't exec call?" - exit - elif [ -e "$path" ]; then - discord="$(dirname "$path")" - else - echo "Unsupported Install. $path not found" - exit 1 - fi - ;; - *) - echo "Unsupported Install. $discord_bin is neither symlink nor a wrapper script."; - exit 1 - ;; - esac -else - discord="$(dirname "$discord_actual")" -fi - -resources="$discord/resources" -app="$resources/app" -app_asar="app.asar" - -if [ ! -e "$resources" ]; then - if [ -e "$discord/app.asar.unpacked" ]; then - # System Electron Install - mv "$discord/app.asar" "$discord/_app.asar" - mv "$discord/app.asar.unpacked" "$discord/_app.asar.unpacked" - app="$discord/app.asar" - app_asar="_app.asar" - else - echo "Unsupported Install. $discord has no resources folder but also isn't system electron install" - exit - fi -fi - -if [ -e "$app" ]; then - echo "app folder exists. Looks like your Discord is already modified." - exit -fi - -mkdir "$app" -tee > "$app/index.js" << EOF -require("$patcher"); -require("../$app_asar"); -EOF - -tee > "$app/package.json" << EOF -{ - "main": "index.js", - "name": "discord" -} -EOF diff --git a/package.json b/package.json index 39ac6996..515abae7 100644 --- a/package.json +++ b/package.json @@ -8,12 +8,15 @@ "yazl": "^2.5.1" }, "dependencies": { + "console-menu": "^0.1.0", "discord-types": "^1.3.26", "electron-devtools-installer": "^3.2.0" }, "scripts": { "buildWeb": "node buildWeb.mjs", "build": "node build.mjs", - "watch": "node build.mjs --watch" + "watch": "node build.mjs --watch", + "patch": "node scripts/patcher/install.js", + "unpatch": "node scripts/patcher/uninstall.js" } } diff --git a/scripts/patcher/common.js b/scripts/patcher/common.js new file mode 100644 index 00000000..94cb383c --- /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 00000000..42ba47f7 --- /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 00000000..c470c643 --- /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 + ); + } +} diff --git a/uninstall.ps1 b/uninstall.ps1 deleted file mode 100644 index 409b20fd..00000000 --- a/uninstall.ps1 +++ /dev/null @@ -1,73 +0,0 @@ -# Vencord Uninstaller - -$branch_paths = Get-ChildItem -Directory -Path $env:LOCALAPPDATA | - Select-String -Pattern "Discord\w*" -AllMatches | - Select-String -Pattern "DiscordGames" -NotMatch # Ignore DiscordGames folder - -$branches = @() - -foreach ($branch in $branch_paths) { - $branch = $branch.Line.Split("\")[-1] - - if ($branch -eq "Discord") { - $branch = "Discord Stable" - } else { - $branch = $branch.Replace("Discord", "Discord ") - } - - $branches = $branches + $branch -} - -$branch_count = $branches.Count - -Write-Output "Found $branch_count Branches" -Write-Output "=====================================" -Write-Output "===== Select a Branch to unpatch ======" - -$i = 0 -foreach ($branch in $branches) { - Write-Output "=== $i. $branch" - $i++ -} - -Write-Output "=====================================" -$pos = Read-Host "Enter a number" - -if ($null -eq $branches[$pos]) { - Write-Output "Invalid branch selection" - exit -} - -$branch = $branches.Get($pos) -$discord_root = $branch_paths.Get($pos) - -Write-Output "`nUnpatch $branch" - -$app_folders = Get-ChildItem -Directory -Path $discord_root | - Select-String -Pattern "app-" - -foreach ($folder in $app_folders) -{ - $version = [regex]::match($folder, 'app-([\d\.]+)').Groups[1].Value - Write-Output "Unpatching $branch Version $version" - - $resources = "$folder\resources" - if (-not(Test-Path -Path "$resources")) { - Write-Output "Resources folder doesn't exist... Possibly an outdated copy and can be ignored.`n" - continue - } - if (-not(Test-Path -Path "$resources\app")) { - Write-Output "App folder doesn't exist... Already unpatched?`n" - continue - } - - Remove-Item -Path "$folder\resources\app" -Recurse -Force -Confirm:$false - - if (Test-Path "$folder\resources\app") - { - Write-Error "Failed to delete $folder\resources\app" - } else { - Write-Output "Successfully unpatched $branch (version $version)" - } - -} \ No newline at end of file diff --git a/uninstall.sh b/uninstall.sh deleted file mode 100755 index 4b4b4229..00000000 --- a/uninstall.sh +++ /dev/null @@ -1,9 +0,0 @@ -#!/bin/sh -# Super simple uninstaller. -# If this doesn't work for you, or you're not on Linux, just -# manually delete the app folder in your Discord folder (inside resources) - -set -e - -discord="$(dirname "$(readlink "$(which discord)")")" -rm -r --interactive=never "${discord:?Cant find discord}/resources/app"