ci: Auto generate plugin json
This commit is contained in:
parent
d0b3678ad6
commit
1368c25824
8 changed files with 497 additions and 78 deletions
191
scripts/generatePluginList.ts
Normal file
191
scripts/generatePluginList.ts
Normal file
|
@ -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);
|
||||
}
|
||||
})();
|
285
scripts/generateReport.ts
Normal file
285
scripts/generateReport.ts
Normal file
|
@ -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");
|
Loading…
Add table
Add a link
Reference in a new issue