Restructure BD into a small mono-repo

This is a repo-wide refactor that introduces the injector to the main branch.

The new architecture is as such:
common/
injector/
renderer/
This commit is contained in:
2021-03-06 03:30:16 -05:00
parent cc0abf089b
commit e3b6cb6485
165 changed files with 13759 additions and 9891 deletions
+1
View File
@@ -0,0 +1 @@
node_modules
+3
View File
@@ -0,0 +1,3 @@
# BetterDiscord Injector
You're probably looking for the main app, [click here](https://github.com/rauenzi/BetterDiscordApp) to go there.
+11
View File
@@ -0,0 +1,11 @@
{
"compilerOptions": {
"target": "es2020",
"allowSyntheticDefaultImports": false,
"baseUrl": "./",
"paths": {
"common": ["../common"]
}
},
"exclude": ["node_modules"]
}
+1810
View File
File diff suppressed because it is too large Load Diff
+19
View File
@@ -0,0 +1,19 @@
{
"name": "betterdiscord-injector",
"version": "0.6.2",
"description": "BetterDiscord injector module",
"main": "src/index.js",
"private": true,
"scripts": {
"build": "cp ./src/preload.js ../dist/preload.js && webpack --progress --color",
"watch": "cp ./src/preload.js ../dist/preload.js && webpack --progress --color --watch",
"build-prod": "cp ./src/preload.js ../dist/preload.js && webpack --stats minimal --mode production",
"lint": "eslint --ext .js src/"
},
"devDependencies": {
"circular-dependency-plugin": "^5.2.2",
"eslint": "^7.21.0",
"webpack": "^5.24.2",
"webpack-cli": "^4.5.0"
}
}
+34
View File
@@ -0,0 +1,34 @@
const path = require("path");
const electron = require("electron");
const Module = require("module");
import ipc from "./modules/ipc";
import BrowserWindow from "./modules/browserwindow";
import CSP from "./modules/csp";
process.env.NODE_OPTIONS = "--no-force-async-hooks-checks";
electron.app.commandLine.appendSwitch("no-force-async-hooks-checks");
process.electronBinding("command_line").appendSwitch("no-force-async-hooks-checks");
// Patch and replace the built-in BrowserWindow
BrowserWindow.patchBrowserWindow();
// Register all IPC events
ipc.registerEvents();
// Remove CSP immediately on linux since they install to discord_desktop_core still
if (process.platform == "win32" || process.platform == "darwin") electron.app.once("ready", CSP.remove);
else CSP.remove();
// Use Discord's info to run the app
if (process.platform == "win32" || process.platform == "darwin") {
const basePath = path.join(electron.app.getAppPath(), "..", "app.asar");
const pkg = __non_webpack_require__(path.join(basePath, "package.json"));
electron.app.setAppPath(basePath);
electron.app.name = pkg.name;
Module._load(path.join(basePath, pkg.main), null, true);
}
+100
View File
@@ -0,0 +1,100 @@
const fs = require("fs");
const path = require("path");
const electron = require("electron");
import ReactDevTools from "./reactdevtools";
import * as IPCEvents from "common/constants/ipcevents";
// Build info file only exists for non-linux (for current injection)
const appPath = electron.app.getAppPath();
const buildInfoFile = path.resolve(appPath, "..", "build_info.json");
// Locate data path to find transparency settings
let dataPath = "";
if (process.platform === "win32") dataPath = process.env.APPDATA;
else if (process.platform === "darwin") dataPath = path.join(process.env.HOME, "Library", "Preferences");
else dataPath = process.env.XDG_CONFIG_HOME ? process.env.XDG_CONFIG_HOME : path.join(process.env.HOME, ".config");
dataPath = path.join(dataPath, "BetterDiscord") + "/";
electron.app.once("ready", async () => {
if (!BetterDiscord.getSetting("developer", "reactDevTools")) return;
await ReactDevTools.install();
});
export default class BetterDiscord {
static getWindowPrefs() {
if (!fs.existsSync(buildInfoFile)) return {};
const buildInfo = __non_webpack_require__(buildInfoFile);
const prefsFile = path.resolve(dataPath, "data", buildInfo.releaseChannel, "windowprefs.json");
if (!fs.existsSync(prefsFile)) return {};
return __non_webpack_require__(prefsFile);
}
static getSetting(category, key) {
if (this._settings) return this._settings[category]?.[key];
try {
const buildInfo = __non_webpack_require__(buildInfoFile);
const settingsFile = path.resolve(dataPath, "data", buildInfo.releaseChannel, "settings.json");
this._settings = __non_webpack_require__(settingsFile) ?? {};
return this._settings[category]?.[key];
}
catch (_) {
this._settings = {};
return this._settings[category]?.[key];
}
}
static ensureDirectories() {
if (!fs.existsSync(dataPath)) fs.mkdirSync(dataPath);
if (!fs.existsSync(path.join(dataPath, "plugins"))) fs.mkdirSync(path.join(dataPath, "plugins"));
if (!fs.existsSync(path.join(dataPath, "themes"))) fs.mkdirSync(path.join(dataPath, "themes"));
}
static async ensureWebpackModules(browserWindow) {
await browserWindow.webContents.executeJavaScript(`new Promise(resolve => {
const check = function() {
if (window.webpackJsonp && window.webpackJsonp.flat().flat().length >= 7000) return resolve();
setTimeout(check, 100);
};
check();
});`);
}
static async injectRenderer(browserWindow) {
const location = path.join(__dirname, "renderer.js");
if (!fs.existsSync(location)) return; // TODO: cut a fatal log
const content = fs.readFileSync(location).toString();
const success = await browserWindow.webContents.executeJavaScript(`
(() => {
try {
${content}
return true;
} catch {
return false;
}
})();
`);
if (!success) return; // TODO: cut a fatal log
}
static setup(browserWindow) {
// Setup some useful vars to avoid blocking IPC calls
process.env.DISCORD_PRELOAD = browserWindow.__originalPreload;
process.env.DISCORD_APP_PATH = appPath;
process.env.DISCORD_USER_DATA = electron.app.getPath("userData");
process.env.BETTERDISCORD_DATA_PATH = dataPath;
// When DOM is available, pass the renderer over the wall
browserWindow.webContents.on("dom-ready", () => {
this.injectRenderer(browserWindow);
});
// This is used to alert renderer code to onSwitch events
browserWindow.webContents.on("did-navigate-in-page", () => {
browserWindow.webContents.send(IPCEvents.NAVIGATE);
});
}
}
+44
View File
@@ -0,0 +1,44 @@
const electron = require("electron");
const path = require("path");
import BetterDiscord from "./betterdiscord";
class BrowserWindow extends electron.BrowserWindow {
constructor(options) {
if (!options || !options.webPreferences || !options.webPreferences.preload || !options.title) return super(options); // eslint-disable-line constructor-super
const originalPreload = options.webPreferences.preload;
options.webPreferences.preload = path.join(__dirname, "preload.js");
// Don't allow just "truthy" values
const shouldBeTransparent = BetterDiscord.getSetting("window", "transparency");
if (typeof(shouldBeTransparent) === "boolean" && shouldBeTransparent) {
options.transparent = true;
options.backgroundColor = "#00000000";
}
// Only affect frame if it is *explicitly* set
// const shouldHaveFrame = BetterDiscord.getSetting("window", "frame");
// if (typeof(shouldHaveFrame) === "boolean") options.frame = shouldHaveFrame;
super(options);
this.__originalPreload = originalPreload;
BetterDiscord.setup(this);
}
}
Object.assign(BrowserWindow, electron.BrowserWindow);
export default class {
static patchBrowserWindow() {
// Reassign electron using proxy to avoid the onReady issue, thanks Powercord!
const newElectron = new Proxy(electron, {
get: function(target, prop) {
if (prop === "BrowserWindow") return BrowserWindow;
return target[prop];
}
});
const electronPath = __non_webpack_require__.resolve("electron");
delete __non_webpack_require__.cache[electronPath].exports; // If it didn't work, try to delete existing
__non_webpack_require__.cache[electronPath].exports = newElectron; // Try to assign again after deleting
}
}
+12
View File
@@ -0,0 +1,12 @@
const electron = require("electron");
export default class {
static remove() {
electron.session.defaultSession.webRequest.onHeadersReceived(function(details, callback) {
if (!details.responseHeaders["content-security-policy-report-only"] && !details.responseHeaders["content-security-policy"]) return callback({cancel: false});
delete details.responseHeaders["content-security-policy-report-only"];
delete details.responseHeaders["content-security-policy"];
callback({cancel: false, responseHeaders: details.responseHeaders});
});
}
}
+74
View File
@@ -0,0 +1,74 @@
import {ipcMain as ipc, BrowserWindow, app, dialog} from "electron";
import * as IPCEvents from "common/constants/ipcevents";
const getPath = (event, pathReq) => {
let returnPath;
switch (pathReq) {
case "appPath":
returnPath = app.getAppPath();
break;
case "appData":
case "userData":
case "home":
case "cache":
case "temp":
case "exe":
case "module":
case "desktop":
case "documents":
case "downloads":
case "music":
case "pictures":
case "videos":
case "recent":
case "logs":
returnPath = app.getPath(pathReq);
break;
default:
returnPath = "";
}
event.returnValue = returnPath;
};
const relaunch = () => {
app.quit();
app.relaunch();
};
const runScript = async (event, script) => {
try {
// TODO: compile with vm to prevent escape with clever strings
await event.sender.executeJavaScript(`(() => {try {${script}} catch {}})();`);
}
catch (e) {
// TODO: cut a log
}
};
const openDevTools = event => event.sender.openDevTools();
const closeDevTools = event => event.sender.closeDevTools();
const createBrowserWindow = async (event, url, {windowOptions, closeOnUrl} = {}) => {
return await new Promise(resolve => {
const windowInstance = new BrowserWindow(windowOptions);
windowInstance.webContents.on("did-navigate", (_, navUrl) => {
if (navUrl != closeOnUrl) return;
windowInstance.close();
resolve();
});
windowInstance.loadURL(url);
});
};
export default class IPCMain {
static registerEvents() {
ipc.on(IPCEvents.GET_PATH, getPath);
ipc.on(IPCEvents.RELAUNCH, relaunch);
ipc.on(IPCEvents.OPEN_DEVTOOLS, openDevTools);
ipc.on(IPCEvents.CLOSE_DEVTOOLS, closeDevTools);
ipc.handle(IPCEvents.RUN_SCRIPT, runScript);
ipc.handle(IPCEvents.OPEN_WINDOW, createBrowserWindow);
}
}
+49
View File
@@ -0,0 +1,49 @@
import fs from "fs";
import path from "path";
import {session} from "electron";
export const REACT_DEVTOOLS_ID = "fmkadmapgofadopljbjfkapdkoienihi";
const findExtension = function() {
let extensionPath = "";
if (process.platform === "win32") extensionPath = path.resolve(process.env.LOCALAPPDATA, "Google/Chrome/User Data");
else if (process.platform === "linux") extensionPath = path.resolve(process.env.HOME, ".config/google-chrome");
else if (process.platform === "darwin") extensionPath = path.resolve(process.env.HOME, "Library/Application Support/Google/Chrome");
else extensionPath = path.resolve(process.env.HOME, ".config/chromium");
extensionPath += `/Default/Extensions/${REACT_DEVTOOLS_ID}`;
if (fs.existsSync(extensionPath)) {
const versions = fs.readdirSync(extensionPath);
extensionPath = path.resolve(extensionPath, versions[versions.length - 1]);
}
const isExtensionInstalled = fs.existsSync(extensionPath);
if (isExtensionInstalled) return extensionPath;
return "";
};
export default class ReactDevTools {
static async install() {
const extPath = findExtension();
if (!extPath) return; // TODO: cut a log
try {
const ext = await session.defaultSession.loadExtension(extPath);
if (!ext) return; // TODO: cut a log
}
catch (err) {
// TODO: cut a log
}
}
static async remove() {
const extPath = findExtension();
if (!extPath) return; // TODO: cut a log
try {
await session.defaultSession.removeExtension(extPath);
}
catch (err) {
// TODO: cut a log
}
}
}
+30
View File
@@ -0,0 +1,30 @@
const Module = require("module");
const path = require("path");
const electron = require("electron");
/* global window:false */
// const context = electron.webFrame.top.context;
Object.defineProperty(window, "webpackJsonp", {
get: () => electron.webFrame.top.context.webpackJsonp
});
electron.webFrame.top.context.global = electron.webFrame.top.context;
electron.webFrame.top.context.require = require;
electron.webFrame.top.context.process = process;
// Load Discord's original preload
const preload = process.env.DISCORD_PRELOAD;
if (preload) {
// Restore original preload for future windows
process.electronBinding("command_line").appendSwitch("preload", preload);
// Run original preload
try {require(preload);}
catch (e) {
// TODO bail out
}
}
Module.globalPaths.push(path.resolve(process.env.DISCORD_APP_PATH, "..", "app.asar", "node_modules"));
+47
View File
@@ -0,0 +1,47 @@
const path = require("path");
const CircularDependencyPlugin = require("circular-dependency-plugin");
const TerserPlugin = require("terser-webpack-plugin");
module.exports = (env, argv) => ({
mode: "development",
target: "node",
devtool: argv.mode === "production" ? undefined : "eval-source-map",
entry: "./src/index.js",
output: {
filename: "injector.js",
path: path.resolve(__dirname, "..", "dist")
},
externals: {
electron: `require("electron")`,
fs: `require("fs")`,
path: `require("path")`,
request: `require("request")`,
events: `require("events")`,
rimraf: `require("rimraf")`,
yauzl: `require("yauzl")`,
mkdirp: `require("mkdirp")`,
module: `require("module")`
},
resolve: {
extensions: [".js"],
alias: {
common: path.resolve(__dirname, "..", "common")
}
},
plugins: [
new CircularDependencyPlugin({
exclude: /node_modules/,
cwd: process.cwd(),
})
],
optimization: {
minimizer: [
new TerserPlugin({
terserOptions: {
compress: {drop_debugger: false},
keep_classnames: true
}
})
]
}
});