Merge pull request #1408 from BetterDiscord/electron17

Prepare for electron 17 and patch for swc
This commit is contained in:
Zack 2022-09-26 23:39:31 -04:00 committed by GitHub
commit b2aae545c4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
62 changed files with 6041 additions and 17045 deletions

View File

@ -1,7 +1,8 @@
{ {
"extends": "eslint:recommended", "extends": "eslint:recommended",
"env": { "env": {
"node": true "node": true,
"es2020": true
}, },
"parserOptions": { "parserOptions": {
"ecmaVersion": 2022, "ecmaVersion": 2022,
@ -82,15 +83,7 @@
"yoda": "error" "yoda": "error"
}, },
"globals": { "globals": {
"Proxy": "readonly",
"Set": "readonly",
"WeakMap": "readonly",
"WeakSet": "readonly",
"Map": "readonly",
"Promise": "readonly",
"Reflect": "readonly",
"DiscordNative": "readonly", "DiscordNative": "readonly",
"__non_webpack_require__": "readonly", "__non_webpack_require__": "readonly"
"Symbol": "readonly"
} }
} }

View File

@ -40,7 +40,7 @@ For normal users, installing via the installers makes the most sense. However wh
### Prerequisites ### Prerequisites
- [Git](https://git-scm.com) - [Git](https://git-scm.com)
- [Node.js](https://nodejs.org/en/) with `npm`. - [Node.js](https://nodejs.org/en/) with [pnpm](https://pnpm.io/).
- Command line of your choice. - Command line of your choice.
### 1: Clone the repository ### 1: Clone the repository
@ -49,25 +49,25 @@ git clone https://github.com/BetterDiscord/BetterDiscord.git
``` ```
### 2: Install dependencies ### 2: Install dependencies
```ps ```ps
npm install pnpm recursive install
``` ```
### 3: Run Build Script ### 3: Run Build Script
This will create a `injector.js`, `preload.js`, and `renderer.js` in the `dist` folder. This will create a `injector.js`, `preload.js`, and `renderer.js` in the `dist` folder.
```ps ```ps
npm run build pnpm run build
``` ```
### 4: Inject into your Discord client ### 4: Inject into your Discord client
#### Install to Stable #### Install to Stable
```ps ```ps
npm run inject pnpm run inject
``` ```
#### Install to PTB #### Install to PTB
```ps ```ps
npm run inject ptb pnpm run inject ptb
``` ```
#### Install to Canary #### Install to Canary
```ps ```ps
npm run inject canary pnpm run inject canary
``` ```
## Additional Scripts ## Additional Scripts
@ -75,7 +75,7 @@ npm run inject canary
### Compiling & Distribution ### Compiling & Distribution
This will create a `betterdiscord.asar` file in the `dist` folder. This will create a `betterdiscord.asar` file in the `dist` folder.
```ps ```ps
npm run dist pnpm run dist
``` ```
--- ---

19
common/clone.js Normal file
View File

@ -0,0 +1,19 @@
export function getKeys(object) {
const keys = [];
for (const key in object) keys.push(key);
return keys;
}
export default function cloneObject(target, newObject = {}, keys) {
if (!Array.isArray(keys)) keys = getKeys(target);
return keys.reduce((clone, key) => {
if (typeof(target[key]) === "object" && !Array.isArray(target[key]) && target[key] !== null) clone[key] = cloneObject(target[key], {});
else if (typeof target[key] === "function") clone[key] = target[key].bind(target);
else clone[key] = target[key];
return clone;
}, newObject);
}

36
common/events.js Normal file
View File

@ -0,0 +1,36 @@
import Logger from "./logger";
export default class EventEmitter {
static get EventEmitter() {return EventEmitter;}
constructor() {
this.events = {};
}
setMaxListeners() {}
on(event, callback) {
if (!this.events[event]) this.events[event] = new Set();
this.events[event].add(callback);
}
emit(event, ...args) {
if (!this.events[event]) return;
for (const [index, listener] of this.events[event].entries()) {
try {
listener(...args);
}
catch (error) {
Logger.error("Emitter", `Cannot fire listener for event ${event} at position ${index}:`, error);
}
}
}
off(event, callback) {
if (!this.events[event]) return;
return this.events[event].delete(callback);
}
}

View File

@ -1,3 +1,3 @@
# BetterDiscord Injector # BetterDiscord Injector
You're probably looking for the main app, [click here](https://github.com/rauenzi/BetterDiscordApp) to go there. You're probably looking for the main app, [click here](https://github.com/BetterDiscord/BetterDiscord) to go there.

File diff suppressed because it is too large Load Diff

View File

@ -1,5 +1,5 @@
{ {
"name": "betterdiscord-injector", "name": "@betterdiscord/injector",
"version": "0.6.2", "version": "0.6.2",
"description": "BetterDiscord injector module", "description": "BetterDiscord injector module",
"main": "src/index.js", "main": "src/index.js",
@ -11,10 +11,6 @@
"lint": "eslint --ext .js src/" "lint": "eslint --ext .js src/"
}, },
"devDependencies": { "devDependencies": {
"circular-dependency-plugin": "^5.2.2", "webpack": "^5.73.0"
"copy-webpack-plugin": "^8.0.0",
"eslint": "^7.21.0",
"webpack": "^5.24.2",
"webpack-cli": "^4.5.0"
} }
} }

View File

@ -65,7 +65,8 @@ export default class BetterDiscord {
try { try {
${content} ${content}
return true; return true;
} catch { } catch(error) {
console.error(error);
return false; return false;
} }
})(); })();

View File

