feat(installer): Implement cross-platform patcher. (#39)

* megu cute
main
megumin 2 years ago committed by GitHub
parent c5e0c7a6e7
commit 88542b9ede
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -15,11 +15,13 @@ A Discord client mod that does things differently
## Installing ## 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) 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. > :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 ```sh
@ -31,12 +33,11 @@ pnpm build
``` ```
Don't close your terminal just yet! 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. ```sh
Run it via terminal: `VencordInstaller.exe` on Windows or `chmod +x vencord_installer && ./vencord_installer` on Mac. pnpm patch
```
Follow along with the prompts. Once you are prompted for the patcher, enter `dist/patcher.js`.
Now fully close Discord. Start and confirm Vencord successfully installed by checking if you have a new Vencord section in Settings. 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. 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 ## Installing on Browser

@ -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"
}

@ -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

@ -8,12 +8,15 @@
"yazl": "^2.5.1" "yazl": "^2.5.1"
}, },
"dependencies": { "dependencies": {
"console-menu": "^0.1.0",
"discord-types": "^1.3.26", "discord-types": "^1.3.26",
"electron-devtools-installer": "^3.2.0" "electron-devtools-installer": "^3.2.0"
}, },
"scripts": { "scripts": {
"buildWeb": "node buildWeb.mjs", "buildWeb": "node buildWeb.mjs",
"build": "node build.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"
} }
} }

@ -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,
};

@ -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));
}
}
}

@ -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
);
}
}

@ -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)"
}
}

@ -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"
Loading…
Cancel
Save