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

* megu cute
This commit is contained in:
megumin 2022-10-04 21:07:34 +01:00 committed by GitHub
parent c5e0c7a6e7
commit 88542b9ede
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 481 additions and 255 deletions

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

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

297
scripts/patcher/common.js Normal file

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

111
scripts/patcher/install.js Normal file

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