diff options
Diffstat (limited to 'scripts')
-rw-r--r-- | scripts/generatePluginList.ts | 191 | ||||
-rw-r--r-- | scripts/generateReport.ts | 285 |
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"); |