Vencord plugin for the Discord desktop app. Toggles fire on key press, push-to-talk infers releases GNOME drops (re-registration probe), and GNOME's trigger descriptions are matched correctly. Includes basis-path tests, a patch checker against the live Discord bundle, the design spec and the README assets.
73 lines
4.1 KiB
JavaScript
73 lines
4.1 KiB
JavaScript
/*
|
|
* Vencord, a Discord client mod
|
|
* Copyright (c) 2026 Evandro
|
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
|
*/
|
|
|
|
// Applies this plugin's patches to a downloaded Discord bundle the way Vencord does:
|
|
// unique find, one match per replacement, patched module still parses.
|
|
// Usage: node check-patches.mjs [dir with discord.com/assets/*.js, optional lazy/ subdir]
|
|
// Without a dir it downloads the entry bundles that discord.com/app serves right now.
|
|
import { existsSync, mkdtempSync, readdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
import { createRequire } from "node:module";
|
|
import { tmpdir } from "node:os";
|
|
import { join } from "node:path";
|
|
|
|
import { PATCHES } from "./patches.ts";
|
|
|
|
// espree is ESLint's acorn-based parser, already installed with the repo
|
|
const { parse: espreeParse } = createRequire(createRequire(import.meta.url).resolve("eslint/package.json"))("espree");
|
|
const parse = (code, opts) => espreeParse(code, { ...opts, ecmaVersion: "latest" });
|
|
const dir = process.argv[2] ?? await downloadEntryBundles();
|
|
|
|
async function downloadEntryBundles() {
|
|
const out = mkdtempSync(join(tmpdir(), "discord-web-"));
|
|
const html = await (await fetch("https://discord.com/app")).text();
|
|
const scripts = [...html.matchAll(/src="(\/assets\/[^"]+\.js)"/g)].map(m => m[1]);
|
|
for (const src of scripts)
|
|
writeFileSync(join(out, src.split("/").pop()), await (await fetch("https://discord.com" + src)).text());
|
|
console.log(`baixados ${scripts.length} bundles de discord.com/app em ${out}`);
|
|
return out;
|
|
}
|
|
const SELF = 'Vencord.Plugins.plugins["WaylandKeybinds"]';
|
|
const canon = re => new RegExp(re.source.replaceAll(/(\\*)\\i/g, (m, esc) => esc.length % 2 === 0 ? esc + "(?:[A-Za-z_$][\\w$]*)" : m.slice(1)), re.flags);
|
|
|
|
const lazy = join(dir, "lazy");
|
|
const files = readdirSync(dir).filter(f => f.endsWith(".js")).map(f => join(dir, f))
|
|
.concat(existsSync(lazy) ? readdirSync(lazy).filter(f => f.endsWith(".js")).map(f => join(lazy, f)) : []);
|
|
let failed = false;
|
|
const fail = msg => { failed = true; console.log("FALHOU " + msg); };
|
|
const ok = msg => console.log("ok " + msg);
|
|
|
|
for (const patch of PATCHES) {
|
|
const hits = files.filter(f => readFileSync(f, "utf8").includes(patch.find));
|
|
if (hits.length !== 1) { fail(`find "${patch.find}" em ${hits.length} arquivos`); continue; }
|
|
const src = readFileSync(hits[0], "utf8");
|
|
const pos = src.indexOf(patch.find);
|
|
let module = null;
|
|
// rspack modules are methods of one big object: `123(e,t,n){...}`
|
|
const walk = node => {
|
|
if (!node || typeof node.type !== "string" || node.end < pos || node.start > pos) return;
|
|
if (node.type === "Property" && node.value?.type === "FunctionExpression" && /^\d+$/.test(String(node.key.value ?? node.key.name))) module = node;
|
|
for (const v of Object.values(node)) Array.isArray(v) ? v.forEach(walk) : walk(v);
|
|
};
|
|
walk(parse(src, { ecmaVersion: "latest", sourceType: "script" }));
|
|
if (!module) { fail(`módulo de "${patch.find}" não encontrado`); continue; }
|
|
ok(`find "${patch.find}" único → módulo ${module.key.value ?? module.key.name} (${hits[0].split("/").pop()})`);
|
|
if (src.indexOf(patch.find, pos + 1) !== -1 && src.indexOf(patch.find, pos + 1) < module.end) ok("find repete dentro do mesmo módulo (permitido)");
|
|
|
|
let code = "(function" + src.slice(module.value.start, module.value.end) + ")";
|
|
for (const { match, replace } of [patch.replacement].flat()) {
|
|
const re = canon(match);
|
|
const count = [...code.matchAll(new RegExp(re.source, re.flags.includes("g") ? re.flags : re.flags + "g"))].length;
|
|
if (count !== 1) { fail(`match ${match} casou ${count} vezes`); continue; }
|
|
const before = code;
|
|
code = code.replace(re, replace.replaceAll("$self", SELF));
|
|
const at = [...before].findIndex((c, i) => c !== code[i]);
|
|
ok(`match ${match} casou 1 vez\n → ${code.slice(Math.max(0, at - 20), at + 150)}`);
|
|
}
|
|
try { parse(code, { ecmaVersion: "latest" }); ok("módulo corrigido continua JavaScript válido"); }
|
|
catch (e) { fail("módulo corrigido não compila: " + e.message); }
|
|
}
|
|
process.exit(failed ? 1 : 0);
|