Skip to content

Commit

Permalink
Make Reporter runnable in desktop
Browse files Browse the repository at this point in the history
  • Loading branch information
Nuckyz committed Jun 1, 2024
1 parent 7ccd073 commit fa9f16a
Show file tree
Hide file tree
Showing 8 changed files with 289 additions and 277 deletions.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
"buildWeb": "node --require=./scripts/suppressExperimentalWarnings.js scripts/build/buildWeb.mjs",
"buildWebStandalone": "pnpm buildWeb --standalone",
"buildReporter": "pnpm buildWebStandalone --reporter --skip-extension",
"buildReporterDesktop": "pnpm build --reporter",
"watch": "pnpm build --watch",
"watchWeb": "pnpm buildWeb --watch",
"generatePluginJson": "tsx scripts/generatePluginList.ts",
Expand Down
315 changes: 48 additions & 267 deletions scripts/generateReport.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/

/* eslint-disable no-fallthrough */

// eslint-disable-next-line spaced-comment
/// <reference types="../src/globals" />
// eslint-disable-next-line spaced-comment
Expand All @@ -40,10 +42,11 @@ const browser = await pup.launch({

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");
await page.setBypassCSP(true);

function maybeGetError(handle: JSHandle) {
return (handle as JSHandle<Error>)?.getProperty("message")
.then(m => m.jsonValue());
async function maybeGetError(handle: JSHandle): Promise<string | undefined> {
return await (handle as JSHandle<Error>)?.getProperty("message")
.then(m => m?.jsonValue());
}

const report = {
Expand Down Expand Up @@ -188,38 +191,39 @@ page.on("console", async e => {
const level = e.type();
const rawArgs = e.args();

const firstArg = await rawArgs[0]?.jsonValue();
if (firstArg === "[PUPPETEER_TEST_DONE_SIGNAL]") {
await browser.close();
await printReport();
process.exit();
async function getText() {
try {
return await Promise.all(
e.args().map(async a => {
return await maybeGetError(a) || await a.jsonValue();
})
).then(a => a.join(" ").trim());
} catch {
return e.text();
}
}

const firstArg = await rawArgs[0]?.jsonValue();

const isVencord = firstArg === "[Vencord]";
const isDebug = firstArg === "[PUP_DEBUG]";
const isWebpackFindFail = firstArg === "[PUP_WEBPACK_FIND_FAIL]";

if (isWebpackFindFail) {
process.exitCode = 1;
report.badWebpackFinds.push(await rawArgs[1].jsonValue() as string);
}

outer:
if (isVencord) {
let args: unknown[] = [];
try {
args = await Promise.all(e.args().map(a => a.jsonValue()));
var args = await Promise.all(e.args().map(a => a.jsonValue()));
} catch {
return;
break outer;
}

const [, tag, message] = args as Array<string>;
const cause = await maybeGetError(e.args()[3]);
const [, tag, message, otherMessage] = args as Array<string>;

switch (tag) {
case "WebpackInterceptor:":
const patchFailMatch = message.match(/Patch by (.+?) (had no effect|errored|found no module) \(Module id is (.+?)\): (.+)/)!;
if (!patchFailMatch) break;

console.log(await getText());
process.exitCode = 1;

const [, plugin, type, id, regex] = patchFailMatch;
Expand All @@ -228,45 +232,44 @@ page.on("console", async e => {
type,
id,
match: regex.replace(/\[A-Za-z_\$\]\[\\w\$\]\*/g, "\\i"),
error: cause
error: await maybeGetError(e.args()[3])
});

break;
case "PluginManager:":
const failedToStartMatch = message.match(/Failed to start (.+)/);
if (!failedToStartMatch) break;

console.log(await getText());
process.exitCode = 1;

const [, name] = failedToStartMatch;
report.badStarts.push({
plugin: name,
error: cause
error: await maybeGetError(e.args()[3]) ?? "Unknown error"
});

break;
}
}

async function getText() {
try {
return await Promise.all(
e.args().map(async a => {
return await maybeGetError(a) || await a.jsonValue();
})
).then(a => a.join(" ").trim());
} catch {
return e.text();
case "Reporter:":
console.log(await getText());

switch (message) {
case "Webpack Find Fail:":
process.exitCode = 1;
report.badWebpackFinds.push(otherMessage);
break;
case "A fatal error occurred:":
process.exit(1);
case "Finished test":
await browser.close();
await printReport();
process.exit();
}
}
}

if (isDebug) {
const text = await getText();

console.error(text);
if (text.includes("A fatal error occurred:")) {
process.exit(1);
}
console.log(await getText());
} else if (level === "error") {
const text = await getText();

Expand All @@ -280,236 +283,14 @@ page.on("console", async e => {
page.on("error", e => console.error("[Error]", e));
page.on("pageerror", e => console.error("[Page Error]", e));

await page.setBypassCSP(true);

async function reporterRuntime(token: string) {
console.log("[PUP_DEBUG]", "Starting test...");

try {
// Spoof languages to not be suspicious
Object.defineProperty(navigator, "languages", {
get: function () {
return ["en-US", "en"];
}
});

let wreq: typeof Vencord.Webpack.wreq;

const { canonicalizeMatch, Logger } = Vencord.Util;

const validChunks = new Set<string>();
const invalidChunks = new Set<string>();
const deferredRequires = new Set<string>();

let chunksSearchingResolve: (value: void | PromiseLike<void>) => void;
const chunksSearchingDone = new Promise<void>(r => chunksSearchingResolve = r);

// True if resolved, false otherwise
const chunksSearchPromises = [] as Array<() => boolean>;

const LazyChunkRegex = canonicalizeMatch(/(?:(?:Promise\.all\(\[)?(\i\.e\("[^)]+?"\)[^\]]*?)(?:\]\))?)\.then\(\i\.bind\(\i,"([^)]+?)"\)\)/g);

async function searchAndLoadLazyChunks(factoryCode: string) {
const lazyChunks = factoryCode.matchAll(LazyChunkRegex);
const validChunkGroups = new Set<[chunkIds: string[], entryPoint: string]>();

// Workaround for a chunk that depends on the ChannelMessage component but may be be force loaded before
// the chunk containing the component
const shouldForceDefer = factoryCode.includes(".Messages.GUILD_FEED_UNFEATURE_BUTTON_TEXT");

await Promise.all(Array.from(lazyChunks).map(async ([, rawChunkIds, entryPoint]) => {
const chunkIds = rawChunkIds ? Array.from(rawChunkIds.matchAll(Vencord.Webpack.ChunkIdsRegex)).map(m => m[1]) : [];

if (chunkIds.length === 0) {
return;
}

let invalidChunkGroup = false;

for (const id of chunkIds) {
if (wreq.u(id) == null || wreq.u(id) === "undefined.js") continue;

const isWasm = await fetch(wreq.p + wreq.u(id))
.then(r => r.text())
.then(t => t.includes(".module.wasm") || !t.includes("(this.webpackChunkdiscord_app=this.webpackChunkdiscord_app||[]).push"));

if (isWasm) {
invalidChunks.add(id);
invalidChunkGroup = true;
continue;
}

validChunks.add(id);
}

if (!invalidChunkGroup) {
validChunkGroups.add([chunkIds, entryPoint]);
}
}));

// Loads all found valid chunk groups
await Promise.all(
Array.from(validChunkGroups)
.map(([chunkIds]) =>
Promise.all(chunkIds.map(id => wreq.e(id as any).catch(() => { })))
)
);

// Requires the entry points for all valid chunk groups
for (const [, entryPoint] of validChunkGroups) {
try {
if (shouldForceDefer) {
deferredRequires.add(entryPoint);
continue;
}

if (wreq.m[entryPoint]) wreq(entryPoint as any);
} catch (err) {
console.error(err);
}
}

// setImmediate to only check if all chunks were loaded after this function resolves
// We check if all chunks were loaded every time a factory is loaded
// If we are still looking for chunks in the other factories, the array will have that factory's chunk search promise not resolved
// But, if all chunk search promises are resolved, this means we found every lazy chunk loaded by Discord code and manually loaded them
setTimeout(() => {
let allResolved = true;

for (let i = 0; i < chunksSearchPromises.length; i++) {
const isResolved = chunksSearchPromises[i]();

if (isResolved) {
// Remove finished promises to avoid having to iterate through a huge array everytime
chunksSearchPromises.splice(i--, 1);
} else {
allResolved = false;
}
}

if (allResolved) chunksSearchingResolve();
}, 0);
}

Vencord.Webpack.waitFor(
"loginToken",
m => {
console.log("[PUP_DEBUG]", "Logging in with token...");
m.loginToken(token);
}
);

Vencord.Webpack.beforeInitListeners.add(async webpackRequire => {
console.log("[PUP_DEBUG]", "Loading all chunks...");

wreq = webpackRequire;

Vencord.Webpack.factoryListeners.add(factory => {
let isResolved = false;
searchAndLoadLazyChunks(factory.toString()).then(() => isResolved = true);

chunksSearchPromises.push(() => isResolved);
});

// setImmediate to only search the initial factories after Discord initialized the app
// our beforeInitListeners are called before Discord initializes the app
setTimeout(() => {
for (const factoryId in wreq.m) {
let isResolved = false;
searchAndLoadLazyChunks(wreq.m[factoryId].toString()).then(() => isResolved = true);

chunksSearchPromises.push(() => isResolved);
}
}, 0);
});

await chunksSearchingDone;

// Require deferred entry points
for (const deferredRequire of deferredRequires) {
wreq!(deferredRequire as any);
}

// All chunks Discord has mapped to asset files, even if they are not used anymore
const allChunks = [] as string[];

// Matches "id" or id:
for (const currentMatch of wreq!.u.toString().matchAll(/(?:"(\d+?)")|(?:(\d+?):)/g)) {
const id = currentMatch[1] ?? currentMatch[2];
if (id == null) continue;

allChunks.push(id);
Vencord.Webpack.waitFor(
"loginToken",
m => {
console.log("[PUP_DEBUG]", "Logging in with token...");
m.loginToken(token);
}

if (allChunks.length === 0) throw new Error("Failed to get all chunks");

// Chunks that are not loaded (not used) by Discord code anymore
const chunksLeft = allChunks.filter(id => {
return !(validChunks.has(id) || invalidChunks.has(id));
});

await Promise.all(chunksLeft.map(async id => {
const isWasm = await fetch(wreq.p + wreq.u(id))
.then(r => r.text())
.then(t => t.includes(".module.wasm") || !t.includes("(this.webpackChunkdiscord_app=this.webpackChunkdiscord_app||[]).push"));

// Loads and requires a chunk
if (!isWasm) {
await wreq.e(id as any);
if (wreq.m[id]) wreq(id as any);
}
}));

console.log("[PUP_DEBUG]", "Finished loading all chunks!");

for (const patch of Vencord.Plugins.patches) {
if (!patch.all) {
new Logger("WebpackInterceptor").warn(`Patch by ${patch.plugin} found no module (Module id is -): ${patch.find}`);
}
}

for (const [searchType, args] of Vencord.Webpack.lazyWebpackSearchHistory) {
let method = searchType;

if (searchType === "findComponent") method = "find";
if (searchType === "findExportedComponent") method = "findByProps";
if (searchType === "waitFor" || searchType === "waitForComponent") {
if (typeof args[0] === "string") method = "findByProps";
else method = "find";
}
if (searchType === "waitForStore") method = "findStore";

try {
let result: any;

if (method === "proxyLazyWebpack" || method === "LazyComponentWebpack") {
const [factory] = args;
result = factory();
} else if (method === "extractAndLoadChunks") {
const [code, matcher] = args;

result = await Vencord.Webpack.extractAndLoadChunks(code, matcher);
if (result === false) result = null;
} else {
// @ts-ignore
result = Vencord.Webpack[method](...args);
}

if (result == null || (result.$$vencordInternal != null && result.$$vencordInternal() == null)) throw "a rock at ben shapiro";
} catch (e) {
let logMessage = searchType;
if (method === "find" || method === "proxyLazyWebpack" || method === "LazyComponentWebpack") logMessage += `(${args[0].toString().slice(0, 147)}...)`;
else if (method === "extractAndLoadChunks") logMessage += `([${args[0].map(arg => `"${arg}"`).join(", ")}], ${args[1].toString()})`;
else logMessage += `(${args.map(arg => `"${arg}"`).join(", ")})`;

console.log("[PUP_WEBPACK_FIND_FAIL]", logMessage);
}
}

setTimeout(() => console.log("[PUPPETEER_TEST_DONE_SIGNAL]"), 1000);
} catch (e) {
console.log("[PUP_DEBUG]", "A fatal error occurred:", e);
}
);
}

await page.evaluateOnNewDocument(`
Expand Down
Loading

0 comments on commit fa9f16a

Please sign in to comment.