aboutsummaryrefslogtreecommitdiff
path: root/scripts
diff options
context:
space:
mode:
Diffstat (limited to 'scripts')
-rw-r--r--scripts/generatePluginList.ts191
-rw-r--r--scripts/generateReport.ts285
2 files changed, 476 insertions, 0 deletions
diff --git a/scripts/generatePluginList.ts b/scripts/generatePluginList.ts
new file mode 100644
index 0000000..1f66c3d
--- /dev/null
+++ b/scripts/generatePluginList.ts
@@ -0,0 +1,191 @@
+/*
+ * Vencord, a modification for Discord's desktop app
+ * Copyright (c) 2023 Vendicated and contributors
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+*/
+
+import { Dirent, readdirSync, readFileSync, writeFileSync } from "fs";
+import { access, readFile } from "fs/promises";
+import { join } from "path";
+import { BigIntLiteral, createSourceFile, Identifier, isArrayLiteralExpression, isCallExpression, isExportAssignment, isIdentifier, isObjectLiteralExpression, isPropertyAccessExpression, isPropertyAssignment, isStringLiteral, isVariableStatement, NamedDeclaration, NodeArray, ObjectLiteralExpression, ScriptTarget, StringLiteral, SyntaxKind } from "typescript";
+
+interface Dev {
+ name: string;
+ id: string;
+}
+
+interface PluginData {
+ name: string;
+ description: string;
+ authors: Dev[];
+ dependencies: string[];
+ hasPatches: boolean;
+ hasCommands: boolean;
+ required: boolean;
+ enabledByDefault: boolean;
+ target: "desktop" | "web" | "dev";
+}
+
+const devs = {} as Record<string, Dev>;
+
+function getName(node: NamedDeclaration) {
+ return node.name && isIdentifier(node.name) ? node.name.text : undefined;
+}
+
+function hasName(node: NamedDeclaration, name: string) {
+ return getName(node) === name;
+}
+
+function getObjectProp(node: ObjectLiteralExpression, name: string) {
+ const prop = node.properties.find(p => hasName(p, name));
+ if (prop && isPropertyAssignment(prop)) return prop.initializer;
+ return prop;
+}
+
+function parseDevs() {
+ const file = createSourceFile("constants.ts", readFileSync("src/utils/constants.ts", "utf8"), ScriptTarget.Latest);
+
+ for (const child of file.getChildAt(0).getChildren()) {
+ if (!isVariableStatement(child)) continue;
+
+ const devsDeclaration = child.declarationList.declarations.find(d => hasName(d, "Devs"));
+ if (!devsDeclaration?.initializer || !isCallExpression(devsDeclaration.initializer)) continue;
+
+ const value = devsDeclaration.initializer.arguments[0];
+
+ if (!isObjectLiteralExpression(value)) return;
+
+ for (const prop of value.properties) {
+ const name = (prop.name as Identifier).text;
+ const value = isPropertyAssignment(prop) ? prop.initializer : prop;
+
+ if (!isObjectLiteralExpression(value)) throw new Error(`Failed to parse devs: ${name} is not an object literal`);
+
+ devs[name] = {
+ name: (getObjectProp(value, "name") as StringLiteral).text,
+ id: (getObjectProp(value, "id") as BigIntLiteral).text.slice(0, -1)
+ };
+ }
+
+ return;
+ }
+
+ throw new Error("Could not find Devs constant");
+}
+
+async function parseFile(fileName: string) {
+ const file = createSourceFile(fileName, await readFile(fileName, "utf8"), ScriptTarget.Latest);
+
+ const fail = (reason: string) => {
+ return new Error(`Invalid plugin ${fileName}, because ${reason}`);
+ };
+
+ for (const node of file.getChildAt(0).getChildren()) {
+ if (!isExportAssignment(node) || !isCallExpression(node.expression)) continue;
+
+ const call = node.expression;
+ if (!isIdentifier(call.expression) || call.expression.text !== "definePlugin") continue;
+
+ const pluginObj = node.expression.arguments[0];
+ if (!isObjectLiteralExpression(pluginObj)) throw fail("no object literal passed to definePlugin");
+
+ const data = {
+ hasPatches: false,
+ hasCommands: false,
+ enabledByDefault: false,
+ required: false,
+ } as PluginData;
+
+ for (const prop of pluginObj.properties) {
+ const key = getName(prop);
+ const value = isPropertyAssignment(prop) ? prop.initializer : prop;
+
+ switch (key) {
+ case "name":
+ case "description":
+ if (!isStringLiteral(value)) throw fail(`${key} is not a string literal`);
+ data[key] = value.text;
+ break;
+ case "patches":
+ data.hasPatches = true;
+ break;
+ case "commands":
+ data.hasCommands = true;
+ break;
+ case "authors":
+ if (!isArrayLiteralExpression(value)) throw fail("authors is not an array literal");
+ data.authors = value.elements.map(e => {
+ if (!isPropertyAccessExpression(e)) throw fail("authors array contains non-property access expressions");
+ return devs[getName(e)!];
+ });
+ break;
+ case "dependencies":
+ if (!isArrayLiteralExpression(value)) throw fail("dependencies is not an array literal");
+ const { elements } = value;
+ if (elements.some(e => !isStringLiteral(e))) throw fail("dependencies array contains non-string elements");
+ data.dependencies = (elements as NodeArray<StringLiteral>).map(e => e.text);
+ break;
+ case "required":
+ case "enabledByDefault":
+ data[key] = value.kind === SyntaxKind.TrueKeyword;
+ if (!data[key] && value.kind !== SyntaxKind.FalseKeyword) throw fail(`${key} is not a boolean literal`);
+ break;
+ }
+ }
+
+ if (!data.name || !data.description || !data.authors) throw fail("name, description or authors are missing");
+
+ const fileBits = fileName.split(".");
+ if (fileBits.length > 2 && ["ts", "tsx"].includes(fileBits.at(-1)!)) {
+ const mod = fileBits.at(-2)!;
+ if (!["web", "desktop", "dev"].includes(mod)) throw fail(`invalid target ${fileBits.at(-2)}`);
+ data.target = mod as any;
+ }
+
+ return data;
+ }
+
+ throw fail("no default export called 'definePlugin' found");
+}
+
+async function getEntryPoint(dirent: Dirent) {
+ const base = join("./src/plugins", dirent.name);
+ if (!dirent.isDirectory()) return base;
+
+ for (const name of ["index.ts", "index.tsx"]) {
+ const full = join(base, name);
+ try {
+ await access(full);
+ return full;
+ } catch { }
+ }
+
+ throw new Error(`${dirent.name}: Couldn't find entry point`);
+}
+
+(async () => {
+ parseDevs();
+ const plugins = readdirSync("./src/plugins", { withFileTypes: true }).filter(d => d.name !== "index.ts");
+
+ const promises = plugins.map(async dirent => parseFile(await getEntryPoint(dirent)));
+
+ const data = JSON.stringify(await Promise.all(promises));
+
+ if (process.argv.length > 2) {
+ writeFileSync(process.argv[2], data);
+ } else {
+ console.log(data);
+ }
+})();
diff --git a/scripts/generateReport.ts b/scripts/generateReport.ts
new file mode 100644
index 0000000..d55cc8a
--- /dev/null
+++ b/scripts/generateReport.ts
@@ -0,0 +1,285 @@
+/*
+ * Vencord, a modification for Discord's desktop app
+ * Copyright (c) 2022 Vendicated and contributors
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+*/
+
+// eslint-disable-next-line spaced-comment
+/// <reference types="../src/globals" />
+// eslint-disable-next-line spaced-comment
+/// <reference types="../src/modules" />
+
+import { readFileSync } from "fs";
+import pup, { JSHandle } from "puppeteer-core";
+
+for (const variable of ["DISCORD_TOKEN", "CHROMIUM_BIN"]) {
+ if (!process.env[variable]) {
+ console.error(`Missing environment variable ${variable}`);
+ process.exit(1);
+ }
+}
+
+const CANARY = process.env.USE_CANARY === "true";
+
+const browser = await pup.launch({
+ headless: true,
+ executablePath: process.env.CHROMIUM_BIN
+});
+
+const page = await browser.newPage();
+await page.setUserAgent("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/108.0.0.0 Safari/537.36");
+
+function maybeGetError(handle: JSHandle) {
+ return (handle as JSHandle<Error>)?.getProperty("message")
+ .then(m => m.jsonValue());
+}
+
+const report = {
+ badPatches: [] as {
+ plugin: string;
+ type: string;
+ id: string;
+ match: string;
+ error?: string;
+ }[],
+ badStarts: [] as {
+ plugin: string;
+ error: string;
+ }[],
+ otherErrors: [] as string[]
+};
+
+function toCodeBlock(s: string) {
+ s = s.replace(/```/g, "`\u200B`\u200B`");
+ return "```" + s + " ```";
+}
+
+async function printReport() {
+ console.log("# Vencord Report" + (CANARY ? " (Canary)" : ""));
+ console.log();
+
+ console.log("## Bad Patches");
+ report.badPatches.forEach(p => {
+ console.log(`- ${p.plugin} (${p.type})`);
+ console.log(` - ID: \`${p.id}\``);
+ console.log(` - Match: ${toCodeBlock(p.match)}`);
+ if (p.error) console.log(` - Error: ${toCodeBlock(p.error)}`);
+ });
+
+ console.log();
+
+ console.log("## Bad Starts");
+ report.badStarts.forEach(p => {
+ console.log(`- ${p.plugin}`);
+ console.log(` - Error: ${toCodeBlock(p.error)}`);
+ });
+
+ console.log("## Discord Errors");
+ report.otherErrors.forEach(e => {
+ console.log(`- ${toCodeBlock(e)}`);
+ });
+
+ if (process.env.DISCORD_WEBHOOK) {
+ // this code was written almost entirely by Copilot xD
+ await fetch(process.env.DISCORD_WEBHOOK, {
+ method: "POST",
+ headers: {
+ "Content-Type": "application/json"
+ },
+ body: JSON.stringify({
+ description: "Here's the latest Vencord Report!",
+ username: "Vencord Reporter" + (CANARY ? " (Canary)" : ""),
+ avatar_url: "https://cdn.discordapp.com/icons/1015060230222131221/f0204a918c6c9c9a43195997e97d8adf.webp",
+ embeds: [
+ {
+ title: "Bad Patches",
+ description: report.badPatches.map(p => {
+ const lines = [
+ `**__${p.plugin} (${p.type}):__**`,
+ `ID: \`${p.id}\``,
+ `Match: ${toCodeBlock(p.match)}`
+ ];
+ if (p.error) lines.push(`Error: ${toCodeBlock(p.error)}`);
+ return lines.join("\n");
+ }).join("\n\n") || "None",
+ color: report.badPatches.length ? 0xff0000 : 0x00ff00
+ },
+ {
+ title: "Bad Starts",
+ description: report.badStarts.map(p => {
+ const lines = [
+ `**__${p.plugin}:__**`,
+ toCodeBlock(p.error)
+ ];
+ return lines.join("\n");
+ }
+ ).join("\n\n") || "None",
+ color: report.badStarts.length ? 0xff0000 : 0x00ff00
+ },
+ {
+ title: "Discord Errors",
+ description: toCodeBlock(report.otherErrors.join("\n")),
+ color: report.otherErrors.length ? 0xff0000 : 0x00ff00
+ }
+ ]
+ })
+ }).then(res => {
+ if (!res.ok) console.error(`Webhook failed with status ${res.status}`);
+ else console.error("Posted to Discord Webhook successfully");
+ });
+ }
+}
+
+page.on("console", async e => {
+ const level = e.type();
+ const args = e.args();
+
+ const firstArg = (await args[0]?.jsonValue());
+ if (firstArg === "PUPPETEER_TEST_DONE_SIGNAL") {
+ await browser.close();
+ await printReport();
+ process.exit();
+ }
+
+ const isVencord = (await args[0]?.jsonValue()) === "[Vencord]";
+ const isDebug = (await args[0]?.jsonValue()) === "[PUP_DEBUG]";
+
+ if (isVencord) {
+ // make ci fail
+ process.exitCode = 1;
+
+ const jsonArgs = await Promise.all(args.map(a => a.jsonValue()));
+ const [, tag, message] = jsonArgs;
+ const cause = await maybeGetError(args[3]);
+
+ switch (tag) {
+ case "WebpackInterceptor:":
+ const [, plugin, type, id, regex] = (message as string).match(/Patch by (.+?) (had no effect|errored|found no module) \(Module id is (.+?)\): (.+)/)!;
+ report.badPatches.push({
+ plugin,
+ type,
+ id,
+ match: regex,
+ error: cause
+ });
+ break;
+ case "PluginManager:":
+ const [, name] = (message as string).match(/Failed to start (.+)/)!;
+ report.badStarts.push({
+ plugin: name,
+ error: cause
+ });
+ break;
+ }
+ } else if (isDebug) {
+ console.error(e.text());
+ } else if (level === "error") {
+ const text = e.text();
+ if (!text.startsWith("Failed to load resource: the server responded with a status of")) {
+ console.error("Got unexpected error", text);
+ report.otherErrors.push(text);
+ }
+ }
+});
+
+page.on("error", e => console.error("[Error]", e));
+page.on("pageerror", e => console.error("[Page Error]", e));
+
+await page.setBypassCSP(true);
+
+function runTime(token: string) {
+ console.error("[PUP_DEBUG]", "Starting test...");
+
+ try {
+ // spoof languages to not be suspicious
+ Object.defineProperty(navigator, "languages", {
+ get: function () {
+ return ["en-US", "en"];
+ },
+ });
+
+
+ // Monkey patch Logger to not log with custom css
+ // @ts-ignore
+ Vencord.Util.Logger.prototype._log = function (level, levelColor, args) {
+ if (level === "warn" || level === "error")
+ console[level]("[Vencord]", this.name + ":", ...args);
+ };
+
+ // force enable all plugins and patches
+ Vencord.Plugins.patches.length = 0;
+ Object.values(Vencord.Plugins.plugins).forEach(p => {
+ // Needs native server to run
+ if (p.name === "WebRichPresence (arRPC)") return;
+
+ p.required = true;
+ p.patches?.forEach(patch => {
+ patch.plugin = p.name;
+ delete patch.predicate;
+ if (!Array.isArray(patch.replacement))
+ patch.replacement = [patch.replacement];
+ Vencord.Plugins.patches.push(patch);
+ });
+ });
+
+ Vencord.Webpack.waitFor(
+ "loginToken",
+ m => {
+ console.error("[PUP_DEBUG]", "Logging in with token...");
+ m.loginToken(token);
+ }
+ );
+
+ // force load all chunks
+ Vencord.Webpack.onceReady.then(() => setTimeout(async () => {
+ console.error("[PUP_DEBUG]", "Webpack is ready!");
+
+ const { wreq } = Vencord.Webpack;
+
+ console.error("[PUP_DEBUG]", "Loading all chunks...");
+ const ids = Function("return" + wreq.u.toString().match(/\{.+\}/s)![0])();
+ for (const id in ids) {
+ const isWasm = await fetch(wreq.p + wreq.u(id))
+ .then(r => r.text())
+ .then(t => t.includes(".module.wasm"));
+
+ if (!isWasm)
+ await wreq.e(id as any);
+
+ await new Promise(r => setTimeout(r, 100));
+ }
+ console.error("[PUP_DEBUG]", "Finished loading chunks!");
+
+ for (const patch of Vencord.Plugins.patches) {
+ if (!patch.all) {
+ new Vencord.Util.Logger("WebpackInterceptor").warn(`Patch by ${patch.plugin} found no module (Module id is -): ${patch.find}`);
+ }
+ }
+ setTimeout(() => console.log("PUPPETEER_TEST_DONE_SIGNAL"), 1000);
+ }, 1000));
+ } catch (e) {
+ console.error("[PUP_DEBUG]", "A fatal error occured");
+ console.error("[PUP_DEBUG]", e);
+ process.exit(1);
+ }
+}
+
+await page.evaluateOnNewDocument(`
+ ${readFileSync("./dist/browser.js", "utf-8")}
+
+ ;(${runTime.toString()})(${JSON.stringify(process.env.DISCORD_TOKEN)});
+`);
+
+await page.goto(CANARY ? "https://canary.discord.com/login" : "https://discord.com/login");