@ -1,59 +0,0 @@
const Module = require("module");
const path = require("path");
const electron = require("electron");
const NodeEvents = require("events");
const cloneObject = function (target, newObject = {}, keys) {
if (!Array.isArray(keys)) keys = Object.keys(Object.getOwnPropertyDescriptors(target));
return keys.reduce((clone, key) => {
if (typeof(target[key]) === "object" && !Array.isArray(target[key]) && target[key] !== null && !(target[key] instanceof NodeEvents)) clone[key] = cloneObject(target[key], {});
else clone[key] = target[key];
return clone;
}, newObject);
};
/* 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.Buffer = Buffer;
electron.webFrame.top.context.process = new class PatchedProcess extends NodeEvents {
get __ORIGINAL_PROCESS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED__() {return process;}
constructor() {
super();
Object.assign(this,
cloneObject(process, {}, Object.keys(NodeEvents.prototype)),
cloneObject(process, {})
);
}
};
// Load Discord's original preload
const preload = process.env.DISCORD_PRELOAD;
if (preload) {
// Restore original preload for future windows
electron.ipcRenderer.send("bd-register-preload", preload);
// Run original preload
try {
const originalKill = process.kill;
process.kill = function() {};
require(preload);
process.kill = originalKill;
}
catch (e) {
// TODO bail out
}
}
Module.globalPaths.push(path.resolve(process.env.DISCORD_APP_PATH, "..", "app.asar", "node_modules"));

View File

@ -1,6 +1,4 @@
const path = require("path"); const path = require("path");
const CopyPlugin = require("copy-webpack-plugin");
const CircularDependencyPlugin = require("circular-dependency-plugin");
const TerserPlugin = require("terser-webpack-plugin"); const TerserPlugin = require("terser-webpack-plugin");
module.exports = (env, argv) => ({ module.exports = (env, argv) => ({
@ -29,20 +27,6 @@ module.exports = (env, argv) => ({
common: path.resolve(__dirname, "..", "common") common: path.resolve(__dirname, "..", "common")
} }
}, },
plugins: [
new CircularDependencyPlugin({
exclude: /node_modules/,
cwd: process.cwd(),
}),
new CopyPlugin({
patterns: [
{
from: path.resolve(__dirname, "src", "preload.js"),
to: path.resolve(__dirname, "..", "dist", "preload.js")
},
],
})
],
optimization: { optimization: {
minimizer: [ minimizer: [
new TerserPlugin({ new TerserPlugin({

2060
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,26 +1,31 @@
{ {
"name": "betterdiscord", "name": "betterdiscord",
"version": "1.6.3", "version": "1.7.0",
"description": "Enhances Discord by adding functionality and themes.", "description": "Enhances Discord by adding functionality and themes.",
"main": "src/index.js", "main": "src/index.js",
"scripts": { "scripts": {
"install": "cd injector && npm install && cd ../renderer && npm install", "build": "pnpm run build-injector && pnpm run build-preload && pnpm run build-renderer",
"build": "npm run build-injector && npm run build-renderer", "build-prod": "pnpm --filter injector build-prod && pnpm --filter preload build-prod && pnpm --filter renderer build-prod",
"build-prod": "npm run build-prod --prefix injector && npm run build-prod --prefix renderer", "build-injector": "pnpm --filter injector build",
"build-injector": "npm run build --prefix injector", "build-renderer": "pnpm --filter renderer build",
"build-renderer": "npm run build --prefix renderer", "build-preload": "pnpm --filter preload build",
"pack-emotes": "node scripts/emotes.js", "pack-emotes": "node scripts/emotes.js",
"inject": "node scripts/inject.js", "inject": "node scripts/inject.js",
"lint": "eslint --ext .js common/ && npm run lint --prefix injector && npm run lint --prefix renderer", "lint": "eslint --ext .js common/ && pnpm --filter injector lint && pnpm --filter preload lint && pnpm --filter renderer lint-js",
"test": "mocha --require @babel/register --recursive \"./tests/renderer/*.js\"", "test": "mocha --require @babel/register --recursive \"./tests/renderer/*.js\"",
"dist": "npm run build-prod && node scripts/pack.js", "dist": "pnpm run build-prod && node scripts/pack.js",
"api": "jsdoc -X renderer/src/modules/pluginapi.js > jsdoc-ast.json" "api": "jsdoc -X renderer/src/modules/pluginapi.js > jsdoc-ast.json"
}, },
"devDependencies": { "devDependencies": {
"asar": "^3.0.3", "asar": "^3.2.0",
"eslint": "^7.12.0", "eslint": "^8.23.0",
"eslint-plugin-react": "^7.21.5", "eslint-plugin-react": "^7.31.6",
"jsdoc": "^3.6.11", "mocha": "^10.0.0",
"mocha": "^10.0.0" "webpack": "^5.74.0",
"webpack-cli": "^4.10.0"
},
"engines": {
"node": ">=14",
"pnpm": ">=7"
} }
} }

4862
pnpm-lock.yaml Normal file

File diff suppressed because it is too large Load Diff

4
pnpm-workspace.yaml Normal file
View File

@ -0,0 +1,4 @@
packages:
- "injector/"
- "preload/"
- "renderer/"

3
preload/README.md Normal file
View File

@ -0,0 +1,3 @@
# BetterDiscord Preload
You're probably looking for the main app, [click here](https://github.com/BetterDiscord/BetterDiscord) to go there.

11
preload/jsconfig.json Normal file
View File

@ -0,0 +1,11 @@
{
"compilerOptions": {
"target": "es2020",
"allowSyntheticDefaultImports": false,
"baseUrl": "./",
"paths": {
"common": ["../common"]
}
},
"exclude": ["node_modules"]
}

16
preload/package.json Normal file
View File

@ -0,0 +1,16 @@
{
"name": "@betterdiscord/preload",
"version": "0.0.1",
"description": "BetterDiscord preload module",
"main": "src/index.js",
"private": true,
"scripts": {
"build": "webpack --progress --color",
"watch": "webpack --progress --color --watch",
"build-prod": "webpack --stats minimal --mode production",
"lint": "eslint --ext .js src/"
},
"devDependencies": {
"webpack": "^5.73.0"
}
}

28
preload/src/api/crypto.js Normal file
View File

@ -0,0 +1,28 @@
const crypto = (() => {
let cache = null;
return () => {
if (cache) return cache;
return cache = __non_webpack_require__("crypto");
};
})();
export function createHash(type) {
const hash = crypto().createHash(type);
const ctx = {
update(data) {
hash.update(data);
return ctx;
},
digest(encoding) {return hash.digest(encoding);}
};
return ctx;
}
export function randomBytes(length) {
return crypto().randomBytes(length);
}

View File

@ -0,0 +1,13 @@
import {ipcRenderer as IPC, shell} from "electron";
export const ipcRenderer = {
send: IPC.send.bind(IPC),
sendToHost: IPC.sendToHost.bind(IPC),
sendTo: IPC.sendTo.bind(IPC),
sendSync: IPC.sendSync.bind(IPC),
invoke: IPC.invoke.bind(IPC),
on: IPC.on.bind(IPC),
off: IPC.off.bind(IPC)
};
export {shell};

View File

@ -0,0 +1,77 @@
import * as fs from "fs";
import cloneObject from "common/clone";
import Logger from "common/logger";
export function readFile(path, options = "utf8") {
return fs.readFileSync(path, options);
}
export function writeFile(path, content, options) {
if (content instanceof Uint8Array) {
content = Buffer.from(content);
}
const doWriteFile = options?.originalFs ? __non_webpack_require__("original-fs").writeFileSync : fs.writeFileSync;
return doWriteFile(path, content, options);
}
export function readDirectory(path, options) {
return fs.readdirSync(path, options);
}
export function createDirectory(path, options) {
return fs.mkdirSync(path, options);
}
export function deleteDirectory(path, options) {
fs.rmdirSync(path, options);
}
export function exists(path) {
return fs.existsSync(path);
}
export function getRealPath(path, options) {
return fs.realpathSync(path, options);
}
export function rename(oldPath, newPath) {
return fs.renameSync(oldPath, newPath);
}
export function unlinkSync(fileToDelete) {
return fs.unlinkSync(fileToDelete);
}
export function createWriteStream(path, options) {
return cloneObject(fs.createWriteStream(path, options));
}
export function watch(path, options, callback) {
const watcher = fs.watch(path, options, (event, filename) => {
try {
callback(event, filename);
}
catch (error) {
Logger.stacktrace("filesystem", "Failed to watch path", error);
}
});
return {
close: () => {
watcher.close();
}
};
}
export function getStats(path, options) {
const stats = fs.statSync(path, options);
return {
...stats,
isFile: stats.isFile.bind(stats),
isDirectory: stats.isDirectory.bind(stats),
isSymbolicLink: stats.isSymbolicLink.bind(stats)
};
}

59
preload/src/api/https.js Normal file
View File

@ -0,0 +1,59 @@
import * as https from "https";
const methods = ["get", "put", "post", "delete"];
const headersToClone = ["statusCode", "statusMessage", "url", "headers", "method", "aborted", "complete", "rawHeaders", "end"];
const request = function (url, options, callback) {
let responseObject = undefined;
let pipe = undefined;
const req = https.request(url, Object.assign({method: "GET"}, options), res => {
const chunks = [];
let error = null;
responseObject = res;
if (pipe) {
res.pipe(pipe);
}
res.addListener("error", err => {error = err;});
res.addListener("data", chunk => {
chunks.push(chunk);
});
res.addListener("end", () => {
const headers = Object.fromEntries(headersToClone.map(h => [h, res[h]]));
callback(error, headers, Buffer.concat(chunks));
req.end();
});
});
req.end();
return {
end() {req.end();},
pipe(fsStream) {
if (!responseObject) {
pipe = fsStream;
} else {
responseObject.pipe(fsStream);
}
}
};
};
export default Object.assign({request},
Object.fromEntries(methods.map(method => [
method,
function () {
arguments[1] ??= {};
arguments[1].method ??= method.toUpperCase();
return Reflect.apply(request, this, arguments);
}
]))
);

44
preload/src/api/index.js Normal file
View File

@ -0,0 +1,44 @@
import fs from "fs";
import path from "path";
import Module from "module";
// const Module = require("module");
Module.globalPaths.push(path.resolve(process.env.DISCORD_APP_PATH, "..", "app.asar", "node_modules"));
// module.paths.push(path.resolve(process.env.DISCORD_APP_PATH, "..", "app.asar", "node_modules"));
Module._load = (load => (req, parent, isMain) => {
if (req.includes("./") || req.includes("..")) return load(req, parent, isMain);
const found = Module.globalPaths.find(m => fs.existsSync(path.resolve(m, req)));
return found ? load(path.resolve(found, req), parent, isMain) : load(req, parent, isMain);
})(Module._load);
// const originalLoad = Module.prototype.load;
// Module.prototype.load = function() {
// const returnValue = Reflect.apply(originalLoad, this, arguments);
// console.log(this, arguments, returnValue);
// return returnValue;
// };
// const nodeModulePaths = Module._nodeModulePaths;
// console.log(nodeModulePaths);
// Module._nodeModulePaths = (from) => {
// return nodeModulePaths(from).concat([path.resolve(process.env.DISCORD_APP_PATH, "..", "app.asar", "node_modules")]);
// };
// console.log(Module._nodeModulePaths, Module._nodeModulePaths("request"));
// console.dir(Module);
// console.log(Object.keys(Module));
// console.log(require("request"));
export * as filesystem from "./filesystem";
export {default as https} from "./https";
export * as electron from "./electron";
export * as crypto from "./crypto";
export * as vm from "./vm";
// We can expose that without any issues.
export * as path from "path";
export * as net from "net"; // TODO: evaluate need and create wrapper
export * as os from "os";

14
preload/src/api/vm.js Normal file
View File

@ -0,0 +1,14 @@
import vm from "vm";
export function compileFunction(code, params = [], options = {}) {
try {
return vm.compileFunction(code, params, options);
}
catch (error) {
return {
name: error.name,
message: error.message,
stack: error.stack
};
}
}

15
preload/src/index.js Normal file
View File

@ -0,0 +1,15 @@
import {contextBridge} from "electron";
import newProcess from "./process";
import * as BdApi from "./api";
import init from "./init";
let hasInitialized = false;
contextBridge.exposeInMainWorld("BetterDiscord", BdApi);
contextBridge.exposeInMainWorld("process", newProcess);
contextBridge.exposeInMainWorld("BetterDiscordPreload", () => {
if (hasInitialized) return null;
hasInitialized = true;
return BdApi;
});
init();

22
preload/src/init.js Normal file
View File

@ -0,0 +1,22 @@
import {ipcRenderer as IPC} from "electron";
import * as IPCEvents from "common/constants/ipcevents";
export default function() {
// Load Discord's original preload
const preload = process.env.DISCORD_PRELOAD;
if (preload) {
// Restore original preload for future windows
IPC.send(IPCEvents.REGISTER_PRELOAD, preload);
// Run original preload
try {
const originalKill = process.kill;
process.kill = function() {};
__non_webpack_require__(preload);
process.kill = originalKill;
}
catch (e) {
// TODO bail out
}
}
}

3
preload/src/process.js Normal file
View File

@ -0,0 +1,3 @@
import cloneObject, {getKeys} from "common/clone";
export default cloneObject(process, {}, getKeys(process).filter(p => p !== "config"));

42
preload/webpack.config.js Normal file
View File

@ -0,0 +1,42 @@
const path = require("path");
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: "preload.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")`,
os: `require("os")`,
net: `require("net")`
},
resolve: {
extensions: [".js"],
alias: {
common: path.resolve(__dirname, "..", "common")
}
},
optimization: {
minimizer: [
new TerserPlugin({
terserOptions: {
compress: {drop_debugger: false},
keep_classnames: true
}
})
]
}
});

12637
renderer/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,5 +1,5 @@
{ {
"name": "betterdiscord-renderer", "name": "@betterdiscord/renderer",
"description": "Renderer portion of the BetterDiscord application.", "description": "Renderer portion of the BetterDiscord application.",
"private": true, "private": true,
"main": "src/index.js", "main": "src/index.js",
@ -24,16 +24,13 @@
"babel-plugin-module-resolver": "^4.1.0", "babel-plugin-module-resolver": "^4.1.0",
"circular-dependency-plugin": "^5.2.2", "circular-dependency-plugin": "^5.2.2",
"css-loader": "^6.5.1", "css-loader": "^6.5.1",
"eslint": "^8.8.0",
"eslint-plugin-react": "^7.28.0",
"postcss": "^8.4.5", "postcss": "^8.4.5",
"postcss-cli": "^9.1.0", "postcss-cli": "^9.1.0",
"postcss-csso": "^6.0.0", "postcss-csso": "^6.0.0",
"postcss-easy-import": "^4.0.0", "postcss-easy-import": "^4.0.0",
"postcss-loader": "^6.2.1", "postcss-loader": "^6.2.1",
"stylelint": "^14.3.0", "stylelint": "^14.3.0",
"stylelint-config-standard": "^24.0.0", "webpack": "^5.73.0",
"webpack": "^5.67.0", "stylelint-config-standard": "^24.0.0"
"webpack-cli": "^4.9.2"
} }
} }

View File

@ -6,8 +6,8 @@ export {default as PublicServers} from "./general/publicservers";
export {default as VoiceDisconnect} from "./general/voicedisconnect"; export {default as VoiceDisconnect} from "./general/voicedisconnect";
export {default as MediaKeys} from "./general/mediakeys"; export {default as MediaKeys} from "./general/mediakeys";
export {default as EmoteModule} from "./emotes/emotes"; // export {default as EmoteModule} from "./emotes/emotes";
export {default as EmoteMenu} from "./emotes/emotemenu"; // export {default as EmoteMenu} from "./emotes/emotemenu";
// export {default as EmoteAutocaps} from "./emotes/emoteautocaps"; // export {default as EmoteAutocaps} from "./emotes/emoteautocaps";
export {default as DevToolsListener} from "./developer/devtools"; export {default as DevToolsListener} from "./developer/devtools";

View File

@ -1,8 +1,42 @@
import Builtin from "../../structs/builtin"; import Builtin from "../../structs/builtin";
import {DiscordModules, WebpackModules, Strings, DOM} from "modules"; import {DiscordModules, WebpackModules, Strings, DOM, React} from "modules";
import PublicServersMenu from "../../ui/publicservers/menu"; import PublicServersMenu from "../../ui/publicservers/menu";
import Globe from "../../ui/icons/globe";
const LayerStack = WebpackModules.getByProps("pushLayer"); const LayerManager = {
pushLayer(component) {
DiscordModules.Dispatcher.dispatch({
type: "LAYER_PUSH",
component
});
},
popLayer() {
DiscordModules.Dispatcher.dispatch({
type: "LAYER_POP"
});
},
popAllLayers() {
DiscordModules.Dispatcher.dispatch({
type: "LAYER_POP_ALL"
});
}
};
class ErrorBoundary extends React.Component {
constructor(props) {
super(props);
this.state = {hasError: false};
}
componentDidCatch() {
this.setState({hasError: true});
}
render() {
if (this.state.hasError) return null;
return this.props.children;
}
}
export default new class PublicServers extends Builtin { export default new class PublicServers extends Builtin {
get name() {return "PublicServers";} get name() {return "PublicServers";}
@ -10,17 +44,37 @@ export default new class PublicServers extends Builtin {
get id() {return "publicServers";} get id() {return "publicServers";}
enabled() { enabled() {
this._appendButton(); // let target = null;
const ListNavigators = WebpackModules.getByProps("ListNavigatorProvider"); // WebpackModules.getModule((_, m) => {
this.after(ListNavigators, "ListNavigatorProvider", (_, __, returnValue) => { // if (m.exports?.toString().includes("privateChannelIds")) {
if (returnValue.props.value.id !== "guildsnav") return; // target = m.exports;
this._appendButton(); // }
}); // });
// if (!target || !target.Z) return;
// const PrivateChannelListComponents = WebpackModules.getByProps("LinkButton");
// this.after(target, "Z", (_, __, returnValue) => {
// const destination = returnValue?.props?.children?.props?.children;
// if (!destination || !Array.isArray(destination)) return;
// if (destination.find(b => b?.props?.children?.props?.id === "public-server-button")) return;
// destination.push(
// React.createElement(ErrorBoundary, null,
// React.createElement(PrivateChannelListComponents.LinkButton,
// {
// id: "public-server-button",
// onClick: () => this.openPublicServers(),
// text: "Public Servers",
// icon: () => React.createElement(Globe, {color: "currentColor"})
// }
// )
// )
// );
// });
} }
disabled() { disabled() {
this.unpatchAll(); // this.unpatchAll();
DOM.query("#bd-pub-li").remove(); // DOM.query("#bd-pub-li").remove();
} }
async _appendButton() { async _appendButton() {
@ -36,7 +90,7 @@ export default new class PublicServers extends Builtin {
} }
openPublicServers() { openPublicServers() {
LayerStack.pushLayer(() => DiscordModules.React.createElement(PublicServersMenu, {close: LayerStack.popLayer})); LayerManager.pushLayer(() => DiscordModules.React.createElement(PublicServersMenu, {close: LayerManager.popLayer}));
} }
get button() { get button() {

View File

@ -1,20 +1,23 @@
// fixed, improved, added, progress // fixed, improved, added, progress
export default { export default {
description: "Discord is _still_ making a lot of internal changes!", description: "BetterDiscord is alive! At least... _sorta_.",
changes: [ changes: [
{ {
title: "Changes", title: "Known Issues",
type: "improved", type: "improved",
items: [ items: [
"Plugin startup errors should be more descriptive for developers.", "**Many many plugins are either completely broken or missing functionality.** Please refer to the respective developers for ETAs.",
"The Twitch Emote system is completely broken, and there is no ETA on being fixed.",
"The Public Servers module is also broken with no ETA for a fix.",
] ]
}, },
{ {
title: "Fixes", title: "Important News!",
type: "fixed", type: "fixed",
items: [ items: [
"Fixed an issue where custom css crashed Discord.", "Due to recent and upcoming changes, BetterDiscord is going to go through a rewrite.",
"Fixed an issue where `waitForModule` returned a boolean instead of a module.", "There is no ETA or timeline for this rewrite.",
"We will continue to try and __maintain__ this version of BetterDiscord without adding new features."
] ]
} }
] ]

View File

@ -4,7 +4,7 @@ export default [
id: "general", id: "general",
collapsible: true, collapsible: true,
settings: [ settings: [
{type: "switch", id: "emotes", value: true}, {type: "switch", id: "emotes", value: true, disabled: true},
{type: "switch", id: "publicServers", value: true}, {type: "switch", id: "publicServers", value: true},
{type: "switch", id: "voiceDisconnect", value: false}, {type: "switch", id: "voiceDisconnect", value: false},
{type: "switch", id: "showToasts", value: true}, {type: "switch", id: "showToasts", value: true},

View File

@ -1,3 +1,4 @@
import require from "./polyfill"; // eslint-disable-line no-unused-vars
import secure from "./secure"; import secure from "./secure";
import patchModuleLoad from "./moduleloader"; import patchModuleLoad from "./moduleloader";
import LoadingIcon from "./loadingicon"; import LoadingIcon from "./loadingicon";
@ -8,6 +9,7 @@ import BdApi from "./modules/pluginapi";
secure(); secure();
patchModuleLoad(); patchModuleLoad();
window.BdApi = BdApi; window.BdApi = BdApi;
window.global = window;
// Add loading icon at the bottom right // Add loading icon at the bottom right
LoadingIcon.show(); LoadingIcon.show();

View File

@ -68,7 +68,9 @@ export default class AddonManager {
if (this.watcher) return Logger.err(this.name, `Already watching ${this.prefix} addons.`); if (this.watcher) return Logger.err(this.name, `Already watching ${this.prefix} addons.`);
Logger.log(this.name, `Starting to watch ${this.prefix} addons.`); Logger.log(this.name, `Starting to watch ${this.prefix} addons.`);
this.watcher = fs.watch(this.addonFolder, {persistent: false}, async (eventType, filename) => { this.watcher = fs.watch(this.addonFolder, {persistent: false}, async (eventType, filename) => {
// console.log("watcher", eventType, filename, !eventType || !filename, !filename.endsWith(this.extension));
if (!eventType || !filename) return; if (!eventType || !filename) return;
// console.log(eventType, filename)
const absolutePath = path.resolve(this.addonFolder, filename); const absolutePath = path.resolve(this.addonFolder, filename);
if (!filename.endsWith(this.extension)) { if (!filename.endsWith(this.extension)) {
@ -93,10 +95,11 @@ export default class AddonManager {
Logger.err(this.name, `Could not rename file: ${filename} ${newFilename}`, error); Logger.err(this.name, `Could not rename file: ${filename} ${newFilename}`, error);
} }
} }
// console.log("watcher", "before promise");
await new Promise(r => setTimeout(r, 100)); await new Promise(r => setTimeout(r, 100));
try { try {
const stats = fs.statSync(absolutePath); const stats = fs.statSync(absolutePath);
// console.log("watcher", stats);
if (!stats.isFile()) return; if (!stats.isFile()) return;
if (!stats || !stats.mtime || !stats.mtime.getTime()) return; if (!stats || !stats.mtime || !stats.mtime.getTime()) return;
if (typeof(stats.mtime.getTime()) !== "number") return; if (typeof(stats.mtime.getTime()) !== "number") return;
@ -106,7 +109,10 @@ export default class AddonManager {
if (eventType == "change") this.reloadAddon(filename, true); if (eventType == "change") this.reloadAddon(filename, true);
} }
catch (err) { catch (err) {
if (err.code !== "ENOENT") return; // window.watcherError = err;
// console.log("watcher", err);
// console.dir(err);
if (err.code !== "ENOENT" && !err?.message.startsWith("ENOENT")) return;
delete this.timeCache[filename]; delete this.timeCache[filename];
this.unloadAddon(filename, true); this.unloadAddon(filename, true);
} }
@ -207,6 +213,7 @@ export default class AddonManager {
unloadAddon(idOrFileOrAddon, shouldToast = true, isReload = false) { unloadAddon(idOrFileOrAddon, shouldToast = true, isReload = false) {
const addon = typeof(idOrFileOrAddon) == "string" ? this.addonList.find(c => c.id == idOrFileOrAddon || c.filename == idOrFileOrAddon) : idOrFileOrAddon; const addon = typeof(idOrFileOrAddon) == "string" ? this.addonList.find(c => c.id == idOrFileOrAddon || c.filename == idOrFileOrAddon) : idOrFileOrAddon;
// console.log("watcher", "unloadAddon", idOrFileOrAddon, addon);
if (!addon) return false; if (!addon) return false;
if (this.state[addon.id]) isReload ? this.stopAddon(addon) : this.disableAddon(addon); if (this.state[addon.id]) isReload ? this.stopAddon(addon) : this.disableAddon(addon);
@ -314,6 +321,7 @@ export default class AddonManager {
deleteAddon(idOrFileOrAddon) { deleteAddon(idOrFileOrAddon) {
const addon = typeof(idOrFileOrAddon) == "string" ? this.addonList.find(c => c.id == idOrFileOrAddon || c.filename == idOrFileOrAddon) : idOrFileOrAddon; const addon = typeof(idOrFileOrAddon) == "string" ? this.addonList.find(c => c.id == idOrFileOrAddon || c.filename == idOrFileOrAddon) : idOrFileOrAddon;
// console.log(path.resolve(this.addonFolder, addon.filename), fs.unlinkSync)
return fs.unlinkSync(path.resolve(this.addonFolder, addon.filename)); return fs.unlinkSync(path.resolve(this.addonFolder, addon.filename));
} }

View File

@ -8,8 +8,8 @@ import BDLogo from "../ui/icons/bdlogo";
import Logger from "common/logger"; import Logger from "common/logger";
const React = DiscordModules.React; const React = DiscordModules.React;
const Tooltip = WebpackModules.getByDisplayName("Tooltip"); const Tooltip = WebpackModules.getByPrototypes("renderTooltip");
const Anchor = WebpackModules.getByDisplayName("Anchor"); const Anchor = WebpackModules.getByProps("Link");
const Developers = [ const Developers = [
/* Zerebos#7790 */ /* Zerebos#7790 */
@ -21,7 +21,7 @@ const Developers = [
const DeveloperBadge = function DeveloperBadge({type, size = 16}) { const DeveloperBadge = function DeveloperBadge({type, size = 16}) {
return React.createElement(Tooltip, {color: "primary", position: "top", text: "BetterDiscord Developer"}, return React.createElement(Tooltip, {color: "primary", position: "top", text: "BetterDiscord Developer"},
props => React.createElement(Anchor, Object.assign({className: `bd-${type}-badge`, href: "https://github.com/BetterDiscord/BetterDiscord", title: "BetterDiscord", target: "_blank"}, props), props => React.createElement(Anchor.Link, Object.assign({className: `bd-${type}-badge`, href: "https://github.com/BetterDiscord/BetterDiscord", title: "BetterDiscord", target: "_blank"}, props),
React.createElement(BDLogo, {size, className: "bd-logo"}) React.createElement(BDLogo, {size, className: "bd-logo"})
) )
); );
@ -33,17 +33,14 @@ export default new class ComponentPatcher {
debug(...message) {return Logger.debug("ComponentPatcher", ...message);} debug(...message) {return Logger.debug("ComponentPatcher", ...message);}
initialize() { initialize() {
Utilities.suppressErrors(this.patchSocial.bind(this), "BD Social Patch")(); // Utilities.suppressErrors(this.patchSocial.bind(this), "BD Social Patch")();
Utilities.suppressErrors(this.patchGuildPills.bind(this), "BD Guild Pills Patch")(); // Utilities.suppressErrors(this.patchMemberList.bind(this), "BD Member List Patch")();
Utilities.suppressErrors(this.patchGuildListItems.bind(this), "BD Guild List Items Patch")(); // Utilities.suppressErrors(this.patchProfile.bind(this), "BD Profile Badges Patch")();
Utilities.suppressErrors(this.patchMessageHeader.bind(this), "BD Message Header Patch")();
Utilities.suppressErrors(this.patchMemberList.bind(this), "BD Member List Patch")();
Utilities.suppressErrors(this.patchProfile.bind(this), "BD Profile Badges Patch")();
} }
patchSocial() { patchSocial() {
if (this.socialPatch) return; if (this.socialPatch) return;
const TabBar = WebpackModules.getByDisplayName("TabBar"); const TabBar = WebpackModules.getByProps("Types", "Looks", "Header");
if (!TabBar) return; if (!TabBar) return;
this.socialPatch = Patcher.after("ComponentPatcher", TabBar.prototype, "render", (thisObject, args, returnValue) => { this.socialPatch = Patcher.after("ComponentPatcher", TabBar.prototype, "render", (thisObject, args, returnValue) => {
const children = returnValue.props.children; const children = returnValue.props.children;
@ -75,102 +72,6 @@ export default new class ComponentPatcher {
}; };
}); });
} }
patchGuildListItems() {
if (this.guildListItemsPatch) return;
const ListNavigators = WebpackModules.getByProps("ListNavigatorProvider");
const GuildComponent = WebpackModules.find(m => m.type && m.type.toString().includes("guildNode") && m.type.toString().includes("treeitem"));
if (!GuildComponent || typeof(GuildComponent.type) !== "function") return this.warn("Failed to get Guild component.");
if (!ListNavigators || typeof(ListNavigators.ListNavigatorProvider) !== "function") return this.warn("Failed to get ListNavigatorProvider component.");
this.guildListItemsPatch = Patcher.after("ComponentPatcher", GuildComponent, "type", (_, [props], returnValue) => {
if (!returnValue || !returnValue.props) return;
try {
returnValue.props.className += " bd-guild";
if (props.unread) returnValue.props.className += " bd-unread";
if (props.selected) returnValue.props.className += " bd-selected";
if (props.mediaState.audio) returnValue.props.className += " bd-audio";
if (props.mediaState.video) returnValue.props.className += " bd-video";
if (props.badge) returnValue.props.className += " bd-badge";
if (props.animatable) returnValue.props.className += " bd-animatable";
if (props.unavailable) returnValue.props.className += " bd-unavailable";
if (props.mediaState.screenshare) returnValue.props.className += " bd-screenshare";
if (props.mediaState.liveStage) returnValue.props.className += " bd-live-stage";
if (props.muted) returnValue.props.className += " bd-muted";
}
catch (err) {
Logger.error("ComponentPatcher:Guilds", `Error inside BDGuild:`, err);
this.guildListItemsPatch();
}
});
const {useState} = DiscordModules.React;
function useForceUpdate() {
const [, setValue] = useState(false);
return () => setValue(v => !v); // update the state to force render
}
let hasForced = false;
this.cancelForceUpdate = Patcher.after("ComponentPatcher", ListNavigators, "ListNavigatorProvider", (_, __, returnValue) => {
if (returnValue.props.value.id !== "guildsnav") return;
const originalParent = Utilities.findInTree(returnValue, m => m?.props?.className, {walkable: ["children", "props"]});
if (!originalParent) return;
const original = originalParent.type;
originalParent.type = e => {
const forceUpdate = useForceUpdate();
if (!hasForced) {
hasForced = true;
setTimeout(() => {
forceUpdate();
this.cancelForceUpdate();
}, 1);
}
return Reflect.apply(original, null, [e]);
};
});
}
patchGuildPills() {
if (this.guildPillPatch) return;
const guildPill = WebpackModules.find(m => m?.default?.displayName === "AnimatedHalfPill");
if (!guildPill) return;
this.guildPillPatch = Patcher.after("ComponentPatcher", guildPill, "default", (_, args, returnValue) => {
const props = args[0];
if (props.unread) returnValue.props.className += " bd-unread";
if (props.selected) returnValue.props.className += " bd-selected";
if (props.hovered) returnValue.props.className += " bd-hovered";
return returnValue;
});
}
patchMessageHeader() {
if (this.messageHeaderPatch) return;
// const MessageTimestamp = WebpackModules.getModule(m => m?.default?.toString().indexOf("showTimestampOnHover") > -1);
// this.messageHeaderPatch = Patcher.after("ComponentPatcher", MessageTimestamp, "default", (_, [{message}], returnValue) => {
// const userId = Utilities.getNestedProp(message, "author.id");
// if (Developers.indexOf(userId) < 0) return;
// if (!returnValue?.type) return;
// const orig = returnValue.type;
// returnValue.type = function() {
// const retVal = Reflect.apply(orig, this, arguments);
// const children = Utilities.getNestedProp(retVal, "props.children.1.props.children");
// if (!Array.isArray(children)) return;
// children.splice(3, 0,
// React.createElement(DeveloperBadge, {
// type: "chat"
// })
// );
// return retVal;
// };
// });
}
async patchMemberList() { async patchMemberList() {
if (this.memberListPatch) return; if (this.memberListPatch) return;
@ -198,7 +99,7 @@ export default new class ComponentPatcher {
patchProfile() { patchProfile() {
if (this.profilePatch) return; if (this.profilePatch) return;
const UserProfileBadgeLists = WebpackModules.getModule(m => m?.default?.displayName === "UserProfileBadgeList", {first: false}); const UserProfileBadgeLists = WebpackModules.getModule(m => m?.toString()?.includes("PROFILE_USER_BADGES"), {first: false});
for (const UserProfileBadgeList of UserProfileBadgeLists) { for (const UserProfileBadgeList of UserProfileBadgeLists) {
this.profilePatch = Patcher.after("ComponentPatcher", UserProfileBadgeList, "default", (_, [{user}], res) => { this.profilePatch = Patcher.after("ComponentPatcher", UserProfileBadgeList, "default", (_, [{user}], res) => {
if (Developers.indexOf(user?.id) < 0) return; if (Developers.indexOf(user?.id) < 0) return;

View File

@ -3,6 +3,7 @@ import LocaleManager from "./localemanager";
import Logger from "common/logger"; import Logger from "common/logger";
import {Config, Changelog} from "data"; import {Config, Changelog} from "data";
import WebpackModules from "./webpackmodules";
import DOMManager from "./dommanager"; import DOMManager from "./dommanager";
import PluginManager from "./pluginmanager"; import PluginManager from "./pluginmanager";
import ThemeManager from "./thememanager"; import ThemeManager from "./thememanager";
@ -18,7 +19,6 @@ import IPC from "./ipc";
import LoadingIcon from "../loadingicon"; import LoadingIcon from "../loadingicon";
import Styles from "../styles/index.css"; import Styles from "../styles/index.css";
import Editor from "./editor"; import Editor from "./editor";
import {WebpackModules} from "modules";
export default new class Core { export default new class Core {
async startup() { async startup() {
@ -48,6 +48,7 @@ export default new class Core {
Logger.log("Startup", "Initializing Settings"); Logger.log("Startup", "Initializing Settings");
Settings.initialize(); Settings.initialize();
// SettingsRenderer.patchSections();
Logger.log("Startup", "Initializing DOMManager"); Logger.log("Startup", "Initializing DOMManager");
DOMManager.initialize(); DOMManager.initialize();
@ -68,6 +69,7 @@ export default new class Core {
for (const module in Builtins) { for (const module in Builtins) {
Builtins[module].initialize(); Builtins[module].initialize();
} }
this.polyfillWebpack(); this.polyfillWebpack();
Logger.log("Startup", "Loading Plugins"); Logger.log("Startup", "Loading Plugins");
// const pluginErrors = []; // const pluginErrors = [];
@ -86,9 +88,18 @@ export default new class Core {
const previousVersion = DataStore.getBDData("version"); const previousVersion = DataStore.getBDData("version");
if (Config.version > previousVersion) { if (Config.version > previousVersion) {
Modals.showChangelogModal(Changelog); // Modals.showChangelogModal(Changelog);
const md = [Changelog.description];
for (const type of Changelog.changes) {
md.push(`**${type.title}**`);
for (const entry of type.items) {
md.push(` - ${entry}`);
}
}
Modals.showConfirmationModal(`BetterDiscord v${Config.version}`, md, {cancelText: ""});
DataStore.setBDData("version", Config.version); DataStore.setBDData("version", Config.version);
} }
// SettingsRenderer.patchSections();
} }
polyfillWebpack() { polyfillWebpack() {

View File

@ -47,7 +47,7 @@ export default Utilities.memoizeObject({
get MentionStore() {return WebpackModules.getByProps("getMentions");}, get MentionStore() {return WebpackModules.getByProps("getMentions");},
/* User Stores and Utils */ /* User Stores and Utils */
get UserStore() {return WebpackModules.getByProps("getCurrentUser");}, get UserStore() {return WebpackModules.getByProps("getCurrentUser", "getUser");},
get UserStatusStore() {return WebpackModules.getByProps("getStatus", "getState");}, get UserStatusStore() {return WebpackModules.getByProps("getStatus", "getState");},
get UserTypingStore() {return WebpackModules.getByProps("isTyping");}, get UserTypingStore() {return WebpackModules.getByProps("isTyping");},
get UserActivityStore() {return WebpackModules.getByProps("getActivity");}, get UserActivityStore() {return WebpackModules.getByProps("getActivity");},
@ -133,14 +133,14 @@ export default Utilities.memoizeObject({
/* Electron & Other Internals with Utils*/ /* Electron & Other Internals with Utils*/
get ElectronModule() {return WebpackModules.getByProps("setBadge");}, get ElectronModule() {return WebpackModules.getByProps("setBadge");},
get Dispatcher() {return WebpackModules.getByProps("dispatch", "subscribe");}, get Dispatcher() {return WebpackModules.getByProps("dispatch", "subscribe", "register");},
get PathUtils() {return WebpackModules.getByProps("hasBasename");}, get PathUtils() {return WebpackModules.getByProps("hasBasename");},
get NotificationModule() {return WebpackModules.getByProps("showNotification");}, get NotificationModule() {return WebpackModules.getByProps("showNotification");},
get RouterModule() {return WebpackModules.getByProps("Router");}, get RouterModule() {return WebpackModules.getByProps("Router");},
get APIModule() {return WebpackModules.getByProps("getAPIBaseURL");}, get APIModule() {return WebpackModules.getByProps("getAPIBaseURL");},
get AnalyticEvents() {return WebpackModules.getByProps("AnalyticEventConfigs");}, get AnalyticEvents() {return WebpackModules.getByProps("AnalyticEventConfigs");},
get KeyGenerator() {return WebpackModules.getByRegex(/"binary"/);}, get KeyGenerator() {return WebpackModules.getByRegex(/"binary"/);},
get Buffers() {return WebpackModules.getByProps("Buffer", "kMaxLength");}, get Buffers() {return WebpackModules.getByProps("INSPECT_MAX_BYTES", "kMaxLength");},
get DeviceStore() {return WebpackModules.getByProps("getDevices");}, get DeviceStore() {return WebpackModules.getByProps("getDevices");},
get SoftwareInfo() {return WebpackModules.getByProps("os");}, get SoftwareInfo() {return WebpackModules.getByProps("os");},
get CurrentContext() {return WebpackModules.getByProps("setTagsContext");}, get CurrentContext() {return WebpackModules.getByProps("setTagsContext");},

View File

@ -16,10 +16,7 @@ export default new class LocaleManager {
initialize() { initialize() {
this.setLocale(this.discordLocale); this.setLocale(this.discordLocale);
Dispatcher.subscribe("USER_SETTINGS_UPDATE", ({settings}) => { Dispatcher.subscribe("USER_SETTINGS_UPDATE", (newLocale) => this.setLocale(newLocale));
const newLocale = settings.locale;
if (newLocale && newLocale != this.locale) this.setLocale(newLocale);
});
} }
setLocale(newLocale) { setLocale(newLocale) {

View File

@ -117,7 +117,7 @@ export default new class PluginManager extends AddonManager {
try { try {
const module = {filename, exports: {}}; const module = {filename, exports: {}};
// Test if the code is valid gracefully // Test if the code is valid gracefully
vm.compileFunction(addon.fileContent, ["require", "module", "exports", "__filename", "__dirname"]); vm.compileFunction(addon.fileContent, ["require", "module", "exports", "__filename", "__dirname"], {filename: path.basename(filename)});
addon.fileContent += normalizeExports(addon.exports || addon.name); addon.fileContent += normalizeExports(addon.exports || addon.name);
addon.fileContent += `\n//# sourceURL=betterdiscord://plugins/${addon.filename}`; addon.fileContent += `\n//# sourceURL=betterdiscord://plugins/${addon.filename}`;
const wrappedPlugin = new Function(["require", "module", "exports", "__filename", "__dirname"], addon.fileContent); // eslint-disable-line no-new-func const wrappedPlugin = new Function(["require", "module", "exports", "__filename", "__dirname"], addon.fileContent); // eslint-disable-line no-new-func

View File

@ -113,24 +113,6 @@ export class Filters {
} }
} }
const protect = theModule => {
if (theModule.remove && theModule.set && theModule.clear && theModule.get && !theModule.sort) return null;
if (!theModule.getToken && !theModule.getEmail && !theModule.showToken) return theModule;
const proxy = new Proxy(theModule, {
getOwnPropertyDescriptor: function(obj, prop) {
if (prop === "getToken" || prop === "getEmail" || prop === "showToken") return undefined;
return Object.getOwnPropertyDescriptor(obj, prop);
},
get: function(obj, func) {
if (func == "getToken") return () => "mfa.XCnbKzo0CLIqdJzBnL0D8PfDruqkJNHjwHXtr39UU3F8hHx43jojISyi5jdjO52e9_e9MjmafZFFpc-seOMa";
if (func == "getEmail") return () => "puppet11112@gmail.com";
if (func == "showToken") return () => true;
// if (func == "__proto__") return proxy;
return obj[func];
}
});
return proxy;
};
const hasThrown = new WeakSet(); const hasThrown = new WeakSet();
@ -152,6 +134,10 @@ export default class WebpackModules {
const {first = true, defaultExport = true} = options; const {first = true, defaultExport = true} = options;
const wrappedFilter = (exports, module, moduleId) => { const wrappedFilter = (exports, module, moduleId) => {
try { try {
if (exports?.default?.remove && exports?.default?.set && exports?.default?.clear && exports?.default?.get && !exports?.default?.sort) return false;
if (exports.remove && exports.set && exports.clear && exports.get && !exports.sort) return false;
if (exports?.default?.getToken || exports?.default?.getEmail || exports?.default?.showToken) return false;
if (exports.getToken || exports.getEmail || exports.showToken) return false;
return filter(exports, module, moduleId); return filter(exports, module, moduleId);
} }
catch (err) { catch (err) {
@ -160,6 +146,7 @@ export default class WebpackModules {
return false; return false;
} }
}; };
const modules = this.getAllModules(); const modules = this.getAllModules();
const rm = []; const rm = [];
const indices = Object.keys(modules); const indices = Object.keys(modules);
@ -168,14 +155,42 @@ export default class WebpackModules {
if (!modules.hasOwnProperty(index)) continue; if (!modules.hasOwnProperty(index)) continue;
const module = modules[index]; const module = modules[index];
const {exports} = module; const {exports} = module;
if (exports === window) continue;
let foundModule = null; let foundModule = null;
if (!exports) continue; if (typeof(exports) === "object") {
if (exports.__esModule && exports.default && wrappedFilter(exports.default, module, index)) foundModule = defaultExport ? exports.default : exports; const wrappers = Object.getOwnPropertyDescriptors(exports);
if (wrappedFilter(exports, module, index)) foundModule = exports; const getters = Object.keys(wrappers).filter(k => wrappers[k].get);
if (!foundModule) continue; if (getters.length) {
if (first) return protect(foundModule); for (const getter of getters) {
rm.push(protect(foundModule)); const wrappedExport = exports[getter];
if (!wrappedExport) continue;
if (wrappedExport.__esModule && wrappedExport.default && wrappedFilter(wrappedExport.default, module, index)) foundModule = defaultExport ? wrappedExport.default : wrappedExport;
if (wrappedFilter(wrappedExport, module, index)) foundModule = wrappedExport;
if (!foundModule) continue;
if (first) return foundModule;
rm.push(foundModule);
}
}
else {
if (!exports) continue;
if (exports.__esModule && exports.default && wrappedFilter(exports.default, module, index)) foundModule = defaultExport ? exports.default : exports;
if (wrappedFilter(exports, module, index)) foundModule = exports;
if (!foundModule) continue;
if (first) return foundModule;
rm.push(foundModule);
}
}
else {
if (!exports) continue;
if (exports.__esModule && exports.default && wrappedFilter(exports.default, module, index)) foundModule = defaultExport ? exports.default : exports;
if (wrappedFilter(exports, module, index)) foundModule = exports;
if (!foundModule) continue;
if (first) return foundModule;
rm.push(foundModule);
}
} }
return first || rm.length == 0 ? undefined : rm; return first || rm.length == 0 ? undefined : rm;
@ -219,11 +234,36 @@ export default class WebpackModules {
}; };
let foundModule = null; let foundModule = null;
if (exports.__esModule && exports.default && wrappedFilter(exports.default, module, index)) foundModule = defaultExport ? exports.default : exports; if (typeof(exports) === "object") {
if (wrappedFilter(exports, module, index)) foundModule = exports; const wrappers = Object.getOwnPropertyDescriptors(exports);
if (!foundModule) continue; const getters = Object.keys(wrappers).filter(k => wrappers[k].get);
if (first) returnedModules[q] = protect(foundModule); if (getters.length) {
else returnedModules[q].push(protect(foundModule)); for (const getter of getters) {
const wrappedExport = exports[getter];
if (!wrappedExport) continue;
if (wrappedExport.__esModule && wrappedExport.default && wrappedFilter(wrappedExport.default, module, index)) foundModule = defaultExport ? wrappedExport.default : wrappedExport;
if (wrappedFilter(wrappedExport, module, index)) foundModule = wrappedExport;
if (!foundModule) continue;
if (first) returnedModules[q] = foundModule;
else returnedModules[q].push(foundModule);
}
}
else {
if (!exports) continue;
if (exports.__esModule && exports.default && wrappedFilter(exports.default, module, index)) foundModule = defaultExport ? exports.default : exports;
if (wrappedFilter(exports, module, index)) foundModule = exports;
if (!foundModule) continue;
if (first) returnedModules[q] = foundModule;
else returnedModules[q].push(foundModule);
}
}
else {
if (exports.__esModule && exports.default && wrappedFilter(exports.default, module, index)) foundModule = defaultExport ? exports.default : exports;
if (wrappedFilter(exports, module, index)) foundModule = exports;
if (!foundModule) continue;
if (first) returnedModules[q] = foundModule;
else returnedModules[q].push(foundModule);
}
} }
} }
@ -341,12 +381,30 @@ export default class WebpackModules {
if (!exports) return; if (!exports) return;
let foundModule = null; let foundModule = null;
if (exports.__esModule && exports.default && wrappedFilter(exports.default)) foundModule = defaultExport ? exports.default : exports; if (typeof(exports) === "object") {
if (wrappedFilter(exports)) foundModule = exports; const wrappers = Object.getOwnPropertyDescriptors(exports);
if (!foundModule) return; const getters = Object.keys(wrappers).filter(k => wrappers[k].get);
if (getters.length) {
for (const getter of getters) {
const wrappedExport = exports[getter];
if (!wrappedExport) continue;
if (wrappedExport.__esModule && wrappedExport.default && wrappedFilter(wrappedExport.default)) foundModule = defaultExport ? wrappedExport.default : wrappedExport;
if (wrappedFilter(wrappedExport)) foundModule = wrappedExport;
}
}
else {
if (exports.__esModule && exports.default && wrappedFilter(exports.default)) foundModule = defaultExport ? exports.default : exports;
if (wrappedFilter(exports)) foundModule = exports;
}
}
else {
if (exports.__esModule && exports.default && wrappedFilter(exports.default)) foundModule = defaultExport ? exports.default : exports;
if (wrappedFilter(exports)) foundModule = exports;
}
if (!foundModule) return;
cancel(); cancel();
resolve(protect(foundModule)); resolve(foundModule);
}; };
this.addListener(listener); this.addListener(listener);

View File

@ -0,0 +1,17 @@
import WebpackModules from "../modules/webpackmodules";
Object.defineProperty(window, "Buffer", {
get() {return Buffer.getBuffer().Buffer;},
configurable: true,
enumerable: false
});
export default class Buffer {
static getBuffer() {
if (this.cached) return this.cached;
this.cached = WebpackModules.getByProps("INSPECT_MAX_BYTES");
return this.cached;
}
}

View File

@ -0,0 +1,9 @@
import Remote from "./remote";
export default {
...Remote.crypto,
// Wrap it in Buffer
randomBytes(length) {
return Buffer.from(Remote.crypto.randomBytes(length));
}
};

172
renderer/src/polyfill/fs.js Normal file
View File

@ -0,0 +1,172 @@
import Remote from "./remote";
export const readFileSync = function (path, options = "utf8") {
return Remote.filesystem.readFile(path, options);
};
export const readFile = function (path, options = "utf8", callback) {
try {
const contents = Remote.filesystem.readFile(path, options);
callback(null, contents);
}
catch (error) {
callback(error, null);
}
};
export const writeFile = function (path, data, options = "utf8", callback) {
if (typeof(options) === "function") {
callback = options;
if (!["object", "string"].includes(typeof(options))) options = undefined;
}
try {
Remote.filesystem.writeFile(path, data, options);
callback(null);
}
catch (error) {
callback(error);
}
};
export const writeFileSync = function (path, data, options) {
Remote.filesystem.writeFile(path, data, options);
};
export const readdir = function (path, options, callback) {
try {
const result = Remote.filesystem.readDirectory(path, options);
callback(null, result);
}
catch (error) {
callback(error, null);
}
};
export const readdirSync = function (path, options) {
return Remote.filesystem.readDirectory(path, options);
};
export const mkdir = function (path, options, callback) {
try {
const result = Remote.filesystem.createDirectory(path, options);
callback(null, result);
}
catch (error) {
callback(error, null);
}
};
export const mkdirSync = function (path, options) {
Remote.filesystem.createDirectory(path, options);
};
export const rmdir = function (path, options, callback) {
try {
const result = Remote.filesystem.deleteDirectory(path, options);
callback(null, result);
}
catch (error) {
callback(error, null);
}
};
export const rmdirSync = function (path, options) {
Remote.filesystem.deleteDirectory(path, options);
};
export const exists = function (path, options, callback) {
try {
const result = Remote.filesystem.exists(path, options);
callback(null, result);
}
catch (error) {
callback(error, null);
}
};
export const existsSync = function (path, options) {
return Remote.filesystem.exists(path, options);
};
export const stat = function (path, options, callback) {
try {
const result = Remote.filesystem.getStats(path, options);
callback(null, result);
}
catch (error) {
callback(error);
}
};
export const statSync = function (path, options) {
return Remote.filesystem.getStats(path, options);
};
export const lstat = stat;
export const lstatSync = statSync;
export const rename = function (oldPath, newPath, options, callback) {
try {
const result = Remote.filesystem.rename(oldPath, newPath, options);
callback(null, result);
}
catch (error) {
callback(error, null);
}
};
export const renameSync = function (oldPath, newPath, options) {
return Remote.filesystem.renameSync(oldPath, newPath, options);
};
export const realpath = function (path, options, callback) {
try {
const result = Remote.filesystem.getStats(path, options);
callback(null, result);
}
catch (error) {
callback(error, null);
}
};
export const realpathSync = function (path, options) {
return Remote.filesystem.getRealPath(path, options);
};
export const watch = (path, options, callback) => {
return Remote.filesystem.watch(path, options, callback);
};
export const createWriteStream = (path, options) => {
return Remote.filesystem.createWriteStream(path, options);
};
export const unlinkSync = (path) => Remote.filesystem.unlinkSync(path);
export const unlink = (path) => Remote.filesystem.unlinkSync(path);
export default {
readFile,
exists,
existsSync,
lstat,
lstatSync,
mkdir,
mkdirSync,
readFileSync,
readdir,
readdirSync,
realpath,
realpathSync,
rename,
renameSync,
rmdir,
rmdirSync,
unlink,
unlinkSync,
watch,
writeFile,
writeFileSync,
createWriteStream
};

View File

@ -0,0 +1,23 @@
import EventEmitter from "common/events";
import Remote from "./remote";
export function get(url, options = {}, callback) {
if (typeof(options) === "function") {
callback = options;
options = null;
}
const emitter = new EventEmitter();
callback(emitter);
Remote.https.get(url, options, (error, res, body) => {
if (error) return emitter.emit("error", error);
emitter.emit("data", body);
emitter.emit("end", res);
});
return emitter;
}
export default {get};

View File

@ -0,0 +1,45 @@
import Module from "./module";
import * as vm from "./vm";
import * as fs from "./fs";
import request from "./request";
import EventEmitter from "common/events";
import * as https from "./https";
import Buffer from "./buffer";
import crypto from "./crypto";
import Remote from "./remote";
const originalFs = Object.assign({}, fs);
originalFs.writeFileSync = (path, data, options) => fs.writeFileSync(path, data, Object.assign({}, options, {originalFs: true}));
originalFs.writeFile = (path, data, options) => fs.writeFile(path, data, Object.assign({}, options, {originalFs: true}));
export const createRequire = function (path) {
return mod => {
switch (mod) {
case "request": return request;
case "https": return https;
case "original-fs": return originalFs;
case "fs": return fs;
case "path": return Remote.path;
case "events": return EventEmitter;
case "electron": return Remote.electron;
case "process": return window.process;
case "vm": return vm;
case "module": return Module;
case "buffer": return Buffer.getBuffer();
case "crypto": return crypto;
default:
return Module._load(mod, path, createRequire);
}
};
};
const require = window.require = createRequire(".");
require.cache = {};
require.resolve = (path) => {
for (const key of Object.keys(require.cache)) {
if (key.startsWith(path)) return require.cache[key];
}
};
export default require;

View File

@ -0,0 +1,103 @@
import Logger from "common/logger";
import {compileFunction} from "./vm";
import Remote from "./remote";
import fs from "./fs";
const path = Remote.path;
export const RequireExtensions = {
".js": (module, filename) => {
const fileContent = Remote.filesystem.readFile(filename, "utf8");
module.fileContent = fileContent;
module._compile(fileContent);
return module.exports;
},
".json": (module, filename) => {
const fileContent = Remote.filesystem.readFile(filename, "utf8");
module.fileContent = fileContent;
module.exports = JSON.parse(fileContent);
return module.exports;
}
};
export default class Module {
static resolveMainFile(mod, basePath) {
const parent = path.extname(basePath) ? path.dirname(basePath) : basePath;
const files = Remote.filesystem.readDirectory(parent);
if (!Array.isArray(files)) return null;
for (const file of files) {
const ext = path.extname(file);
if (file === "package.json") {
const pkg = require(path.resolve(parent, file));
if (!Reflect.has(pkg, "main")) continue;
return path.resolve(parent, pkg.main);
}
if (path.slice(0, -ext.length) == "index" && RequireExtensions[ext]) return mod;
}
}
static getExtension(mod) {
return path.extname(mod) || Reflect.ownKeys(RequireExtensions).find(e => Remote.filesystem.exists(mod + e));
}
static getFilePath(basePath, mod) {
if (!path.isAbsolute(mod)) mod = path.resolve(basePath, mod);
const defaultExtension = path.extname(mod);
if (!defaultExtension) {
const ext = Reflect.ownKeys(RequireExtensions).find(e => Remote.filesystem.exists(mod + e));
if (ext) {
mod = mod + ext;
}
}
return fs.realpathSync(mod);
}
static _load(mod, basePath, createRequire) {
const originalReq = mod;
if (!path.isAbsolute(mod)) mod = path.resolve(basePath, mod);
const filePath = this.getFilePath(basePath, mod);
if (!Remote.filesystem.exists(filePath)) throw new Error(`Cannot find module ${mod}`);
if (window.require.cache[filePath]) return window.require.cache[filePath].exports;
const stats = Remote.filesystem.getStats(filePath);
if (stats.isDirectory()) mod = this.resolveMainFile(mod, basePath);
const ext = this.getExtension(filePath);
const loader = RequireExtensions[ext];
if (!loader) throw new Error(`Cannot find module ${originalReq}`);
const module = window.require.cache[mod] = new Module(filePath, internalModule, createRequire(mod));
loader(module, filePath);
return module.exports;
}
static get Module() {return Module;}
static get createRequire() {return Logger.warn("ContextModule", "Module.createRequire not implemented yet.");}
static get _extensions() {return RequireExtensions;}
constructor(id, parent, require) {
this.id = id;
this.path = Remote.path.dirname(id);
this.exports = {};
this.parent = parent;
this.filename = id;
this.loaded = false;
this.children = [];
this.require = require;
if (parent) parent.children.push(this);
}
_compile(code) {
const wrapped = compileFunction(code, ["require", "module", "exports", "__filename", "__dirname", "global"], this.filename);
wrapped(this.require, this, this.exports, this.filename, this.path, window);
}
}
const internalModule = new Module(".", null);

View File

@ -0,0 +1,3 @@
/** @type {import("../../../preload/src/api/index")} */
const RemoteAPI = window.BetterDiscordPreload(); // eslint-disable-line new-cap
export default RemoteAPI;

View File

@ -0,0 +1,68 @@
import Remote from "./remote";
const methods = ["get", "put", "post", "delete", "head"];
const aliases = {del: "delete"};
function parseArguments() {
let url, options, callback;
for (const arg of arguments) {
switch (typeof arg) {
case (arg !== null && "object"):
options = arg;
if ("url" in options) {
url = options.url;
}
break;
case (!url && "string"):
url = arg;
break;
case (!callback && "function"):
callback = arg;
break;
}
}
return {url, options, callback};
}
function validOptions(url, callback) {
return typeof url === "string" && typeof callback === "function";
}
function fixBuffer(options, callback) {
return (error, res, body) => {
if ("Content-Type" in Object(options.headers) && options.headers["Content-Type"] !== "text/plain") {
body = Buffer.from(body);
}
else {
body = Buffer.from(body).toString();
}
callback(error, res, body);
};
}
export default function request() {
const {url, options = {}, callback} = parseArguments.apply(this, arguments);
if (!validOptions(url, callback)) return null;
if ("method" in options && methods.indexOf(options.method.toLowerCase()) >= 0) {
return Remote.https[options.method](url, options, fixBuffer(options, callback));
}
return Remote.https.request(url, options, fixBuffer(options, callback));
}
Object.assign(request, Object.fromEntries(
methods.concat(Object.keys(aliases)).map(method => [method, function () {
const {url, options = {}, callback} = parseArguments.apply(this, arguments);
if (!validOptions(url, callback)) return null;
return Remote.https[aliases[method] || method](url, options, fixBuffer(options, callback));
}])
));

View File

@ -0,0 +1,9 @@
import Remote from "./remote";
export const compileFunction = function(code, params = [], options = {}) {
const returned = Remote.vm.compileFunction(code, params, options);
if (typeof(returned) === "function") return returned;
const syntaxError = new SyntaxError(returned.message);
syntaxError.stack = returned.stack;
throw syntaxError;
};

View File

@ -2,7 +2,7 @@ import {React, WebpackModules, DiscordModules, Settings} from "modules";
import Checkbox from "./checkbox"; import Checkbox from "./checkbox";
const Tooltip = WebpackModules.getByDisplayName("Tooltip"); const Tooltip = WebpackModules.getByPrototypes("renderTooltip");
const ThemeStore = DiscordModules.ThemeStore; const ThemeStore = DiscordModules.ThemeStore;
const languages = ["abap", "abc", "actionscript", "ada", "apache_conf", "asciidoc", "assembly_x86", "autohotkey", "batchfile", "bro", "c_cpp", "c9search", "cirru", "clojure", "cobol", "coffee", "coldfusion", "csharp", "csound_document", "csound_orchestra", "csound_score", "css", "curly", "d", "dart", "diff", "dockerfile", "dot", "drools", "dummy", "dummysyntax", "eiffel", "ejs", "elixir", "elm", "erlang", "forth", "fortran", "ftl", "gcode", "gherkin", "gitignore", "glsl", "gobstones", "golang", "graphqlschema", "groovy", "haml", "handlebars", "haskell", "haskell_cabal", "haxe", "hjson", "html", "html_elixir", "html_ruby", "ini", "io", "jack", "jade", "java", "javascript", "json", "jsoniq", "jsp", "jssm", "jsx", "julia", "kotlin", "latex", "less", "liquid", "lisp", "livescript", "logiql", "lsl", "lua", "luapage", "lucene", "makefile", "markdown", "mask", "matlab", "maze", "mel", "mushcode", "mysql", "nix", "nsis", "objectivec", "ocaml", "pascal", "perl", "pgsql", "php", "pig", "powershell", "praat", "prolog", "properties", "protobuf", "python", "r", "razor", "rdoc", "red", "rhtml", "rst", "ruby", "rust", "sass", "scad", "scala", "scheme", "scss", "sh", "sjs", "smarty", "snippets", "soy_template", "space", "sql", "sqlserver", "stylus", "svg", "swift", "tcl", "tex", "text", "textile", "toml", "tsx", "twig", "typescript", "vala", "vbscript", "velocity", "verilog", "vhdl", "wollok", "xml", "xquery", "yaml", "django"]; const languages = ["abap", "abc", "actionscript", "ada", "apache_conf", "asciidoc", "assembly_x86", "autohotkey", "batchfile", "bro", "c_cpp", "c9search", "cirru", "clojure", "cobol", "coffee", "coldfusion", "csharp", "csound_document", "csound_orchestra", "csound_score", "css", "curly", "d", "dart", "diff", "dockerfile", "dot", "drools", "dummy", "dummysyntax", "eiffel", "ejs", "elixir", "elm", "erlang", "forth", "fortran", "ftl", "gcode", "gherkin", "gitignore", "glsl", "gobstones", "golang", "graphqlschema", "groovy", "haml", "handlebars", "haskell", "haskell_cabal", "haxe", "hjson", "html", "html_elixir", "html_ruby", "ini", "io", "jack", "jade", "java", "javascript", "json", "jsoniq", "jsp", "jssm", "jsx", "julia", "kotlin", "latex", "less", "liquid", "lisp", "livescript", "logiql", "lsl", "lua", "luapage", "lucene", "makefile", "markdown", "mask", "matlab", "maze", "mel", "mushcode", "mysql", "nix", "nsis", "objectivec", "ocaml", "pascal", "perl", "pgsql", "php", "pig", "powershell", "praat", "prolog", "properties", "protobuf", "python", "r", "razor", "rdoc", "red", "rhtml", "rst", "ruby", "rust", "sass", "scad", "scala", "scheme", "scss", "sh", "sjs", "smarty", "snippets", "soy_template", "space", "sql", "sqlserver", "stylus", "svg", "swift", "tcl", "tex", "text", "textile", "toml", "tsx", "twig", "typescript", "vala", "vbscript", "velocity", "verilog", "vhdl", "wollok", "xml", "xquery", "yaml", "django"];

View File

@ -1,6 +1,6 @@
import {Settings, React, WebpackModules, Events, Strings} from "modules"; import {Settings, React, WebpackModules, Events, Strings} from "modules";
const TooltipWrapper = WebpackModules.getByDisplayName("Tooltip"); const TooltipWrapper = WebpackModules.getByPrototypes("renderTooltip");
export default class BDEmote extends React.Component { export default class BDEmote extends React.Component {
constructor(props) { constructor(props) {

View File

@ -2,9 +2,9 @@ import {React, WebpackModules} from "modules";
import EmoteModule from "../builtins/emotes/emotes"; import EmoteModule from "../builtins/emotes/emotes";
const ContextMenuActions = WebpackModules.getByProps("openContextMenu"); const ContextMenuActions = WebpackModules.getByProps("openContextMenu");
const {MenuItem, MenuGroup} = WebpackModules.find(m => m.MenuRadioItem && !m.default); const {MenuItem, MenuGroup} = WebpackModules.find(m => m.MenuRadioItem && !m.default) ?? {MenuItem: () => null, MenuGroup: () => null};
const ContextMenu = WebpackModules.getByProps("default", "MenuStyle").default; const ContextMenu = WebpackModules.getByProps("default", "MenuStyle")?.default;
const {ComponentDispatch} = WebpackModules.getByProps("ComponentDispatch"); const {ComponentDispatch} = WebpackModules.getByProps("ComponentDispatch") ?? {ComponentDispatch: () => null};
export default class EmoteIcon extends React.Component { export default class EmoteIcon extends React.Component {
render() { render() {

View File

@ -1,5 +1,5 @@
import {React, WebpackModules} from "modules"; import {React, WebpackModules} from "modules";
const {ScrollerAuto: Scroller} = WebpackModules.getByProps("ScrollerAuto"); const {ScrollerAuto: Scroller} = WebpackModules.getByProps("ScrollerAuto") ?? {ScrollerAuto: () => null};
export default class EmoteMenuCard extends React.Component { export default class EmoteMenuCard extends React.Component {
render() { render() {
return <div className={`bd-emote-menu`}> return <div className={`bd-emote-menu`}>

View File

@ -3,14 +3,14 @@ import FloatingWindowContainer from "./floating/container";
/* eslint-disable new-cap */ /* eslint-disable new-cap */
const LayerProviders = WebpackModules.getByProps("AppReferencePositionLayer"); const AppLayerProvider = WebpackModules.getByDisplayName("AppLayerProvider");
export default class FloatingWindows { export default class FloatingWindows {
static initialize() { static initialize() {
const containerRef = React.createRef(); const containerRef = React.createRef();
const container = <FloatingWindowContainer ref={containerRef} />; const container = <FloatingWindowContainer ref={containerRef} />;
const wrapped = LayerProviders const wrapped = AppLayerProvider
? React.createElement(LayerProviders.AppLayerProvider().props.layerContext.Provider, {value: [document.querySelector("#app-mount > .layerContainer-yqaFcK")]}, container) // eslint-disable-line new-cap ? React.createElement(AppLayerProvider().props.layerContext.Provider, {value: [document.querySelector("#app-mount > .layerContainer-2v_Sit")]}, container) // eslint-disable-line new-cap
: container; : container;
const div = DOM.createElement(`<div id="floating-windows-layer">`); const div = DOM.createElement(`<div id="floating-windows-layer">`);
DOMManager.bdBody.append(div); DOMManager.bdBody.append(div);

View File

@ -3,7 +3,8 @@ import {React} from "modules";
export default class Globe extends React.Component { export default class Globe extends React.Component {
render() { render() {
const size = this.props.size || "18px"; const size = this.props.size || "18px";
return <svg viewBox="2 2 20 20" fill="#FFFFFF" style={{width: size, height: size}} onClick={this.props.onClick}> const color = this.props.color || "#FFFFFF";
return <svg viewBox="2 2 20 20" fill={color} style={{width: size, height: size}} onClick={this.props.onClick}>
<path d="M0 0h24v24H0z" fill="none"/> <path d="M0 0h24v24H0z" fill="none"/>
<path d="M11.99 2C6.47 2 2 6.48 2 12s4.47 10 9.99 10C17.52 22 22 17.52 22 12S17.52 2 11.99 2zm6.93 6h-2.95c-.32-1.25-.78-2.45-1.38-3.56 1.84.63 3.37 1.91 4.33 3.56zM12 4.04c.83 1.2 1.48 2.53 1.91 3.96h-3.82c.43-1.43 1.08-2.76 1.91-3.96zM4.26 14C4.1 13.36 4 12.69 4 12s.1-1.36.26-2h3.38c-.08.66-.14 1.32-.14 2 0 .68.06 1.34.14 2H4.26zm.82 2h2.95c.32 1.25.78 2.45 1.38 3.56-1.84-.63-3.37-1.9-4.33-3.56zm2.95-8H5.08c.96-1.66 2.49-2.93 4.33-3.56C8.81 5.55 8.35 6.75 8.03 8zM12 19.96c-.83-1.2-1.48-2.53-1.91-3.96h3.82c-.43 1.43-1.08 2.76-1.91 3.96zM14.34 14H9.66c-.09-.66-.16-1.32-.16-2 0-.68.07-1.35.16-2h4.68c.09.65.16 1.32.16 2 0 .68-.07 1.34-.16 2zm.25 5.56c.6-1.11 1.06-2.31 1.38-3.56h2.95c-.96 1.65-2.49 2.93-4.33 3.56zM16.36 14c.08-.66.14-1.32.14-2 0-.68-.06-1.34-.14-2h3.38c.16.64.26 1.31.26 2s-.1 1.36-.26 2h-3.38z"/> <path d="M11.99 2C6.47 2 2 6.48 2 12s4.47 10 9.99 10C17.52 22 22 17.52 22 12S17.52 2 11.99 2zm6.93 6h-2.95c-.32-1.25-.78-2.45-1.38-3.56 1.84.63 3.37 1.91 4.33 3.56zM12 4.04c.83 1.2 1.48 2.53 1.91 3.96h-3.82c.43-1.43 1.08-2.76 1.91-3.96zM4.26 14C4.1 13.36 4 12.69 4 12s.1-1.36.26-2h3.38c-.08.66-.14 1.32-.14 2 0 .68.06 1.34.14 2H4.26zm.82 2h2.95c.32 1.25.78 2.45 1.38 3.56-1.84-.63-3.37-1.9-4.33-3.56zm2.95-8H5.08c.96-1.66 2.49-2.93 4.33-3.56C8.81 5.55 8.35 6.75 8.03 8zM12 19.96c-.83-1.2-1.48-2.53-1.91-3.96h3.82c-.43 1.43-1.08 2.76-1.91 3.96zM14.34 14H9.66c-.09-.66-.16-1.32-.16-2 0-.68.07-1.35.16-2h4.68c.09.65.16 1.32.16 2 0 .68-.07 1.34-.16 2zm.25 5.56c.6-1.11 1.06-2.31 1.38-3.56h2.95c-.96 1.65-2.49 2.93-4.33 3.56zM16.36 14c.08-.66.14-1.32.14-2 0-.68-.06-1.34-.14-2h3.38c.16.64.26 1.31.26 2s-.1 1.36-.26 2h-3.38z"/>
</svg>; </svg>;

View File

@ -5,21 +5,27 @@ import FormattableString from "../structs/string";
import AddonErrorModal from "./addonerrormodal"; import AddonErrorModal from "./addonerrormodal";
import ErrorBoundary from "./errorboundary"; import ErrorBoundary from "./errorboundary";
export default class Modals { export default class Modals {
static get shouldShowAddonErrors() {return Settings.get("settings", "addons", "addonErrors");} static get shouldShowAddonErrors() {return Settings.get("settings", "addons", "addonErrors");}
static get ModalActions() {return WebpackModules.getByProps("openModal", "updateModal");} static get ModalActions() {
return {
openModal: WebpackModules.getModule(m => m?.toString().includes("onCloseCallback") && m?.toString().includes("Layer")),
closeModal: WebpackModules.getModule(m => m?.toString().includes("onCloseCallback()"))
};
}
static get ModalStack() {return WebpackModules.getByProps("push", "update", "pop", "popWithKey");} static get ModalStack() {return WebpackModules.getByProps("push", "update", "pop", "popWithKey");}
static get ModalComponents() {return WebpackModules.getByProps("ModalRoot");} static get ModalComponents() {return WebpackModules.getByProps("Header", "Footer");}
static get ModalRoot() {return WebpackModules.getModule(m => m?.toString().includes("ENTERING"));}
static get ModalClasses() {return WebpackModules.getByProps("modal", "content");} static get ModalClasses() {return WebpackModules.getByProps("modal", "content");}
static get AlertModal() {return WebpackModules.getByPrototypes("handleCancel", "handleSubmit", "handleMinorConfirm");}
static get FlexElements() {return WebpackModules.getByProps("Child", "Align");} static get FlexElements() {return WebpackModules.getByProps("Child", "Align");}
static get FormTitle() {return WebpackModules.findByDisplayName("FormTitle");} static get FormTitle() {return WebpackModules.getByProps("Tags", "Sizes");}
static get TextElement() {return WebpackModules.getByProps("Sizes", "Weights");} static get TextElement() {return WebpackModules.getModule(m => m?.Sizes?.SIZE_32 && m.Colors);}
static get ConfirmationModal() {return WebpackModules.findByDisplayName("ConfirmModal");} static get ConfirmationModal() {return WebpackModules.getModule(m => m?.toString()?.includes("confirmText"));}
static get Markdown() {return WebpackModules.find(m => m.displayName === "Markdown" && m.rules);} static get Markdown() {return WebpackModules.find(m => m?.prototype?.render && m.rules);}
static get Buttons() {return WebpackModules.getByProps("ButtonSizes");} static get Buttons() {return WebpackModules.getByProps("BorderColors");}
static default(title, content) { static default(title, content) {
const modal = DOM.createElement(`<div class="bd-modal-wrapper theme-dark"> const modal = DOM.createElement(`<div class="bd-modal-wrapper theme-dark">
@ -86,7 +92,7 @@ export default class Modals {
return ModalActions.openModal(props => { return ModalActions.openModal(props => {
return React.createElement(ConfirmationModal, Object.assign({ return React.createElement(ConfirmationModal, Object.assign({
header: title, header: title,
confirmButtonColor: danger ? this.Buttons.ButtonColors.RED : this.Buttons.ButtonColors.BRAND, confirmButtonColor: danger ? this.Buttons.Colors.RED : this.Buttons.Colors.BRAND,
confirmText: confirmText, confirmText: confirmText,
cancelText: cancelText, cancelText: cancelText,
onConfirm: onConfirm, onConfirm: onConfirm,
@ -104,7 +110,7 @@ export default class Modals {
} }
this.addonErrorsRef = React.createRef(); this.addonErrorsRef = React.createRef();
this.ModalActions.openModal(props => React.createElement(this.ModalComponents.ModalRoot, Object.assign(props, { this.ModalActions.openModal(props => React.createElement(this.ModalRoot, Object.assign(props, {
size: "medium", size: "medium",
className: "bd-error-modal", className: "bd-error-modal",
children: [ children: [
@ -114,9 +120,9 @@ export default class Modals {
themeErrors: Array.isArray(themeErrors) ? themeErrors : [], themeErrors: Array.isArray(themeErrors) ? themeErrors : [],
onClose: props.onClose onClose: props.onClose
}), }),
React.createElement(this.ModalComponents.ModalFooter, { React.createElement(this.ModalComponents.Footer, {
className: "bd-error-modal-footer", className: "bd-error-modal-footer",
}, React.createElement(this.Buttons.default, { }, React.createElement(this.Buttons, {
onClick: props.onClose, onClick: props.onClose,
className: "bd-button" className: "bd-button"
}, Strings.Modals.okay)) }, Strings.Modals.okay))
@ -221,17 +227,17 @@ export default class Modals {
const mc = this.ModalComponents; const mc = this.ModalComponents;
const modal = props => { const modal = props => {
return React.createElement(mc.ModalRoot, Object.assign({size: mc.ModalSize.MEDIUM, className: "bd-addon-modal"}, props), return React.createElement(ErrorBoundary, {}, React.createElement(this.ModalRoot, Object.assign({size: mc.Sizes.MEDIUM, className: "bd-addon-modal" + " " + mc.Sizes.MEDIUM}, props),
React.createElement(mc.ModalHeader, {separator: false, className: "bd-addon-modal-header"}, React.createElement(mc.Header, {separator: false, className: "bd-addon-modal-header"},
React.createElement(this.FormTitle, {tag: "h4"}, `${name} Settings`) React.createElement(this.FormTitle, {tag: "h4"}, `${name} Settings`)
), ),
React.createElement(mc.ModalContent, {className: "bd-addon-modal-settings"}, React.createElement(mc.Content, {className: "bd-addon-modal-settings"},
React.createElement(ErrorBoundary, {}, child) React.createElement(ErrorBoundary, {}, child)
), ),
React.createElement(mc.ModalFooter, {className: "bd-addon-modal-footer"}, React.createElement(mc.Footer, {className: "bd-addon-modal-footer"},
React.createElement(this.Buttons.default, {onClick: props.onClose, className: "bd-button"}, Strings.Modals.done) React.createElement(this.Buttons, {onClick: props.onClose, className: "bd-button"}, Strings.Modals.done)
) )
); ));
}; };
return this.ModalActions.openModal(props => { return this.ModalActions.openModal(props => {

View File

@ -1,4 +1,4 @@
import {React, WebpackModules, Strings} from "modules"; import {React, WebpackModules, Strings, DiscordModules} from "modules";
import Modals from "../modals"; import Modals from "../modals";
import SettingsTitle from "../settings/title"; import SettingsTitle from "../settings/title";
import ServerCard from "./card"; import ServerCard from "./card";
@ -8,9 +8,26 @@ import Search from "../settings/components/search";
import Previous from "../icons/previous"; import Previous from "../icons/previous";
import Next from "../icons/next"; import Next from "../icons/next";
const SettingsView = WebpackModules.getByDisplayName("SettingsView"); const SettingsView = WebpackModules.getByPrototypes("renderSidebar");
const GuildActions = WebpackModules.getByProps("transitionToGuildSync"); const GuildActions = WebpackModules.getByProps("transitionToGuildSync");
const LayerManager = WebpackModules.getByProps("popLayer"); const LayerManager = {
pushLayer(component) {
DiscordModules.Dispatcher.dispatch({
type: "LAYER_PUSH",
component
});
},
popLayer() {
DiscordModules.Dispatcher.dispatch({
type: "LAYER_POP"
});
},
popAllLayers() {
DiscordModules.Dispatcher.dispatch({
type: "LAYER_POP_ALL"
});
}
};
const EMPTY_RESULTS = { const EMPTY_RESULTS = {
servers: [], servers: [],

View File

@ -62,7 +62,7 @@ export default new class SettingsRenderer {
} }
async patchSections() { async patchSections() {
const UserSettings = await WebpackModules.getLazy(Filters.byDisplayName("SettingsView")); const UserSettings = await WebpackModules.getLazy(Filters.byPrototypeFields(["getPredicateSections"]));
Patcher.after("SettingsManager", UserSettings.prototype, "getPredicateSections", (thisObject, args, returnValue) => { Patcher.after("SettingsManager", UserSettings.prototype, "getPredicateSections", (thisObject, args, returnValue) => {
let location = returnValue.findIndex(s => s.section.toLowerCase() == "changelog") - 1; let location = returnValue.findIndex(s => s.section.toLowerCase() == "changelog") - 1;

View File

@ -1,7 +1,6 @@
import Logger from "common/logger"; import Logger from "common/logger";
import {React, Strings, WebpackModules, DiscordModules} from "modules"; import {React, Strings, WebpackModules, DiscordModules} from "modules";
import SimpleMarkdown from "../../structs/markdown"; import SimpleMarkdown from "../../structs/markdown";
import ReloadIcon from "../icons/reload";
import EditIcon from "../icons/edit"; import EditIcon from "../icons/edit";
import DeleteIcon from "../icons/delete"; import DeleteIcon from "../icons/delete";
import CogIcon from "../icons/cog"; import CogIcon from "../icons/cog";
@ -25,8 +24,25 @@ const LinkIcons = {
patreon: PatreonIcon patreon: PatreonIcon
}; };
const Tooltip = WebpackModules.getByDisplayName("Tooltip"); const Tooltip = WebpackModules.getByPrototypes("renderTooltip");
const LayerStack = WebpackModules.getByProps("popLayer"); const LayerManager = {
pushLayer(component) {
DiscordModules.Dispatcher.dispatch({
type: "LAYER_PUSH",
component
});
},
popLayer() {
DiscordModules.Dispatcher.dispatch({
type: "LAYER_POP"
});
},
popAllLayers() {
DiscordModules.Dispatcher.dispatch({
type: "LAYER_POP_ALL"
});
}
};
const UserStore = WebpackModules.getByProps("getCurrentUser"); const UserStore = WebpackModules.getByProps("getCurrentUser");
const ChannelStore = WebpackModules.getByProps("getDMFromUserId"); const ChannelStore = WebpackModules.getByProps("getDMFromUserId");
const PrivateChannelActions = WebpackModules.getByProps("openPrivateChannel"); const PrivateChannelActions = WebpackModules.getByProps("openPrivateChannel");
@ -41,7 +57,6 @@ export default class AddonCard extends React.Component {
this.panelRef = React.createRef(); this.panelRef = React.createRef();
this.onChange = this.onChange.bind(this); this.onChange = this.onChange.bind(this);
this.reload = this.reload.bind(this);
this.showSettings = this.showSettings.bind(this); this.showSettings = this.showSettings.bind(this);
this.messageAuthor = this.messageAuthor.bind(this); this.messageAuthor = this.messageAuthor.bind(this);
} }
@ -58,12 +73,6 @@ export default class AddonCard extends React.Component {
} }
} }
reload() {
if (!this.props.reload) return;
this.props.addon = this.props.reload(this.props.addon.id);
this.forceUpdate();
}
getString(value) {return typeof value == "string" ? value : value.toString();} getString(value) {return typeof value == "string" ? value : value.toString();}
onChange() { onChange() {
@ -74,7 +83,7 @@ export default class AddonCard extends React.Component {
messageAuthor() { messageAuthor() {
if (!this.props.addon.authorId) return; if (!this.props.addon.authorId) return;
if (LayerStack) LayerStack.popLayer(); if (LayerManager) LayerManager.popLayer();
if (!UserStore || !ChannelActions || !ChannelStore || !PrivateChannelActions) return; if (!UserStore || !ChannelActions || !ChannelStore || !PrivateChannelActions) return;
const selfId = UserStore.getCurrentUser().id; const selfId = UserStore.getCurrentUser().id;
if (selfId == this.props.addon.authorId) return; if (selfId == this.props.addon.authorId) return;
@ -114,7 +123,7 @@ export default class AddonCard extends React.Component {
let code = url; let code = url;
const tester = /\.gg\/(.*)$/; const tester = /\.gg\/(.*)$/;
if (tester.test(code)) code = code.match(tester)[1]; if (tester.test(code)) code = code.match(tester)[1];
DiscordModules.LayerStack.popLayer(); LayerManager.popLayer();
DiscordModules.InviteActions.acceptInviteAndTransitionToInviteChannel(code); DiscordModules.InviteActions.acceptInviteAndTransitionToInviteChannel(code);
}; };
} }
@ -124,7 +133,6 @@ export default class AddonCard extends React.Component {
get controls() { // {this.props.hasSettings && <button onClick={this.showSettings} className="bd-button bd-button-addon-settings" disabled={!this.props.enabled}>{Strings.Addons.addonSettings}</button>} get controls() { // {this.props.hasSettings && <button onClick={this.showSettings} className="bd-button bd-button-addon-settings" disabled={!this.props.enabled}>{Strings.Addons.addonSettings}</button>}
return <div className="bd-controls"> return <div className="bd-controls">
{this.props.hasSettings && this.makeControlButton(Strings.Addons.addonSettings, <CogIcon size={"20px"} />, this.showSettings, {disabled: !this.props.enabled})} {this.props.hasSettings && this.makeControlButton(Strings.Addons.addonSettings, <CogIcon size={"20px"} />, this.showSettings, {disabled: !this.props.enabled})}
{this.props.showReloadIcon && this.makeControlButton(Strings.Addons.reload, <ReloadIcon size={"20px"} />, this.reload)}
{this.props.editAddon && this.makeControlButton(Strings.Addons.editAddon, <EditIcon size={"20px"} />, this.props.editAddon)} {this.props.editAddon && this.makeControlButton(Strings.Addons.editAddon, <EditIcon size={"20px"} />, this.props.editAddon)}
{this.props.deleteAddon && this.makeControlButton(Strings.Addons.deleteAddon, <DeleteIcon size={"20px"} />, this.props.deleteAddon, {danger: true})} {this.props.deleteAddon && this.makeControlButton(Strings.Addons.deleteAddon, <DeleteIcon size={"20px"} />, this.props.deleteAddon, {danger: true})}
</div>; </div>;

View File

@ -1,9 +1,8 @@
import Logger from "common/logger"; import Logger from "common/logger";
import {React, Settings, Strings, Events, WebpackModules, DataStore} from "modules"; import {React, Strings, Events, WebpackModules, DataStore} from "modules";
import Modals from "../modals"; import Modals from "../modals";
import SettingsTitle from "./title"; import SettingsTitle from "./title";
import ReloadIcon from "../icons/reload";
import AddonCard from "./addoncard"; import AddonCard from "./addoncard";
import Dropdown from "./components/dropdown"; import Dropdown from "./components/dropdown";
import Search from "./components/search"; import Search from "./components/search";
@ -14,7 +13,7 @@ import GridIcon from "../icons/grid";
import NoResults from "../blankslates/noresults"; import NoResults from "../blankslates/noresults";
import EmptyImage from "../blankslates/emptyimage"; import EmptyImage from "../blankslates/emptyimage";
const Tooltip = WebpackModules.getByDisplayName("Tooltip"); const Tooltip = WebpackModules.getByPrototypes("renderTooltip");
export default class AddonList extends React.Component { export default class AddonList extends React.Component {
@ -125,7 +124,6 @@ export default class AddonList extends React.Component {
render() { render() {
const {title, folder, addonList, addonState, onChange, reload} = this.props; const {title, folder, addonList, addonState, onChange, reload} = this.props;
const showReloadIcon = !Settings.get("settings", "addons", "autoReload");
const button = folder ? {title: Strings.Addons.openFolder.format({type: title}), onClick: this.openFolder} : null; const button = folder ? {title: Strings.Addons.openFolder.format({type: title}), onClick: this.openFolder} : null;
let sortedAddons = addonList.sort((a, b) => { let sortedAddons = addonList.sort((a, b) => {
const sortByEnabled = this.state.sort === "isEnabled"; const sortByEnabled = this.state.sort === "isEnabled";
@ -152,7 +150,7 @@ export default class AddonList extends React.Component {
const renderedCards = sortedAddons.map(addon => { const renderedCards = sortedAddons.map(addon => {
const hasSettings = addon.instance && typeof(addon.instance.getSettingsPanel) === "function"; const hasSettings = addon.instance && typeof(addon.instance.getSettingsPanel) === "function";
const getSettings = hasSettings && addon.instance.getSettingsPanel.bind(addon.instance); const getSettings = hasSettings && addon.instance.getSettingsPanel.bind(addon.instance);
return <ErrorBoundary><AddonCard type={this.props.type} editAddon={this.editAddon.bind(this, addon.id)} deleteAddon={this.deleteAddon.bind(this, addon.id)} showReloadIcon={showReloadIcon} key={addon.id} enabled={addonState[addon.id]} addon={addon} onChange={onChange} reload={reload} hasSettings={hasSettings} getSettingsPanel={getSettings} /></ErrorBoundary>; return <ErrorBoundary><AddonCard type={this.props.type} editAddon={this.editAddon.bind(this, addon.id)} deleteAddon={this.deleteAddon.bind(this, addon.id)} key={addon.id} enabled={addonState[addon.id]} addon={addon} onChange={onChange} reload={reload} hasSettings={hasSettings} getSettingsPanel={getSettings} /></ErrorBoundary>;
}); });
const hasAddonsInstalled = this.props.addonList.length !== 0; const hasAddonsInstalled = this.props.addonList.length !== 0;
@ -160,7 +158,7 @@ export default class AddonList extends React.Component {
const hasResults = sortedAddons.length !== 0; const hasResults = sortedAddons.length !== 0;
return [ return [
<SettingsTitle key="title" text={title} button={button} otherChildren={showReloadIcon && <ReloadIcon className="bd-reload" onClick={this.reload.bind(this)} />} />, <SettingsTitle key="title" text={title} button={button} />,
<div className={"bd-controls bd-addon-controls"}> <div className={"bd-controls bd-addon-controls"}>
<Search onChange={this.search} placeholder={`${Strings.Addons.search.format({type: this.props.title})}...`} /> <Search onChange={this.search} placeholder={`${Strings.Addons.search.format({type: this.props.title})}...`} />
<div className="bd-controls-advanced"> <div className="bd-controls-advanced">

View File

@ -4,7 +4,7 @@ import HistoryIcon from "../icons/history";
import Modals from "../modals"; import Modals from "../modals";
const SidebarComponents = WebpackModules.getModule(m => m.Header && m.Separator && m.Item); const SidebarComponents = WebpackModules.getModule(m => m.Header && m.Separator && m.Item);
const Tooltip = WebpackModules.getByDisplayName("Tooltip"); const Tooltip = WebpackModules.getByPrototypes("renderTooltip");
export default class SettingsTitle extends React.Component { export default class SettingsTitle extends React.Component {
render() { render() {