diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index c7a2f24e8..ba22b1230 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -32,7 +32,7 @@ jobs:
run: pnpm install --frozen-lockfile
- name: Build web
- run: pnpm buildWeb --standalone
+ run: pnpm buildWebStandalone
- name: Build
run: pnpm build --standalone
diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml
index 8407e08e2..190e3069c 100644
--- a/.github/workflows/publish.yml
+++ b/.github/workflows/publish.yml
@@ -32,7 +32,7 @@ jobs:
run: pnpm install --frozen-lockfile
- name: Build web
- run: pnpm buildWeb --standalone
+ run: pnpm buildWebStandalone
- name: Publish extension
run: |
diff --git a/.github/workflows/reportBrokenPlugins.yml b/.github/workflows/reportBrokenPlugins.yml
index d3a882fa3..a669c1a27 100644
--- a/.github/workflows/reportBrokenPlugins.yml
+++ b/.github/workflows/reportBrokenPlugins.yml
@@ -37,8 +37,8 @@ jobs:
with:
chrome-version: stable
- - name: Build web
- run: pnpm buildWeb --standalone --dev
+ - name: Build Vencord Reporter Version
+ run: pnpm buildReporter
- name: Create Report
timeout-minutes: 10
diff --git a/package.json b/package.json
index 29b1506e2..01fe3552b 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
{
"name": "vencord",
"private": "true",
- "version": "1.8.6",
+ "version": "1.8.8",
"description": "The cutest Discord client mod",
"homepage": "https://github.com/Vendicated/Vencord#readme",
"bugs": {
@@ -20,7 +20,11 @@
"build": "node --require=./scripts/suppressExperimentalWarnings.js scripts/build/build.mjs",
"buildStandalone": "pnpm build --standalone",
"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",
"generateTypes": "tspc --emitDeclarationOnly --declaration --outDir packages/vencord-types",
"inject": "node scripts/runInstaller.mjs",
@@ -103,6 +107,6 @@
},
"engines": {
"node": ">=18",
- "pnpm": ">=8"
+ "pnpm": ">=9"
}
}
diff --git a/scripts/build/build.mjs b/scripts/build/build.mjs
index 0c2a930a0..fcf56f66c 100755
--- a/scripts/build/build.mjs
+++ b/scripts/build/build.mjs
@@ -21,19 +21,21 @@ import esbuild from "esbuild";
import { readdir } from "fs/promises";
import { join } from "path";
-import { BUILD_TIMESTAMP, commonOpts, existsAsync, globPlugins, isDev, isStandalone, updaterDisabled, VERSION, watch } from "./common.mjs";
+import { BUILD_TIMESTAMP, commonOpts, exists, globPlugins, IS_DEV, IS_REPORTER, IS_STANDALONE, IS_UPDATER_DISABLED, VERSION, watch } from "./common.mjs";
const defines = {
- IS_STANDALONE: isStandalone,
- IS_DEV: JSON.stringify(isDev),
- IS_UPDATER_DISABLED: updaterDisabled,
+ IS_STANDALONE,
+ IS_DEV,
+ IS_REPORTER,
+ IS_UPDATER_DISABLED,
IS_WEB: false,
IS_EXTENSION: false,
VERSION: JSON.stringify(VERSION),
- BUILD_TIMESTAMP,
+ BUILD_TIMESTAMP
};
-if (defines.IS_STANDALONE === "false")
- // If this is a local build (not standalone), optimise
+
+if (defines.IS_STANDALONE === false)
+ // If this is a local build (not standalone), optimize
// for the specific platform we're on
defines["process.platform"] = JSON.stringify(process.platform);
@@ -46,7 +48,7 @@ const nodeCommonOpts = {
platform: "node",
target: ["esnext"],
external: ["electron", "original-fs", "~pluginNatives", ...commonOpts.external],
- define: defines,
+ define: defines
};
const sourceMapFooter = s => watch ? "" : `//# sourceMappingURL=vencord://${s}.js.map`;
@@ -73,13 +75,13 @@ const globNativesPlugin = {
let i = 0;
for (const dir of pluginDirs) {
const dirPath = join("src", dir);
- if (!await existsAsync(dirPath)) continue;
+ if (!await exists(dirPath)) continue;
const plugins = await readdir(dirPath);
for (const p of plugins) {
const nativePath = join(dirPath, p, "native.ts");
const indexNativePath = join(dirPath, p, "native/index.ts");
- if (!(await existsAsync(nativePath)) && !(await existsAsync(indexNativePath)))
+ if (!(await exists(nativePath)) && !(await exists(indexNativePath)))
continue;
const nameParts = p.split(".");
diff --git a/scripts/build/buildWeb.mjs b/scripts/build/buildWeb.mjs
index b4c726064..bc15ccced 100644
--- a/scripts/build/buildWeb.mjs
+++ b/scripts/build/buildWeb.mjs
@@ -23,7 +23,7 @@ import { appendFile, mkdir, readdir, readFile, rm, writeFile } from "fs/promises
import { join } from "path";
import Zip from "zip-local";
-import { BUILD_TIMESTAMP, commonOpts, globPlugins, isDev, VERSION } from "./common.mjs";
+import { BUILD_TIMESTAMP, commonOpts, globPlugins, IS_DEV, IS_REPORTER, VERSION } from "./common.mjs";
/**
* @type {esbuild.BuildOptions}
@@ -33,22 +33,23 @@ const commonOptions = {
entryPoints: ["browser/Vencord.ts"],
globalName: "Vencord",
format: "iife",
- external: ["plugins", "git-hash", "/assets/*"],
+ external: ["~plugins", "~git-hash", "/assets/*"],
plugins: [
globPlugins("web"),
...commonOpts.plugins,
],
target: ["esnext"],
define: {
- IS_WEB: "true",
- IS_EXTENSION: "false",
- IS_STANDALONE: "true",
- IS_DEV: JSON.stringify(isDev),
- IS_DISCORD_DESKTOP: "false",
- IS_VESKTOP: "false",
- IS_UPDATER_DISABLED: "true",
+ IS_WEB: true,
+ IS_EXTENSION: false,
+ IS_STANDALONE: true,
+ IS_DEV,
+ IS_REPORTER,
+ IS_DISCORD_DESKTOP: false,
+ IS_VESKTOP: false,
+ IS_UPDATER_DISABLED: true,
VERSION: JSON.stringify(VERSION),
- BUILD_TIMESTAMP,
+ BUILD_TIMESTAMP
}
};
@@ -87,16 +88,16 @@ await Promise.all(
esbuild.build({
...commonOptions,
outfile: "dist/browser.js",
- footer: { js: "//# sourceURL=VencordWeb" },
+ footer: { js: "//# sourceURL=VencordWeb" }
}),
esbuild.build({
...commonOptions,
outfile: "dist/extension.js",
define: {
...commonOptions?.define,
- IS_EXTENSION: "true",
+ IS_EXTENSION: true,
},
- footer: { js: "//# sourceURL=VencordWeb" },
+ footer: { js: "//# sourceURL=VencordWeb" }
}),
esbuild.build({
...commonOptions,
@@ -112,7 +113,7 @@ await Promise.all(
footer: {
// UserScripts get wrapped in an iife, so define Vencord prop on window that returns our local
js: "Object.defineProperty(unsafeWindow,'Vencord',{get:()=>Vencord});"
- },
+ }
})
]
);
@@ -165,7 +166,7 @@ async function buildExtension(target, files) {
f.startsWith("manifest") ? "manifest.json" : f,
content
];
- }))),
+ })))
};
await rm(target, { recursive: true, force: true });
@@ -192,14 +193,19 @@ const appendCssRuntime = readFile("dist/Vencord.user.css", "utf-8").then(content
return appendFile("dist/Vencord.user.js", cssRuntime);
});
-await Promise.all([
- appendCssRuntime,
- buildExtension("chromium-unpacked", ["modifyResponseHeaders.json", "content.js", "manifest.json", "icon.png"]),
- buildExtension("firefox-unpacked", ["background.js", "content.js", "manifestv2.json", "icon.png"]),
-]);
+if (!process.argv.includes("--skip-extension")) {
+ await Promise.all([
+ appendCssRuntime,
+ buildExtension("chromium-unpacked", ["modifyResponseHeaders.json", "content.js", "manifest.json", "icon.png"]),
+ buildExtension("firefox-unpacked", ["background.js", "content.js", "manifestv2.json", "icon.png"]),
+ ]);
-Zip.sync.zip("dist/chromium-unpacked").compress().save("dist/extension-chrome.zip");
-console.info("Packed Chromium Extension written to dist/extension-chrome.zip");
+ Zip.sync.zip("dist/chromium-unpacked").compress().save("dist/extension-chrome.zip");
+ console.info("Packed Chromium Extension written to dist/extension-chrome.zip");
-Zip.sync.zip("dist/firefox-unpacked").compress().save("dist/extension-firefox.zip");
-console.info("Packed Firefox Extension written to dist/extension-firefox.zip");
+ Zip.sync.zip("dist/firefox-unpacked").compress().save("dist/extension-firefox.zip");
+ console.info("Packed Firefox Extension written to dist/extension-firefox.zip");
+
+} else {
+ await appendCssRuntime;
+}
diff --git a/scripts/build/common.mjs b/scripts/build/common.mjs
index 3b1473e1c..cdbb26eec 100644
--- a/scripts/build/common.mjs
+++ b/scripts/build/common.mjs
@@ -35,24 +35,26 @@ const PackageJSON = JSON.parse(readFileSync("package.json"));
export const VERSION = PackageJSON.version;
// https://reproducible-builds.org/docs/source-date-epoch/
export const BUILD_TIMESTAMP = Number(process.env.SOURCE_DATE_EPOCH) || Date.now();
+
export const watch = process.argv.includes("--watch");
-export const isDev = watch || process.argv.includes("--dev");
-export const isStandalone = JSON.stringify(process.argv.includes("--standalone"));
-export const updaterDisabled = JSON.stringify(process.argv.includes("--disable-updater"));
+export const IS_DEV = watch || process.argv.includes("--dev");
+export const IS_REPORTER = process.argv.includes("--reporter");
+export const IS_STANDALONE = process.argv.includes("--standalone");
+
+export const IS_UPDATER_DISABLED = process.argv.includes("--disable-updater");
export const gitHash = process.env.VENCORD_HASH || execSync("git rev-parse --short HEAD", { encoding: "utf-8" }).trim();
+
export const banner = {
js: `
// Vencord ${gitHash}
-// Standalone: ${isStandalone}
-// Platform: ${isStandalone === "false" ? process.platform : "Universal"}
-// Updater disabled: ${updaterDisabled}
+// Standalone: ${IS_STANDALONE}
+// Platform: ${IS_STANDALONE === false ? process.platform : "Universal"}
+// Updater Disabled: ${IS_UPDATER_DISABLED}
`.trim()
};
-const isWeb = process.argv.slice(0, 2).some(f => f.endsWith("buildWeb.mjs"));
-
-export function existsAsync(path) {
- return access(path, FsConstants.F_OK)
+export async function exists(path) {
+ return await access(path, FsConstants.F_OK)
.then(() => true)
.catch(() => false);
}
@@ -66,7 +68,7 @@ export const makeAllPackagesExternalPlugin = {
setup(build) {
const filter = /^[^./]|^\.[^./]|^\.\.[^/]/; // Must not start with "/" or "./" or "../"
build.onResolve({ filter }, args => ({ path: args.path, external: true }));
- },
+ }
};
/**
@@ -89,14 +91,14 @@ export const globPlugins = kind => ({
let plugins = "\n";
let i = 0;
for (const dir of pluginDirs) {
- if (!await existsAsync(`./src/${dir}`)) continue;
+ if (!await exists(`./src/${dir}`)) continue;
const files = await readdir(`./src/${dir}`);
for (const file of files) {
if (file.startsWith("_") || file.startsWith(".")) continue;
if (file === "index.ts") continue;
const target = getPluginTarget(file);
- if (target) {
+ if (target && !IS_REPORTER) {
if (target === "dev" && !watch) continue;
if (target === "web" && kind === "discordDesktop") continue;
if (target === "desktop" && kind === "web") continue;
@@ -178,7 +180,7 @@ export const fileUrlPlugin = {
build.onLoad({ filter, namespace: "file-uri" }, async ({ pluginData: { path, uri } }) => {
const { searchParams } = new URL(uri);
const base64 = searchParams.has("base64");
- const minify = isStandalone === "true" && searchParams.has("minify");
+ const minify = IS_STANDALONE === true && searchParams.has("minify");
const noTrim = searchParams.get("trim") === "false";
const encoding = base64 ? "base64" : "utf-8";
diff --git a/scripts/generateReport.ts b/scripts/generateReport.ts
index 8bb87d812..cf4210779 100644
--- a/scripts/generateReport.ts
+++ b/scripts/generateReport.ts
@@ -16,6 +16,8 @@
* along with this program. If not, see .
*/
+/* eslint-disable no-fallthrough */
+
// eslint-disable-next-line spaced-comment
///
// eslint-disable-next-line spaced-comment
@@ -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)?.getProperty("message")
- .then(m => m.jsonValue());
+async function maybeGetError(handle: JSHandle): Promise {
+ return await (handle as JSHandle)?.getProperty("message")
+ .then(m => m?.jsonValue());
}
const report = {
@@ -59,6 +62,7 @@ const report = {
error: string;
}[],
otherErrors: [] as string[],
+ ignoredErrors: [] as string[],
badWebpackFinds: [] as string[]
};
@@ -106,15 +110,6 @@ async function printReport() {
console.log();
- const ignoredErrors = [] as string[];
- report.otherErrors = report.otherErrors.filter(e => {
- if (IGNORED_DISCORD_ERRORS.some(regex => e.match(regex))) {
- ignoredErrors.push(e);
- return false;
- }
- return true;
- });
-
console.log("## Discord Errors");
report.otherErrors.forEach(e => {
console.log(`- ${toCodeBlock(e)}`);
@@ -123,7 +118,7 @@ async function printReport() {
console.log();
console.log("## Ignored Discord Errors");
- ignoredErrors.forEach(e => {
+ report.ignoredErrors.forEach(e => {
console.log(`- ${toCodeBlock(e)}`);
});
@@ -188,61 +183,6 @@ 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();
- }
-
- 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);
- }
-
- if (isVencord) {
- const args = await Promise.all(e.args().map(a => a.jsonValue()));
-
- const [, tag, message] = args as Array;
- const cause = await maybeGetError(e.args()[3]);
-
- switch (tag) {
- case "WebpackInterceptor:":
- const patchFailMatch = message.match(/Patch by (.+?) (had no effect|errored|found no module) \(Module id is (.+?)\): (.+)/)!;
- if (!patchFailMatch) break;
-
- process.exitCode = 1;
-
- const [, plugin, type, id, regex] = patchFailMatch;
- report.badPatches.push({
- plugin,
- type,
- id,
- match: regex.replace(/\[A-Za-z_\$\]\[\\w\$\]\*/g, "\\i"),
- error: cause
- });
-
- break;
- case "PluginManager:":
- const failedToStartMatch = message.match(/Failed to start (.+)/);
- if (!failedToStartMatch) break;
-
- process.exitCode = 1;
-
- const [, name] = failedToStartMatch;
- report.badStarts.push({
- plugin: name,
- error: cause
- });
-
- break;
- }
- }
-
async function getText() {
try {
return await Promise.all(
@@ -255,299 +195,114 @@ page.on("console", async e => {
}
}
- if (isDebug) {
- const text = await getText();
+ const firstArg = await rawArgs[0]?.jsonValue();
- console.error(text);
- if (text.includes("A fatal error occurred:")) {
- process.exit(1);
+ const isVencord = firstArg === "[Vencord]";
+ const isDebug = firstArg === "[PUP_DEBUG]";
+
+ outer:
+ if (isVencord) {
+ try {
+ var args = await Promise.all(e.args().map(a => a.jsonValue()));
+ } catch {
+ break outer;
}
+
+ const [, tag, message, otherMessage] = args as Array;
+
+ switch (tag) {
+ case "WebpackInterceptor:":
+ const patchFailMatch = message.match(/Patch by (.+?) (had no effect|errored|found no module) \(Module id is (.+?)\): (.+)/)!;
+ if (!patchFailMatch) break;
+
+ console.error(await getText());
+ process.exitCode = 1;
+
+ const [, plugin, type, id, regex] = patchFailMatch;
+ report.badPatches.push({
+ plugin,
+ type,
+ id,
+ match: regex.replace(/\[A-Za-z_\$\]\[\\w\$\]\*/g, "\\i"),
+ error: await maybeGetError(e.args()[3])
+ });
+
+ break;
+ case "PluginManager:":
+ const failedToStartMatch = message.match(/Failed to start (.+)/);
+ if (!failedToStartMatch) break;
+
+ console.error(await getText());
+ process.exitCode = 1;
+
+ const [, name] = failedToStartMatch;
+ report.badStarts.push({
+ plugin: name,
+ error: await maybeGetError(e.args()[3]) ?? "Unknown error"
+ });
+
+ break;
+ case "LazyChunkLoader:":
+ console.error(await getText());
+
+ switch (message) {
+ case "A fatal error occurred:":
+ process.exit(1);
+ }
+
+ break;
+ case "Reporter:":
+ console.error(await getText());
+
+ switch (message) {
+ case "A fatal error occurred:":
+ process.exit(1);
+ case "Webpack Find Fail:":
+ process.exitCode = 1;
+ report.badWebpackFinds.push(otherMessage);
+ break;
+ case "Finished test":
+ await browser.close();
+ await printReport();
+ process.exit();
+ }
+ }
+ }
+
+ if (isDebug) {
+ console.error(await getText());
} else if (level === "error") {
const text = await getText();
if (text.length && !text.startsWith("Failed to load resource: the server responded with a status of") && !text.includes("Webpack")) {
- console.error("[Unexpected Error]", text);
- report.otherErrors.push(text);
+ if (IGNORED_DISCORD_ERRORS.some(regex => text.match(regex))) {
+ report.ignoredErrors.push(text);
+ } else {
+ console.error("[Unexpected Error]", text);
+ report.otherErrors.push(text);
+ }
}
}
});
-page.on("error", e => console.error("[Error]", e));
-page.on("pageerror", e => console.error("[Page Error]", e));
+page.on("error", e => console.error("[Error]", e.message));
+page.on("pageerror", e => console.error("[Page Error]", e.message));
-await page.setBypassCSP(true);
-
-async function runtime(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"];
- },
- });
-
- // Monkey patch Logger to not log with custom css
- // @ts-ignore
- const originalLog = Vencord.Util.Logger.prototype._log;
- // @ts-ignore
- Vencord.Util.Logger.prototype._log = function (level, levelColor, args) {
- if (level === "warn" || level === "error")
- return console[level]("[Vencord]", this.name + ":", ...args);
-
- return originalLog.call(this, level, levelColor, 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;
-
- Vencord.Settings.plugins[p.name].enabled = true;
- p.patches?.forEach(patch => {
- patch.plugin = p.name;
- delete patch.predicate;
- delete patch.group;
-
- Vencord.Util.canonicalizeFind(patch);
- if (!Array.isArray(patch.replacement)) {
- patch.replacement = [patch.replacement];
- }
-
- patch.replacement.forEach(r => {
- delete r.predicate;
- });
-
- Vencord.Plugins.patches.push(patch);
- });
- });
-
- let wreq: typeof Vencord.Webpack.wreq;
-
- const { canonicalizeMatch, Logger } = Vencord.Util;
-
- const validChunks = new Set();
- const invalidChunks = new Set();
- const deferredRequires = new Set();
-
- let chunksSearchingResolve: (value: void | PromiseLike) => void;
- const chunksSearchingDone = new Promise(r => chunksSearchingResolve = r);
-
- // True if resolved, false otherwise
- const chunksSearchPromises = [] as Array<() => boolean>;
-
- const LazyChunkRegex = canonicalizeMatch(/(?:Promise\.all\(\[(\i\.\i\("[^)]+?"\)[^\]]+?)\]\)|(\i\.\i\("[^)]+?"\)))\.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 ([, rawChunkIdsArray, rawChunkIdsSingle, entryPoint]) => {
- const rawChunkIds = rawChunkIdsArray ?? rawChunkIdsSingle;
- 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);
+async function reporterRuntime(token: string) {
+ Vencord.Webpack.waitFor(
+ "loginToken",
+ m => {
+ console.log("[PUP_DEBUG]", "Logging in with token...");
+ m.loginToken(token);
}
-
- 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);
- }
-
- 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;
-
- const module = Vencord.Webpack.findModuleFactory(...code);
- if (module) result = module.toString().match(canonicalizeMatch(matcher));
- } else {
- // @ts-ignore
- result = Vencord.Webpack[method](...args);
- }
-
- if (result == null || ("$$vencordInternal" in result && 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(`
- ${readFileSync("./dist/browser.js", "utf-8")}
-
- ;(${runtime.toString()})(${JSON.stringify(process.env.DISCORD_TOKEN)});
+ if (location.host.endsWith("discord.com")) {
+ ${readFileSync("./dist/browser.js", "utf-8")};
+ (${reporterRuntime.toString()})(${JSON.stringify(process.env.DISCORD_TOKEN)});
+ }
`);
await page.goto(CANARY ? "https://canary.discord.com/login" : "https://discord.com/login");
diff --git a/src/Vencord.ts b/src/Vencord.ts
index 72541148e..c4c6d4705 100644
--- a/src/Vencord.ts
+++ b/src/Vencord.ts
@@ -42,6 +42,10 @@ import { checkForUpdates, update, UpdateLogger } from "./utils/updater";
import { onceReady } from "./webpack";
import { SettingsRouter } from "./webpack/common";
+if (IS_REPORTER) {
+ require("./debug/runReporter");
+}
+
async function syncSettings() {
// pre-check for local shared settings
if (
diff --git a/src/api/DataStore/index.ts b/src/api/DataStore/index.ts
index 97f43edd6..47ae39dbd 100644
--- a/src/api/DataStore/index.ts
+++ b/src/api/DataStore/index.ts
@@ -49,7 +49,7 @@ let defaultGetStoreFunc: UseStore | undefined;
function defaultGetStore() {
if (!defaultGetStoreFunc) {
- defaultGetStoreFunc = createStore("VencordData", "VencordStore");
+ defaultGetStoreFunc = createStore(!IS_REPORTER ? "VencordData" : "VencordDataReporter", "VencordStore");
}
return defaultGetStoreFunc;
}
diff --git a/src/api/MessageUpdater.ts b/src/api/MessageUpdater.ts
new file mode 100644
index 000000000..5cac80528
--- /dev/null
+++ b/src/api/MessageUpdater.ts
@@ -0,0 +1,29 @@
+/*
+ * Vencord, a Discord client mod
+ * Copyright (c) 2024 Vendicated and contributors
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+import { MessageCache, MessageStore } from "@webpack/common";
+import { FluxStore } from "@webpack/types";
+import { Message } from "discord-types/general";
+
+/**
+ * Update and re-render a message
+ * @param channelId The channel id of the message
+ * @param messageId The message id
+ * @param fields The fields of the message to change. Leave empty if you just want to re-render
+ */
+export function updateMessage(channelId: string, messageId: string, fields?: Partial) {
+ const channelMessageCache = MessageCache.getOrCreate(channelId);
+ if (!channelMessageCache.has(messageId)) return;
+
+ // To cause a message to re-render, we basically need to create a new instance of the message and obtain a new reference
+ // If we have fields to modify we can use the merge method of the class, otherwise we just create a new instance with the old fields
+ const newChannelMessageCache = channelMessageCache.update(messageId, (oldMessage: any) => {
+ return fields ? oldMessage.merge(fields) : new oldMessage.constructor(oldMessage);
+ });
+
+ MessageCache.commit(newChannelMessageCache);
+ (MessageStore as unknown as FluxStore).emitChange();
+}
diff --git a/src/api/Notifications/NotificationComponent.tsx b/src/api/Notifications/NotificationComponent.tsx
index caa4b64ef..d07143c45 100644
--- a/src/api/Notifications/NotificationComponent.tsx
+++ b/src/api/Notifications/NotificationComponent.tsx
@@ -113,7 +113,7 @@ export default ErrorBoundary.wrap(function NotificationComponent({
{timeout !== 0 && !permanent && (
)}
diff --git a/src/api/Settings.ts b/src/api/Settings.ts
index 490e6ef7f..70ba0bd4a 100644
--- a/src/api/Settings.ts
+++ b/src/api/Settings.ts
@@ -106,7 +106,7 @@ const DefaultSettings: Settings = {
}
};
-const settings = VencordNative.settings.get();
+const settings = !IS_REPORTER ? VencordNative.settings.get() : {} as Settings;
mergeDefaults(settings, DefaultSettings);
const saveSettingsOnFrequentAction = debounce(async () => {
@@ -129,7 +129,7 @@ export const SettingsStore = new SettingsStoreClass(settings, {
if (path === "plugins" && key in plugins)
return target[key] = {
- enabled: plugins[key].required ?? plugins[key].enabledByDefault ?? false
+ enabled: IS_REPORTER ?? plugins[key].required ?? plugins[key].enabledByDefault ?? false
};
// Since the property is not set, check if this is a plugin's setting and if so, try to resolve
@@ -156,12 +156,14 @@ export const SettingsStore = new SettingsStoreClass(settings, {
}
});
-SettingsStore.addGlobalChangeListener((_, path) => {
- SettingsStore.plain.cloud.settingsSyncVersion = Date.now();
- localStorage.Vencord_settingsDirty = true;
- saveSettingsOnFrequentAction();
- VencordNative.settings.set(SettingsStore.plain, path);
-});
+if (!IS_REPORTER) {
+ SettingsStore.addGlobalChangeListener((_, path) => {
+ SettingsStore.plain.cloud.settingsSyncVersion = Date.now();
+ localStorage.Vencord_settingsDirty = true;
+ saveSettingsOnFrequentAction();
+ VencordNative.settings.set(SettingsStore.plain, path);
+ });
+}
/**
* Same as {@link Settings} but unproxied. You should treat this as readonly,
diff --git a/src/api/index.ts b/src/api/index.ts
index 5dca63105..02c70008a 100644
--- a/src/api/index.ts
+++ b/src/api/index.ts
@@ -26,6 +26,7 @@ import * as $MessageAccessories from "./MessageAccessories";
import * as $MessageDecorations from "./MessageDecorations";
import * as $MessageEventsAPI from "./MessageEvents";
import * as $MessagePopover from "./MessagePopover";
+import * as $MessageUpdater from "./MessageUpdater";
import * as $Notices from "./Notices";
import * as $Notifications from "./Notifications";
import * as $ServerList from "./ServerList";
@@ -110,3 +111,8 @@ export const ContextMenu = $ContextMenu;
* An API allowing you to add buttons to the chat input
*/
export const ChatButtons = $ChatButtons;
+
+/**
+ * An API allowing you to update and re-render messages
+ */
+export const MessageUpdater = $MessageUpdater;
diff --git a/src/debug/Tracer.ts b/src/debug/Tracer.ts
index 4337e0019..7d80f425c 100644
--- a/src/debug/Tracer.ts
+++ b/src/debug/Tracer.ts
@@ -18,14 +18,14 @@
import { Logger } from "@utils/Logger";
-if (IS_DEV) {
+if (IS_DEV || IS_REPORTER) {
var traces = {} as Record;
var logger = new Logger("Tracer", "#FFD166");
}
const noop = function () { };
-export const beginTrace = !IS_DEV ? noop :
+export const beginTrace = !(IS_DEV || IS_REPORTER) ? noop :
function beginTrace(name: string, ...args: any[]) {
if (name in traces)
throw new Error(`Trace ${name} already exists!`);
@@ -33,7 +33,7 @@ export const beginTrace = !IS_DEV ? noop :
traces[name] = [performance.now(), args];
};
-export const finishTrace = !IS_DEV ? noop : function finishTrace(name: string) {
+export const finishTrace = !(IS_DEV || IS_REPORTER) ? noop : function finishTrace(name: string) {
const end = performance.now();
const [start, args] = traces[name];
@@ -48,7 +48,7 @@ type TraceNameMapper = (...args: Parameters) => string;
const noopTracer =
(name: string, f: F, mapper?: TraceNameMapper) => f;
-export const traceFunction = !IS_DEV
+export const traceFunction = !(IS_DEV || IS_REPORTER)
? noopTracer
: function traceFunction(name: string, f: F, mapper?: TraceNameMapper): F {
return function (this: any, ...args: Parameters) {
diff --git a/src/debug/loadLazyChunks.ts b/src/debug/loadLazyChunks.ts
new file mode 100644
index 000000000..d8f84335c
--- /dev/null
+++ b/src/debug/loadLazyChunks.ts
@@ -0,0 +1,167 @@
+/*
+ * Vencord, a Discord client mod
+ * Copyright (c) 2024 Vendicated and contributors
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+import { Logger } from "@utils/Logger";
+import { canonicalizeMatch } from "@utils/patches";
+import * as Webpack from "@webpack";
+import { wreq } from "@webpack";
+
+const LazyChunkLoaderLogger = new Logger("LazyChunkLoader");
+
+export async function loadLazyChunks() {
+ try {
+ LazyChunkLoaderLogger.log("Loading all chunks...");
+
+ const validChunks = new Set();
+ const invalidChunks = new Set();
+ const deferredRequires = new Set();
+
+ let chunksSearchingResolve: (value: void | PromiseLike) => void;
+ const chunksSearchingDone = new Promise(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(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 => (IS_WEB && t.includes(".module.wasm")) || !t.includes("(this.webpackChunkdiscord_app=this.webpackChunkdiscord_app||[]).push"));
+
+ if (isWasm && IS_WEB) {
+ 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);
+ }
+
+ Webpack.factoryListeners.add(factory => {
+ let isResolved = false;
+ searchAndLoadLazyChunks(factory.toString()).then(() => isResolved = true);
+
+ chunksSearchPromises.push(() => isResolved);
+ });
+
+ for (const factoryId in wreq.m) {
+ let isResolved = false;
+ searchAndLoadLazyChunks(wreq.m[factoryId].toString()).then(() => isResolved = true);
+
+ chunksSearchPromises.push(() => isResolved);
+ }
+
+ 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);
+ }
+
+ 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 => (IS_WEB && 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);
+ }
+ }));
+
+ LazyChunkLoaderLogger.log("Finished loading all chunks!");
+ } catch (e) {
+ LazyChunkLoaderLogger.log("A fatal error occurred:", e);
+ }
+}
diff --git a/src/debug/runReporter.ts b/src/debug/runReporter.ts
new file mode 100644
index 000000000..6c7a2a03f
--- /dev/null
+++ b/src/debug/runReporter.ts
@@ -0,0 +1,75 @@
+/*
+ * Vencord, a Discord client mod
+ * Copyright (c) 2024 Vendicated and contributors
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+import { Logger } from "@utils/Logger";
+import * as Webpack from "@webpack";
+import { patches } from "plugins";
+
+import { loadLazyChunks } from "./loadLazyChunks";
+
+const ReporterLogger = new Logger("Reporter");
+
+async function runReporter() {
+ try {
+ ReporterLogger.log("Starting test...");
+
+ let loadLazyChunksResolve: (value: void | PromiseLike) => void;
+ const loadLazyChunksDone = new Promise(r => loadLazyChunksResolve = r);
+
+ Webpack.beforeInitListeners.add(() => loadLazyChunks().then((loadLazyChunksResolve)));
+ await loadLazyChunksDone;
+
+ for (const patch of 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 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 Webpack.extractAndLoadChunks(code, matcher);
+ if (result === false) result = null;
+ } else {
+ // @ts-ignore
+ result = 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(", ")})`;
+
+ ReporterLogger.log("Webpack Find Fail:", logMessage);
+ }
+ }
+
+ ReporterLogger.log("Finished test");
+ } catch (e) {
+ ReporterLogger.log("A fatal error occurred:", e);
+ }
+}
+
+runReporter();
diff --git a/src/globals.d.ts b/src/globals.d.ts
index 94b5f15e8..e20ca4b71 100644
--- a/src/globals.d.ts
+++ b/src/globals.d.ts
@@ -34,9 +34,10 @@ declare global {
*/
export var IS_WEB: boolean;
export var IS_EXTENSION: boolean;
- export var IS_DEV: boolean;
export var IS_STANDALONE: boolean;
export var IS_UPDATER_DISABLED: boolean;
+ export var IS_DEV: boolean;
+ export var IS_REPORTER: boolean;
export var IS_DISCORD_DESKTOP: boolean;
export var IS_VESKTOP: boolean;
export var VERSION: string;
diff --git a/src/main/patcher.ts b/src/main/patcher.ts
index f0e3c3ab4..a3725ef9b 100644
--- a/src/main/patcher.ts
+++ b/src/main/patcher.ts
@@ -140,8 +140,14 @@ if (!IS_VANILLA) {
return originalAppend.apply(this, args);
};
+ // disable renderer backgrounding to prevent the app from unloading when in the background
+ // https://github.com/electron/electron/issues/2822
+ // https://github.com/GoogleChrome/chrome-launcher/blob/5a27dd574d47a75fec0fb50f7b774ebf8a9791ba/docs/chrome-flags-for-tools.md#task-throttling
// Work around discord unloading when in background
+ // Discord also recently started adding these flags but only on windows for some reason dunno why, it happens on Linux too
app.commandLine.appendSwitch("disable-renderer-backgrounding");
+ app.commandLine.appendSwitch("disable-background-timer-throttling");
+ app.commandLine.appendSwitch("disable-backgrounding-occluded-windows");
} else {
console.log("[Vencord] Running in vanilla mode. Not loading Vencord");
}
diff --git a/src/main/updater/index.ts b/src/main/updater/index.ts
index 32d5cd663..539b02a48 100644
--- a/src/main/updater/index.ts
+++ b/src/main/updater/index.ts
@@ -17,4 +17,4 @@
*/
if (!IS_UPDATER_DISABLED)
- import(IS_STANDALONE ? "./http" : "./git");
+ require(IS_STANDALONE ? "./http" : "./git");
diff --git a/src/plugins/_api/messageUpdater.ts b/src/plugins/_api/messageUpdater.ts
new file mode 100644
index 000000000..8f6cca26a
--- /dev/null
+++ b/src/plugins/_api/messageUpdater.ts
@@ -0,0 +1,37 @@
+/*
+ * 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 .
+*/
+
+import { Devs } from "@utils/constants";
+import definePlugin from "@utils/types";
+
+export default definePlugin({
+ name: "MessageUpdaterAPI",
+ description: "API for updating and re-rendering messages.",
+ authors: [Devs.Nuckyz],
+
+ patches: [
+ {
+ // Message accessories have a custom logic to decide if they should render again, so we need to make it not ignore changed message reference
+ find: "}renderEmbeds(",
+ replacement: {
+ match: /(?<=this.props,\i,\[)"message",/,
+ replace: ""
+ }
+ }
+ ]
+});
diff --git a/src/plugins/_core/supportHelper.tsx b/src/plugins/_core/supportHelper.tsx
index c7377833a..d59d82afc 100644
--- a/src/plugins/_core/supportHelper.tsx
+++ b/src/plugins/_core/supportHelper.tsx
@@ -65,7 +65,7 @@ export default definePlugin({
commands: [{
name: "vencord-debug",
description: "Send Vencord Debug info",
- predicate: ctx => AllowedChannelIds.includes(ctx.channel.id),
+ predicate: ctx => isPluginDev(UserStore.getCurrentUser()?.id) || AllowedChannelIds.includes(ctx.channel.id),
async execute() {
const { RELEASE_CHANNEL } = window.GLOBAL_ENV;
diff --git a/src/plugins/arRPC.web/index.tsx b/src/plugins/arRPC.web/index.tsx
index 423dce9b5..e41e8675e 100644
--- a/src/plugins/arRPC.web/index.tsx
+++ b/src/plugins/arRPC.web/index.tsx
@@ -19,7 +19,7 @@
import { popNotice, showNotice } from "@api/Notices";
import { Link } from "@components/Link";
import { Devs } from "@utils/constants";
-import definePlugin from "@utils/types";
+import definePlugin, { ReporterTestable } from "@utils/types";
import { findByPropsLazy } from "@webpack";
import { ApplicationAssetUtils, FluxDispatcher, Forms, Toasts } from "@webpack/common";
@@ -41,6 +41,7 @@ export default definePlugin({
name: "WebRichPresence (arRPC)",
description: "Client plugin for arRPC to enable RPC on Discord Web (experimental)",
authors: [Devs.Ducko],
+ reporterTestable: ReporterTestable.None,
settingsAboutComponent: () => (
<>
diff --git a/src/plugins/consoleShortcuts/index.ts b/src/plugins/consoleShortcuts/index.ts
index ee86b5fcf..0a1323e75 100644
--- a/src/plugins/consoleShortcuts/index.ts
+++ b/src/plugins/consoleShortcuts/index.ts
@@ -25,6 +25,7 @@ import definePlugin, { PluginNative, StartAt } from "@utils/types";
import * as Webpack from "@webpack";
import { extract, filters, findAll, findModuleId, search } from "@webpack";
import * as Common from "@webpack/common";
+import { loadLazyChunks } from "debug/loadLazyChunks";
import type { ComponentType } from "react";
const DESKTOP_ONLY = (f: string) => () => {
@@ -82,6 +83,7 @@ function makeShortcuts() {
wpsearch: search,
wpex: extract,
wpexs: (code: string) => extract(findModuleId(code)!),
+ loadLazyChunks: IS_DEV ? loadLazyChunks : () => { throw new Error("loadLazyChunks is dev only."); },
find,
findAll: findAll,
findByProps,
diff --git a/src/plugins/devCompanion.dev/index.tsx b/src/plugins/devCompanion.dev/index.tsx
index 25fd563e4..a495907b2 100644
--- a/src/plugins/devCompanion.dev/index.tsx
+++ b/src/plugins/devCompanion.dev/index.tsx
@@ -21,7 +21,7 @@ import { definePluginSettings } from "@api/Settings";
import { Devs } from "@utils/constants";
import { Logger } from "@utils/Logger";
import { canonicalizeMatch, canonicalizeReplace } from "@utils/patches";
-import definePlugin, { OptionType } from "@utils/types";
+import definePlugin, { OptionType, ReporterTestable } from "@utils/types";
import { filters, findAll, search } from "@webpack";
const PORT = 8485;
@@ -243,6 +243,7 @@ export default definePlugin({
name: "DevCompanion",
description: "Dev Companion Plugin",
authors: [Devs.Ven],
+ reporterTestable: ReporterTestable.None,
settings,
toolboxActions: {
diff --git a/src/plugins/fakeNitro/index.tsx b/src/plugins/fakeNitro/index.tsx
index 427f36ce8..737406cf5 100644
--- a/src/plugins/fakeNitro/index.tsx
+++ b/src/plugins/fakeNitro/index.tsx
@@ -333,7 +333,7 @@ export default definePlugin({
]
},
{
- find: "renderEmbeds(",
+ find: "}renderEmbeds(",
replacement: [
{
// Call our function to decide whether the embed should be ignored or not
diff --git a/src/plugins/friendsSince/index.tsx b/src/plugins/friendsSince/index.tsx
index eb59fff25..58014f362 100644
--- a/src/plugins/friendsSince/index.tsx
+++ b/src/plugins/friendsSince/index.tsx
@@ -36,7 +36,7 @@ export default definePlugin({
{
find: ".UserPopoutUpsellSource.PROFILE_PANEL,",
replacement: {
- match: /\i.default,\{userId:(\i)}\)/,
+ match: /\i.default,\{userId:([^,]+?)}\)/,
replace: "$&,$self.friendsSince({ userId: $1 })"
}
},
diff --git a/src/plugins/index.ts b/src/plugins/index.ts
index a434b4a6f..32bfe7e97 100644
--- a/src/plugins/index.ts
+++ b/src/plugins/index.ts
@@ -21,7 +21,7 @@ import { addContextMenuPatch, removeContextMenuPatch } from "@api/ContextMenu";
import { Settings } from "@api/Settings";
import { Logger } from "@utils/Logger";
import { canonicalizeFind } from "@utils/patches";
-import { Patch, Plugin, StartAt } from "@utils/types";
+import { Patch, Plugin, ReporterTestable, StartAt } from "@utils/types";
import { FluxDispatcher } from "@webpack/common";
import { FluxEvents } from "@webpack/types";
@@ -39,6 +39,7 @@ export const patches = [] as Patch[];
let enabledPluginsSubscribedFlux = false;
const subscribedFluxEventsPlugins = new Set();
+const pluginsValues = Object.values(Plugins);
const settings = Settings.plugins;
export function isPluginEnabled(p: string) {
@@ -49,25 +50,56 @@ export function isPluginEnabled(p: string) {
) ?? false;
}
-const pluginsValues = Object.values(Plugins);
+export function addPatch(newPatch: Omit, pluginName: string) {
+ const patch = newPatch as Patch;
+ patch.plugin = pluginName;
-// First roundtrip to mark and force enable dependencies (only for enabled plugins)
+ if (IS_REPORTER) {
+ delete patch.predicate;
+ delete patch.group;
+ }
+
+ canonicalizeFind(patch);
+ if (!Array.isArray(patch.replacement)) {
+ patch.replacement = [patch.replacement];
+ }
+
+ if (IS_REPORTER) {
+ patch.replacement.forEach(r => {
+ delete r.predicate;
+ });
+ }
+
+ patches.push(patch);
+}
+
+function isReporterTestable(p: Plugin, part: ReporterTestable) {
+ return p.reporterTestable == null
+ ? true
+ : (p.reporterTestable & part) === part;
+}
+
+// First round-trip to mark and force enable dependencies
//
// FIXME: might need to revisit this if there's ever nested (dependencies of dependencies) dependencies since this only
// goes for the top level and their children, but for now this works okay with the current API plugins
-for (const p of pluginsValues) if (settings[p.name]?.enabled) {
+for (const p of pluginsValues) if (isPluginEnabled(p.name)) {
p.dependencies?.forEach(d => {
const dep = Plugins[d];
- if (dep) {
- settings[d].enabled = true;
- dep.isDependency = true;
- }
- else {
+
+ if (!dep) {
const error = new Error(`Plugin ${p.name} has unresolved dependency ${d}`);
- if (IS_DEV)
+
+ if (IS_DEV) {
throw error;
+ }
+
logger.warn(error);
+ return;
}
+
+ settings[d].enabled = true;
+ dep.isDependency = true;
});
}
@@ -82,23 +114,18 @@ for (const p of pluginsValues) {
}
if (p.patches && isPluginEnabled(p.name)) {
- for (const patch of p.patches) {
- patch.plugin = p.name;
-
- canonicalizeFind(patch);
- if (!Array.isArray(patch.replacement)) {
- patch.replacement = [patch.replacement];
+ if (!IS_REPORTER || isReporterTestable(p, ReporterTestable.Patches)) {
+ for (const patch of p.patches) {
+ addPatch(patch, p.name);
}
-
- patches.push(patch);
}
}
}
export const startAllPlugins = traceFunction("startAllPlugins", function startAllPlugins(target: StartAt) {
logger.info(`Starting plugins (stage ${target})`);
- for (const name in Plugins)
- if (isPluginEnabled(name)) {
+ for (const name in Plugins) {
+ if (isPluginEnabled(name) && (!IS_REPORTER || isReporterTestable(Plugins[name], ReporterTestable.Start))) {
const p = Plugins[name];
const startAt = p.startAt ?? StartAt.WebpackReady;
@@ -106,30 +133,38 @@ export const startAllPlugins = traceFunction("startAllPlugins", function startAl
startPlugin(Plugins[name]);
}
+ }
});
export function startDependenciesRecursive(p: Plugin) {
let restartNeeded = false;
const failures: string[] = [];
- p.dependencies?.forEach(dep => {
- if (!Settings.plugins[dep].enabled) {
- startDependenciesRecursive(Plugins[dep]);
+
+ p.dependencies?.forEach(d => {
+ if (!settings[d].enabled) {
+ const dep = Plugins[d];
+ startDependenciesRecursive(dep);
+
// If the plugin has patches, don't start the plugin, just enable it.
- Settings.plugins[dep].enabled = true;
- if (Plugins[dep].patches) {
- logger.warn(`Enabling dependency ${dep} requires restart.`);
+ settings[d].enabled = true;
+ dep.isDependency = true;
+
+ if (dep.patches) {
+ logger.warn(`Enabling dependency ${d} requires restart.`);
restartNeeded = true;
return;
}
- const result = startPlugin(Plugins[dep]);
- if (!result) failures.push(dep);
+
+ const result = startPlugin(dep);
+ if (!result) failures.push(d);
}
});
+
return { restartNeeded, failures };
}
export function subscribePluginFluxEvents(p: Plugin, fluxDispatcher: typeof FluxDispatcher) {
- if (p.flux && !subscribedFluxEventsPlugins.has(p.name)) {
+ if (p.flux && !subscribedFluxEventsPlugins.has(p.name) && (!IS_REPORTER || isReporterTestable(p, ReporterTestable.FluxEvents))) {
subscribedFluxEventsPlugins.add(p.name);
logger.debug("Subscribing to flux events of plugin", p.name);
diff --git a/src/plugins/invisibleChat.desktop/index.tsx b/src/plugins/invisibleChat.desktop/index.tsx
index 3dfe51e77..01199d999 100644
--- a/src/plugins/invisibleChat.desktop/index.tsx
+++ b/src/plugins/invisibleChat.desktop/index.tsx
@@ -18,12 +18,13 @@
import { addChatBarButton, ChatBarButton } from "@api/ChatButtons";
import { addButton, removeButton } from "@api/MessagePopover";
+import { updateMessage } from "@api/MessageUpdater";
import { definePluginSettings } from "@api/Settings";
import ErrorBoundary from "@components/ErrorBoundary";
import { Devs } from "@utils/constants";
import { getStegCloak } from "@utils/dependencies";
-import definePlugin, { OptionType } from "@utils/types";
-import { ChannelStore, Constants, FluxDispatcher, RestAPI, Tooltip } from "@webpack/common";
+import definePlugin, { OptionType, ReporterTestable } from "@utils/types";
+import { ChannelStore, Constants, RestAPI, Tooltip } from "@webpack/common";
import { Message } from "discord-types/general";
import { buildDecModal } from "./components/DecryptionModal";
@@ -103,7 +104,10 @@ export default definePlugin({
name: "InvisibleChat",
description: "Encrypt your Messages in a non-suspicious way!",
authors: [Devs.SammCheese],
- dependencies: ["MessagePopoverAPI", "ChatInputButtonAPI"],
+ dependencies: ["MessagePopoverAPI", "ChatInputButtonAPI", "MessageUpdaterAPI"],
+ reporterTestable: ReporterTestable.Patches,
+ settings,
+
patches: [
{
// Indicator
@@ -120,7 +124,6 @@ export default definePlugin({
URL_REGEX: new RegExp(
/(http(s)?:\/\/.)?(www\.)?[-a-zA-Z0-9@:%._+~#=]{2,256}\.[a-z]{2,6}\b([-a-zA-Z0-9@:%_+.~#?&//=]*)/,
),
- settings,
async start() {
addButton("InvisibleChat", message => {
return this.INV_REGEX.test(message?.content)
@@ -180,14 +183,7 @@ export default definePlugin({
message.embeds.push(embed);
}
- this.updateMessage(message);
- },
-
- updateMessage: (message: any) => {
- FluxDispatcher.dispatch({
- type: "MESSAGE_UPDATE",
- message,
- });
+ updateMessage(message.channel_id, message.id, { embeds: message.embeds });
},
popOverIcon: () => ,
diff --git a/src/plugins/messageLinkEmbeds/index.tsx b/src/plugins/messageLinkEmbeds/index.tsx
index 6c8fd83e9..e76d53e4a 100644
--- a/src/plugins/messageLinkEmbeds/index.tsx
+++ b/src/plugins/messageLinkEmbeds/index.tsx
@@ -17,6 +17,7 @@
*/
import { addAccessory, removeAccessory } from "@api/MessageAccessories";
+import { updateMessage } from "@api/MessageUpdater";
import { definePluginSettings } from "@api/Settings";
import ErrorBoundary from "@components/ErrorBoundary";
import { Devs } from "@utils/constants.js";
@@ -28,7 +29,6 @@ import {
Button,
ChannelStore,
Constants,
- FluxDispatcher,
GuildStore,
IconUtils,
MessageStore,
@@ -250,15 +250,9 @@ function MessageEmbedAccessory({ message }: { message: Message; }) {
if (linkedMessage) {
messageCache.set(messageID, { message: linkedMessage, fetched: true });
} else {
- const msg = { ...message } as any;
- delete msg.embeds;
- delete msg.interaction;
messageFetchQueue.unshift(() => fetchMessage(channelID, messageID)
- .then(m => m && FluxDispatcher.dispatch({
- type: "MESSAGE_UPDATE",
- message: msg
- }))
+ .then(m => m && updateMessage(message.channel_id, message.id))
);
continue;
}
@@ -367,7 +361,7 @@ export default definePlugin({
name: "MessageLinkEmbeds",
description: "Adds a preview to messages that link another message",
authors: [Devs.TheSun, Devs.Ven, Devs.RyanCaoDev],
- dependencies: ["MessageAccessoriesAPI"],
+ dependencies: ["MessageAccessoriesAPI", "MessageUpdaterAPI"],
settings,
diff --git a/src/plugins/noPendingCount/index.ts b/src/plugins/noPendingCount/index.ts
index 29458df9d..57a65f52c 100644
--- a/src/plugins/noPendingCount/index.ts
+++ b/src/plugins/noPendingCount/index.ts
@@ -62,6 +62,16 @@ export default definePlugin({
replace: "return 0;"
}
},
+ // New message requests hook
+ {
+ find: "useNewMessageRequestsCount:",
+ predicate: () => settings.store.hideMessageRequestsCount,
+ replacement: {
+ match: /getNonChannelAckId\(\i\.\i\.MESSAGE_REQUESTS\).+?return /,
+ replace: "$&0;"
+ }
+ },
+ // Old message requests hook
{
find: "getMessageRequestsCount(){",
predicate: () => settings.store.hideMessageRequestsCount,
diff --git a/src/plugins/partyMode/index.ts b/src/plugins/partyMode/index.ts
index 56c19c02c..c40f2e3c7 100644
--- a/src/plugins/partyMode/index.ts
+++ b/src/plugins/partyMode/index.ts
@@ -18,7 +18,7 @@
import { definePluginSettings, migratePluginSettings } from "@api/Settings";
import { Devs } from "@utils/constants";
-import definePlugin, { OptionType } from "@utils/types";
+import definePlugin, { OptionType, ReporterTestable } from "@utils/types";
import { FluxDispatcher } from "@webpack/common";
const enum Intensity {
@@ -46,6 +46,7 @@ export default definePlugin({
name: "PartyMode",
description: "Allows you to use party mode cause the party never ends ✨",
authors: [Devs.UwUDev],
+ reporterTestable: ReporterTestable.None,
settings,
start() {
diff --git a/src/plugins/secretRingTone/index.ts b/src/plugins/secretRingTone/index.ts
index 9c3956a80..be804efc4 100644
--- a/src/plugins/secretRingTone/index.ts
+++ b/src/plugins/secretRingTone/index.ts
@@ -16,9 +16,8 @@ export default definePlugin({
{
find: '"call_ringing_beat"',
replacement: {
- // FIXME Remove === alternative when it hits stable
- match: /500(!==|===)\i\(\)\.random\(1,1e3\)/,
- replace: (_, predicate) => predicate === "!==" ? "false" : "true",
+ match: /500!==\i\(\)\.random\(1,1e3\)/,
+ replace: "false",
}
},
],
diff --git a/src/plugins/seeSummaries/README.md b/src/plugins/seeSummaries/README.md
index dfb90bab3..1c8b6c793 100644
--- a/src/plugins/seeSummaries/README.md
+++ b/src/plugins/seeSummaries/README.md
@@ -2,6 +2,8 @@
Enables Discord's experimental Summaries feature on every server, displaying AI generated summaries of conversations.
+Read more about summaries in the [official Discord help article](https://support.discord.com/hc/en-us/articles/12926016807575-In-Channel-Conversation-Summaries)!
+
Note that this plugin can't fetch old summaries, it can only display ones created while your Discord is running with the plugin enabled.
![](https://github.com/Vendicated/Vencord/assets/45497981/bd931b0c-2e85-4c10-9f7c-8ba01eb55745)
diff --git a/src/plugins/shikiCodeblocks.desktop/index.ts b/src/plugins/shikiCodeblocks.desktop/index.ts
index 27463195d..e6676a866 100644
--- a/src/plugins/shikiCodeblocks.desktop/index.ts
+++ b/src/plugins/shikiCodeblocks.desktop/index.ts
@@ -20,7 +20,7 @@ import "./shiki.css";
import { enableStyle } from "@api/Styles";
import { Devs } from "@utils/constants";
-import definePlugin from "@utils/types";
+import definePlugin, { ReporterTestable } from "@utils/types";
import previewExampleText from "file://previewExample.tsx";
import { shiki } from "./api/shiki";
@@ -34,6 +34,9 @@ export default definePlugin({
name: "ShikiCodeblocks",
description: "Brings vscode-style codeblocks into Discord, powered by Shiki",
authors: [Devs.Vap],
+ reporterTestable: ReporterTestable.Patches,
+ settings,
+
patches: [
{
find: "codeBlock:{react(",
@@ -66,7 +69,6 @@ export default definePlugin({
isPreview: true,
tempSettings,
}),
- settings,
// exports
shiki,
diff --git a/src/plugins/showConnections/index.tsx b/src/plugins/showConnections/index.tsx
index d70c09315..a78e4c418 100644
--- a/src/plugins/showConnections/index.tsx
+++ b/src/plugins/showConnections/index.tsx
@@ -182,7 +182,7 @@ export default definePlugin({
}
},
{
- find: "\"Profile Panel: user cannot be undefined\"",
+ find: ".UserPopoutUpsellSource.PROFILE_PANEL,",
replacement: {
// createElement(Divider, {}), createElement(NoteComponent)
match: /\(0,\i\.jsx\)\(\i\.\i,\{\}\).{0,100}setNote:(?=.+?channelId:(\i).id)/,
diff --git a/src/plugins/showHiddenChannels/index.tsx b/src/plugins/showHiddenChannels/index.tsx
index c120d72d8..35d56091a 100644
--- a/src/plugins/showHiddenChannels/index.tsx
+++ b/src/plugins/showHiddenChannels/index.tsx
@@ -73,8 +73,9 @@ export default definePlugin({
find: '"placeholder-channel-id"',
replacement: [
// Remove the special logic for channels we don't have access to
+ // FIXME Remove variable matcher from threadsIds when it hits stable
{
- match: /if\(!\i\.\i\.can\(\i\.\i\.VIEW_CHANNEL.+?{if\(this\.id===\i\).+?threadIds:\i}}/,
+ match: /if\(!\i\.\i\.can\(\i\.\i\.VIEW_CHANNEL.+?{if\(this\.id===\i\).+?threadIds:(?:\[\]|\i)}}/,
replace: ""
},
// Do not check for unreads when selecting the render level if the channel is hidden
diff --git a/src/plugins/vcNarrator/index.tsx b/src/plugins/vcNarrator/index.tsx
index ac629e749..6e8e4bbf5 100644
--- a/src/plugins/vcNarrator/index.tsx
+++ b/src/plugins/vcNarrator/index.tsx
@@ -22,7 +22,7 @@ import { Devs } from "@utils/constants";
import { Logger } from "@utils/Logger";
import { Margins } from "@utils/margins";
import { wordsToTitle } from "@utils/text";
-import definePlugin, { OptionType, PluginOptionsItem } from "@utils/types";
+import definePlugin, { OptionType, PluginOptionsItem, ReporterTestable } from "@utils/types";
import { findByPropsLazy } from "@webpack";
import { Button, ChannelStore, Forms, GuildMemberStore, SelectedChannelStore, SelectedGuildStore, useMemo, UserStore } from "@webpack/common";
@@ -155,6 +155,7 @@ export default definePlugin({
name: "VcNarrator",
description: "Announces when users join, leave, or move voice channels via narrator",
authors: [Devs.Ven],
+ reporterTestable: ReporterTestable.None,
flux: {
VOICE_STATE_UPDATES({ voiceStates }: { voiceStates: VoiceState[]; }) {
diff --git a/src/plugins/xsOverlay.desktop/index.ts b/src/plugins/xsOverlay.desktop/index.ts
index 5251959f2..caa44a40c 100644
--- a/src/plugins/xsOverlay.desktop/index.ts
+++ b/src/plugins/xsOverlay.desktop/index.ts
@@ -8,7 +8,7 @@ import { definePluginSettings } from "@api/Settings";
import { makeRange } from "@components/PluginSettings/components";
import { Devs } from "@utils/constants";
import { Logger } from "@utils/Logger";
-import definePlugin, { OptionType, PluginNative } from "@utils/types";
+import definePlugin, { OptionType, PluginNative, ReporterTestable } from "@utils/types";
import { findByPropsLazy } from "@webpack";
import { ChannelStore, GuildStore, UserStore } from "@webpack/common";
import type { Channel, Embed, GuildMember, MessageAttachment, User } from "discord-types/general";
@@ -143,7 +143,9 @@ export default definePlugin({
description: "Forwards discord notifications to XSOverlay, for easy viewing in VR",
authors: [Devs.Nyako],
tags: ["vr", "notify"],
+ reporterTestable: ReporterTestable.None,
settings,
+
flux: {
CALL_UPDATE({ call }: { call: Call; }) {
if (call?.ringing?.includes(UserStore.getCurrentUser().id) && settings.store.callNotifications) {
diff --git a/src/utils/Logger.ts b/src/utils/Logger.ts
index 1ae4762d7..22a381360 100644
--- a/src/utils/Logger.ts
+++ b/src/utils/Logger.ts
@@ -32,6 +32,11 @@ export class Logger {
constructor(public name: string, public color: string = "white") { }
private _log(level: "log" | "error" | "warn" | "info" | "debug", levelColor: string, args: any[], customFmt = "") {
+ if (IS_REPORTER && IS_WEB) {
+ console[level]("[Vencord]", this.name + ":", ...args);
+ return;
+ }
+
console[level](
`%c Vencord %c %c ${this.name} ${customFmt}`,
`background: ${levelColor}; color: black; font-weight: bold; border-radius: 5px;`,
diff --git a/src/utils/dependencies.ts b/src/utils/dependencies.ts
index f05900e14..d8c361e88 100644
--- a/src/utils/dependencies.ts
+++ b/src/utils/dependencies.ts
@@ -17,7 +17,6 @@
*/
import { makeLazy } from "./lazy";
-import { EXTENSION_BASE_URL } from "./web-metadata";
/*
Add dynamically loaded dependencies for plugins here.
@@ -67,15 +66,6 @@ export interface ApngFrameData {
playTime: number;
}
-// On web (extensions), use extension uri as basepath (load files from extension)
-// On desktop (electron), load from cdn
-export const rnnoiseDist = IS_EXTENSION
- ? new URL("/third-party/rnnoise", EXTENSION_BASE_URL).toString()
- : "https://unpkg.com/@sapphi-red/web-noise-suppressor@0.3.3/dist";
-export const rnnoiseWasmSrc = (simd = false) => `${rnnoiseDist}/rnnoise${simd ? "_simd" : ""}.wasm`;
-export const rnnoiseWorkletSrc = `${rnnoiseDist}/rnnoise/workletProcessor.js`;
-
-
// The below code is only used on the Desktop (electron) build of Vencord.
// Browser (extension) builds do not contain these remote imports.
diff --git a/src/utils/types.ts b/src/utils/types.ts
index 6e1524196..fe19a1093 100644
--- a/src/utils/types.ts
+++ b/src/utils/types.ts
@@ -94,6 +94,10 @@ export interface PluginDef {
* @default StartAt.WebpackReady
*/
startAt?: StartAt,
+ /**
+ * Which parts of the plugin can be tested by the reporter. Defaults to all parts
+ */
+ reporterTestable?: number;
/**
* Optionally provide settings that the user can configure in the Plugins tab of settings.
* @deprecated Use `settings` instead
@@ -144,6 +148,13 @@ export const enum StartAt {
WebpackReady = "WebpackReady"
}
+export const enum ReporterTestable {
+ None = 1 << 1,
+ Start = 1 << 2,
+ Patches = 1 << 3,
+ FluxEvents = 1 << 4
+}
+
export const enum OptionType {
STRING,
NUMBER,
diff --git a/src/webpack/common/internal.tsx b/src/webpack/common/internal.tsx
index 9a89af362..8957c254b 100644
--- a/src/webpack/common/internal.tsx
+++ b/src/webpack/common/internal.tsx
@@ -22,7 +22,7 @@ import { LazyComponent } from "@utils/react";
import { FilterFn, filters, lazyWebpackSearchHistory, waitFor } from "../webpack";
export function waitForComponent = React.ComponentType & Record>(name: string, filter: FilterFn | string | string[]): T {
- if (IS_DEV) lazyWebpackSearchHistory.push(["waitForComponent", Array.isArray(filter) ? filter : [filter]]);
+ if (IS_REPORTER) lazyWebpackSearchHistory.push(["waitForComponent", Array.isArray(filter) ? filter : [filter]]);
let myValue: T = function () {
throw new Error(`Vencord could not find the ${name} Component`);
@@ -38,7 +38,7 @@ export function waitForComponent = React.Comp
}
export function waitForStore(name: string, cb: (v: any) => void) {
- if (IS_DEV) lazyWebpackSearchHistory.push(["waitForStore", [name]]);
+ if (IS_REPORTER) lazyWebpackSearchHistory.push(["waitForStore", [name]]);
waitFor(filters.byStoreName(name), cb, { isIndirect: true });
}
diff --git a/src/webpack/common/types/stores.d.ts b/src/webpack/common/types/stores.d.ts
index 083ec2694..f1fc68e8b 100644
--- a/src/webpack/common/types/stores.d.ts
+++ b/src/webpack/common/types/stores.d.ts
@@ -41,8 +41,33 @@ export class FluxStore {
__getLocalVars(): Record;
}
+export class FluxEmitter {
+ constructor();
+
+ changeSentinel: number;
+ changedStores: Set;
+ isBatchEmitting: boolean;
+ isDispatching: boolean;
+ isPaused: boolean;
+ pauseTimer: NodeJS.Timeout | null;
+ reactChangedStores: Set;
+
+ batched(batch: (...args: any[]) => void): void;
+ destroy(): void;
+ emit(): void;
+ emitNonReactOnce(): void;
+ emitReactOnce(): void;
+ getChangeSentinel(): number;
+ getIsPaused(): boolean;
+ injectBatchEmitChanges(batch: (...args: any[]) => void): void;
+ markChanged(store: FluxStore): void;
+ pause(): void;
+ resume(): void;
+}
+
export interface Flux {
Store: typeof FluxStore;
+ Emitter: FluxEmitter;
}
export class WindowStore extends FluxStore {
diff --git a/src/webpack/common/utils.ts b/src/webpack/common/utils.ts
index 72a71f31c..bb96861f1 100644
--- a/src/webpack/common/utils.ts
+++ b/src/webpack/common/utils.ts
@@ -144,6 +144,7 @@ const persistFilter = filters.byCode("[zustand persist middleware]");
export const { persist: zustandPersist } = findLazy(m => m.persist && persistFilter(m.persist));
export const MessageActions = findByPropsLazy("editMessage", "sendMessage");
+export const MessageCache = findByPropsLazy("clearCache", "_channelMessages");
export const UserProfileActions = findByPropsLazy("openUserProfileModal", "closeUserProfileModal");
export const InviteActions = findByPropsLazy("resolveInvite");
diff --git a/src/webpack/patchWebpack.ts b/src/webpack/patchWebpack.ts
index 311e6f2bc..da5ca8b96 100644
--- a/src/webpack/patchWebpack.ts
+++ b/src/webpack/patchWebpack.ts
@@ -209,7 +209,7 @@ function patchFactories(factories: Record findByProps("blah")); console.log(mod.blah);
*/
export function proxyLazyWebpack(factory: () => any, attempts?: number) {
- if (IS_DEV) lazyWebpackSearchHistory.push(["proxyLazyWebpack", [factory]]);
+ if (IS_REPORTER) lazyWebpackSearchHistory.push(["proxyLazyWebpack", [factory]]);
return proxyLazy(factory, attempts);
}
@@ -278,7 +278,7 @@ export function proxyLazyWebpack(factory: () => any, attempts?: number)
* @returns Result of factory function
*/
export function LazyComponentWebpack(factory: () => any, attempts?: number) {
- if (IS_DEV) lazyWebpackSearchHistory.push(["LazyComponentWebpack", [factory]]);
+ if (IS_REPORTER) lazyWebpackSearchHistory.push(["LazyComponentWebpack", [factory]]);
return LazyComponent(factory, attempts);
}
@@ -287,7 +287,7 @@ export function LazyComponentWebpack(factory: () => any,
* Find the first module that matches the filter, lazily
*/
export function findLazy(filter: FilterFn) {
- if (IS_DEV) lazyWebpackSearchHistory.push(["find", [filter]]);
+ if (IS_REPORTER) lazyWebpackSearchHistory.push(["find", [filter]]);
return proxyLazy(() => find(filter));
}
@@ -306,7 +306,7 @@ export function findByProps(...props: string[]) {
* Find the first module that has the specified properties, lazily
*/
export function findByPropsLazy(...props: string[]) {
- if (IS_DEV) lazyWebpackSearchHistory.push(["findByProps", props]);
+ if (IS_REPORTER) lazyWebpackSearchHistory.push(["findByProps", props]);
return proxyLazy(() => findByProps(...props));
}
@@ -325,7 +325,7 @@ export function findByCode(...code: string[]) {
* Find the first function that includes all the given code, lazily
*/
export function findByCodeLazy(...code: string[]) {
- if (IS_DEV) lazyWebpackSearchHistory.push(["findByCode", code]);
+ if (IS_REPORTER) lazyWebpackSearchHistory.push(["findByCode", code]);
return proxyLazy(() => findByCode(...code));
}
@@ -344,7 +344,7 @@ export function findStore(name: string) {
* Find a store by its displayName, lazily
*/
export function findStoreLazy(name: string) {
- if (IS_DEV) lazyWebpackSearchHistory.push(["findStore", [name]]);
+ if (IS_REPORTER) lazyWebpackSearchHistory.push(["findStore", [name]]);
return proxyLazy(() => findStore(name));
}
@@ -363,7 +363,7 @@ export function findComponentByCode(...code: string[]) {
* Finds the first component that matches the filter, lazily.
*/
export function findComponentLazy(filter: FilterFn) {
- if (IS_DEV) lazyWebpackSearchHistory.push(["findComponent", [filter]]);
+ if (IS_REPORTER) lazyWebpackSearchHistory.push(["findComponent", [filter]]);
return LazyComponent(() => {
@@ -378,7 +378,7 @@ export function findComponentLazy(filter: FilterFn) {
* Finds the first component that includes all the given code, lazily
*/
export function findComponentByCodeLazy(...code: string[]) {
- if (IS_DEV) lazyWebpackSearchHistory.push(["findComponentByCode", code]);
+ if (IS_REPORTER) lazyWebpackSearchHistory.push(["findComponentByCode", code]);
return LazyComponent(() => {
const res = find(filters.componentByCode(...code), { isIndirect: true });
@@ -392,7 +392,7 @@ export function findComponentByCodeLazy(...code: string[
* Finds the first component that is exported by the first prop name, lazily
*/
export function findExportedComponentLazy(...props: string[]) {
- if (IS_DEV) lazyWebpackSearchHistory.push(["findExportedComponent", props]);
+ if (IS_REPORTER) lazyWebpackSearchHistory.push(["findExportedComponent", props]);
return LazyComponent(() => {
const res = find(filters.byProps(...props), { isIndirect: true });
@@ -402,14 +402,14 @@ export function findExportedComponentLazy(...props: stri
});
}
-export const DefaultExtractAndLoadChunksRegex = /(?:Promise\.all\(\[(\i\.\i\("[^)]+?"\)[^\]]+?)\]\)|(\i\.\i\("[^)]+?"\))|Promise\.resolve\(\))\.then\(\i\.bind\(\i,"([^)]+?)"\)\)/;
-export const ChunkIdsRegex = /\("(.+?)"\)/g;
+export const DefaultExtractAndLoadChunksRegex = /(?:(?:Promise\.all\(\[)?(\i\.e\("[^)]+?"\)[^\]]*?)(?:\]\))?|Promise\.resolve\(\))\.then\(\i\.bind\(\i,"([^)]+?)"\)\)/;
+export const ChunkIdsRegex = /\("([^"]+?)"\)/g;
/**
* Extract and load chunks using their entry point
* @param code An array of all the code the module factory containing the lazy chunk loading must include
- * @param matcher A RegExp that returns the chunk ids array as the first capture group and the entry point id as the second. Defaults to a matcher that captures the lazy chunk loading found in the module factory
- * @returns A promise that resolves when the chunks were loaded
+ * @param matcher A RegExp that returns the chunk ids array as the first capture group and the entry point id as the second. Defaults to a matcher that captures the first lazy chunk loading found in the module factory
+ * @returns A promise that resolves with a boolean whether the chunks were loaded
*/
export async function extractAndLoadChunks(code: string[], matcher: RegExp = DefaultExtractAndLoadChunksRegex) {
const module = findModuleFactory(...code);
@@ -417,7 +417,11 @@ export async function extractAndLoadChunks(code: string[], matcher: RegExp = Def
const err = new Error("extractAndLoadChunks: Couldn't find module factory");
logger.warn(err, "Code:", code, "Matcher:", matcher);
- return;
+ // Strict behaviour in DevBuilds to fail early and make sure the issue is found
+ if (IS_DEV && !devToolsOpen)
+ throw err;
+
+ return false;
}
const match = module.toString().match(canonicalizeMatch(matcher));
@@ -429,10 +433,10 @@ export async function extractAndLoadChunks(code: string[], matcher: RegExp = Def
if (IS_DEV && !devToolsOpen)
throw err;
- return;
+ return false;
}
- const [, rawChunkIdsArray, rawChunkIdsSingle, entryPointId] = match;
+ const [, rawChunkIds, entryPointId] = match;
if (Number.isNaN(Number(entryPointId))) {
const err = new Error("extractAndLoadChunks: Matcher didn't return a capturing group with the chunk ids array, or the entry point id returned as the second group wasn't a number");
logger.warn(err, "Code:", code, "Matcher:", matcher);
@@ -441,16 +445,27 @@ export async function extractAndLoadChunks(code: string[], matcher: RegExp = Def
if (IS_DEV && !devToolsOpen)
throw err;
- return;
+ return false;
}
- const rawChunkIds = rawChunkIdsArray ?? rawChunkIdsSingle;
if (rawChunkIds) {
const chunkIds = Array.from(rawChunkIds.matchAll(ChunkIdsRegex)).map((m: any) => m[1]);
await Promise.all(chunkIds.map(id => wreq.e(id)));
}
+ if (wreq.m[entryPointId] == null) {
+ const err = new Error("extractAndLoadChunks: Entry point is not loaded in the module factories, perhaps one of the chunks failed to load");
+ logger.warn(err, "Code:", code, "Matcher:", matcher);
+
+ // Strict behaviour in DevBuilds to fail early and make sure the issue is found
+ if (IS_DEV && !devToolsOpen)
+ throw err;
+
+ return false;
+ }
+
wreq(entryPointId);
+ return true;
}
/**
@@ -458,11 +473,11 @@ export async function extractAndLoadChunks(code: string[], matcher: RegExp = Def
*
* Extract and load chunks using their entry point
* @param code An array of all the code the module factory containing the lazy chunk loading must include
- * @param matcher A RegExp that returns the chunk ids array as the first capture group and the entry point id as the second. Defaults to a matcher that captures the lazy chunk loading found in the module factory
- * @returns A function that returns a promise that resolves when the chunks were loaded, on first call
+ * @param matcher A RegExp that returns the chunk ids array as the first capture group and the entry point id as the second. Defaults to a matcher that captures the first lazy chunk loading found in the module factory
+ * @returns A function that returns a promise that resolves with a boolean whether the chunks were loaded, on first call
*/
export function extractAndLoadChunksLazy(code: string[], matcher = DefaultExtractAndLoadChunksRegex) {
- if (IS_DEV) lazyWebpackSearchHistory.push(["extractAndLoadChunks", [code, matcher]]);
+ if (IS_REPORTER) lazyWebpackSearchHistory.push(["extractAndLoadChunks", [code, matcher]]);
return makeLazy(() => extractAndLoadChunks(code, matcher));
}
@@ -472,7 +487,7 @@ export function extractAndLoadChunksLazy(code: string[], matcher = DefaultExtrac
* then call the callback with the module as the first argument
*/
export function waitFor(filter: string | string[] | FilterFn, callback: CallbackFn, { isIndirect = false }: { isIndirect?: boolean; } = {}) {
- if (IS_DEV && !isIndirect) lazyWebpackSearchHistory.push(["waitFor", Array.isArray(filter) ? filter : [filter]]);
+ if (IS_REPORTER && !isIndirect) lazyWebpackSearchHistory.push(["waitFor", Array.isArray(filter) ? filter : [filter]]);
if (typeof filter === "string")
filter = filters.byProps(filter);