diff --git a/.github/ISSUE_TEMPLATE/blank.yml b/.github/ISSUE_TEMPLATE/blank.yml
index e8ca246de..2439d86a7 100644
--- a/.github/ISSUE_TEMPLATE/blank.yml
+++ b/.github/ISSUE_TEMPLATE/blank.yml
@@ -12,7 +12,8 @@ body:
DO NOT USE THIS FORM, unless
- you are a vencord contributor
- you were given explicit permission to use this form by a moderator in our support server
- - you are filing a security related report
+
+ DO NOT USE THIS FORM FOR SECURITY RELATED ISSUES. [CREATE A SECURITY ADVISORY INSTEAD.](https://github.com/Vendicated/Vencord/security/advisories/new)
- type: textarea
id: content
diff --git a/.vscode/settings.json b/.vscode/settings.json
index fa543b38c..8be0795f9 100644
--- a/.vscode/settings.json
+++ b/.vscode/settings.json
@@ -14,6 +14,8 @@
"typescript.preferences.quoteStyle": "double",
"javascript.preferences.quoteStyle": "double",
+ "eslint.experimental.useFlatConfig": false,
+
"gitlens.remotes": [
{
"domain": "codeberg.org",
diff --git a/package.json b/package.json
index 43ac36304..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": {
@@ -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",
@@ -106,6 +107,6 @@
},
"engines": {
"node": ">=18",
- "pnpm": ">=8"
+ "pnpm": ">=9"
}
}
diff --git a/scripts/generateReport.ts b/scripts/generateReport.ts
index 8233f3e5d..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,66 +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) {
- let args: unknown[] = [];
- try {
- args = await Promise.all(e.args().map(a => a.jsonValue()));
- } catch {
- return;
- }
-
- 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(
@@ -260,256 +195,107 @@ 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));
-
-await page.setBypassCSP(true);
+page.on("error", e => console.error("[Error]", e.message));
+page.on("pageerror", e => console.error("[Page Error]", e.message));
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();
- 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(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.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;
-
- 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(`
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/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/components/PluginSettings/index.tsx b/src/components/PluginSettings/index.tsx
index e6b2cf1fb..9c26a9cf1 100644
--- a/src/components/PluginSettings/index.tsx
+++ b/src/components/PluginSettings/index.tsx
@@ -261,8 +261,9 @@ export default function PluginSettings() {
plugins = [];
requiredPlugins = [];
+ const showApi = searchValue.value === "API";
for (const p of sortedPlugins) {
- if (!p.options && p.name.endsWith("API") && searchValue.value !== "API")
+ if (p.hidden || (!p.options && p.name.endsWith("API") && !showApi))
continue;
if (!pluginFilter(p)) continue;
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/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/appleMusic.desktop/README.md b/src/plugins/appleMusic.desktop/README.md
new file mode 100644
index 000000000..52ab93bfd
--- /dev/null
+++ b/src/plugins/appleMusic.desktop/README.md
@@ -0,0 +1,9 @@
+# AppleMusicRichPresence
+
+This plugin enables Discord rich presence for your Apple Music! (This only works on macOS with the Music app.)
+
+![Screenshot of the activity in Discord](https://github.com/Vendicated/Vencord/assets/70191398/1f811090-ab5f-4060-a9ee-d0ac44a1d3c0)
+
+## Configuration
+
+For the customizable activity format strings, you can use several special strings to include track data in activities! `{name}` is replaced with the track name; `{artist}` is replaced with the artist(s)' name(s); and `{album}` is replaced with the album name.
diff --git a/src/plugins/appleMusic.desktop/index.tsx b/src/plugins/appleMusic.desktop/index.tsx
new file mode 100644
index 000000000..16591028d
--- /dev/null
+++ b/src/plugins/appleMusic.desktop/index.tsx
@@ -0,0 +1,253 @@
+/*
+ * Vencord, a Discord client mod
+ * Copyright (c) 2024 Vendicated and contributors
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+import { definePluginSettings } from "@api/Settings";
+import { Devs } from "@utils/constants";
+import definePlugin, { OptionType, PluginNative } from "@utils/types";
+import { ApplicationAssetUtils, FluxDispatcher, Forms } from "@webpack/common";
+
+const Native = VencordNative.pluginHelpers.AppleMusic as PluginNative;
+
+interface ActivityAssets {
+ large_image?: string;
+ large_text?: string;
+ small_image?: string;
+ small_text?: string;
+}
+
+interface ActivityButton {
+ label: string;
+ url: string;
+}
+
+interface Activity {
+ state: string;
+ details?: string;
+ timestamps?: {
+ start?: number;
+ end?: number;
+ };
+ assets?: ActivityAssets;
+ buttons?: Array;
+ name: string;
+ application_id: string;
+ metadata?: {
+ button_urls?: Array;
+ };
+ type: number;
+ flags: number;
+}
+
+const enum ActivityType {
+ PLAYING = 0,
+ LISTENING = 2,
+}
+
+const enum ActivityFlag {
+ INSTANCE = 1 << 0,
+}
+
+export interface TrackData {
+ name: string;
+ album: string;
+ artist: string;
+
+ appleMusicLink?: string;
+ songLink?: string;
+
+ albumArtwork?: string;
+ artistArtwork?: string;
+
+ playerPosition: number;
+ duration: number;
+}
+
+const enum AssetImageType {
+ Album = "Album",
+ Artist = "Artist",
+}
+
+const applicationId = "1239490006054207550";
+
+function setActivity(activity: Activity | null) {
+ FluxDispatcher.dispatch({
+ type: "LOCAL_ACTIVITY_UPDATE",
+ activity,
+ socketId: "AppleMusic",
+ });
+}
+
+const settings = definePluginSettings({
+ activityType: {
+ type: OptionType.SELECT,
+ description: "Which type of activity",
+ options: [
+ { label: "Playing", value: ActivityType.PLAYING, default: true },
+ { label: "Listening", value: ActivityType.LISTENING }
+ ],
+ },
+ refreshInterval: {
+ type: OptionType.SLIDER,
+ description: "The interval between activity refreshes (seconds)",
+ markers: [1, 2, 2.5, 3, 5, 10, 15],
+ default: 5,
+ restartNeeded: true,
+ },
+ enableTimestamps: {
+ type: OptionType.BOOLEAN,
+ description: "Whether or not to enable timestamps",
+ default: true,
+ },
+ enableButtons: {
+ type: OptionType.BOOLEAN,
+ description: "Whether or not to enable buttons",
+ default: true,
+ },
+ nameString: {
+ type: OptionType.STRING,
+ description: "Activity name format string",
+ default: "Apple Music"
+ },
+ detailsString: {
+ type: OptionType.STRING,
+ description: "Activity details format string",
+ default: "{name}"
+ },
+ stateString: {
+ type: OptionType.STRING,
+ description: "Activity state format string",
+ default: "{artist}"
+ },
+ largeImageType: {
+ type: OptionType.SELECT,
+ description: "Activity assets large image type",
+ options: [
+ { label: "Album artwork", value: AssetImageType.Album, default: true },
+ { label: "Artist artwork", value: AssetImageType.Artist }
+ ],
+ },
+ largeTextString: {
+ type: OptionType.STRING,
+ description: "Activity assets large text format string",
+ default: "{album}"
+ },
+ smallImageType: {
+ type: OptionType.SELECT,
+ description: "Activity assets small image type",
+ options: [
+ { label: "Album artwork", value: AssetImageType.Album },
+ { label: "Artist artwork", value: AssetImageType.Artist, default: true }
+ ],
+ },
+ smallTextString: {
+ type: OptionType.STRING,
+ description: "Activity assets small text format string",
+ default: "{artist}"
+ },
+});
+
+function customFormat(formatStr: string, data: TrackData) {
+ return formatStr
+ .replaceAll("{name}", data.name)
+ .replaceAll("{album}", data.album)
+ .replaceAll("{artist}", data.artist);
+}
+
+function getImageAsset(type: AssetImageType, data: TrackData) {
+ const source = type === AssetImageType.Album
+ ? data.albumArtwork
+ : data.artistArtwork;
+
+ if (!source) return undefined;
+
+ return ApplicationAssetUtils.fetchAssetIds(applicationId, [source]).then(ids => ids[0]);
+}
+
+export default definePlugin({
+ name: "AppleMusicRichPresence",
+ description: "Discord rich presence for your Apple Music!",
+ authors: [Devs.RyanCaoDev],
+ hidden: !navigator.platform.startsWith("Mac"),
+
+ settingsAboutComponent() {
+ return <>
+
+ For the customizable activity format strings, you can use several special strings to include track data in activities!{" "}
+ {"{name}"}
is replaced with the track name; {"{artist}"}
is replaced with the artist(s)' name(s); and {"{album}"}
is replaced with the album name.
+
+ >;
+ },
+
+ settings,
+
+ start() {
+ this.updatePresence();
+ this.updateInterval = setInterval(() => { this.updatePresence(); }, settings.store.refreshInterval * 1000);
+ },
+
+ stop() {
+ clearInterval(this.updateInterval);
+ FluxDispatcher.dispatch({ type: "LOCAL_ACTIVITY_UPDATE", activity: null });
+ },
+
+ updatePresence() {
+ this.getActivity().then(activity => { setActivity(activity); });
+ },
+
+ async getActivity(): Promise {
+ const trackData = await Native.fetchTrackData();
+ if (!trackData) return null;
+
+ const [largeImageAsset, smallImageAsset] = await Promise.all([
+ getImageAsset(settings.store.largeImageType, trackData),
+ getImageAsset(settings.store.smallImageType, trackData)
+ ]);
+
+ const assets: ActivityAssets = {
+ large_image: largeImageAsset,
+ large_text: customFormat(settings.store.largeTextString, trackData),
+ small_image: smallImageAsset,
+ small_text: customFormat(settings.store.smallTextString, trackData),
+ };
+
+ const buttons: ActivityButton[] = [];
+
+ if (settings.store.enableButtons) {
+ if (trackData.appleMusicLink)
+ buttons.push({
+ label: "Listen on Apple Music",
+ url: trackData.appleMusicLink,
+ });
+
+ if (trackData.songLink)
+ buttons.push({
+ label: "View on SongLink",
+ url: trackData.songLink,
+ });
+ }
+
+ return {
+ application_id: applicationId,
+
+ name: customFormat(settings.store.nameString, trackData),
+ details: customFormat(settings.store.detailsString, trackData),
+ state: customFormat(settings.store.stateString, trackData),
+
+ timestamps: (settings.store.enableTimestamps ? {
+ start: Date.now() - (trackData.playerPosition * 1000),
+ end: Date.now() - (trackData.playerPosition * 1000) + (trackData.duration * 1000),
+ } : undefined),
+
+ assets,
+
+ buttons: buttons.length ? buttons.map(v => v.label) : undefined,
+ metadata: { button_urls: buttons.map(v => v.url) || undefined, },
+
+ type: settings.store.activityType,
+ flags: ActivityFlag.INSTANCE,
+ };
+ }
+});
diff --git a/src/plugins/appleMusic.desktop/native.ts b/src/plugins/appleMusic.desktop/native.ts
new file mode 100644
index 000000000..2eb2a0757
--- /dev/null
+++ b/src/plugins/appleMusic.desktop/native.ts
@@ -0,0 +1,120 @@
+/*
+ * Vencord, a Discord client mod
+ * Copyright (c) 2024 Vendicated and contributors
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+import { execFile } from "child_process";
+import { promisify } from "util";
+
+import type { TrackData } from ".";
+
+const exec = promisify(execFile);
+
+// function exec(file: string, args: string[] = []) {
+// return new Promise<{ code: number | null, stdout: string | null, stderr: string | null; }>((resolve, reject) => {
+// const process = spawn(file, args, { stdio: [null, "pipe", "pipe"] });
+
+// let stdout: string | null = null;
+// process.stdout.on("data", (chunk: string) => { stdout ??= ""; stdout += chunk; });
+// let stderr: string | null = null;
+// process.stderr.on("data", (chunk: string) => { stdout ??= ""; stderr += chunk; });
+
+// process.on("exit", code => { resolve({ code, stdout, stderr }); });
+// process.on("error", err => reject(err));
+// });
+// }
+
+async function applescript(cmds: string[]) {
+ const { stdout } = await exec("osascript", cmds.map(c => ["-e", c]).flat());
+ return stdout;
+}
+
+function makeSearchUrl(type: string, query: string) {
+ const url = new URL("https://tools.applemediaservices.com/api/apple-media/music/US/search.json");
+ url.searchParams.set("types", type);
+ url.searchParams.set("limit", "1");
+ url.searchParams.set("term", query);
+ return url;
+}
+
+const requestOptions: RequestInit = {
+ headers: { "user-agent": "Mozilla/5.0 (Windows NT 10.0; rv:125.0) Gecko/20100101 Firefox/125.0" },
+};
+
+interface RemoteData {
+ appleMusicLink?: string,
+ songLink?: string,
+ albumArtwork?: string,
+ artistArtwork?: string;
+}
+
+let cachedRemoteData: { id: string, data: RemoteData; } | { id: string, failures: number; } | null = null;
+
+async function fetchRemoteData({ id, name, artist, album }: { id: string, name: string, artist: string, album: string; }) {
+ if (id === cachedRemoteData?.id) {
+ if ("data" in cachedRemoteData) return cachedRemoteData.data;
+ if ("failures" in cachedRemoteData && cachedRemoteData.failures >= 5) return null;
+ }
+
+ try {
+ const [songData, artistData] = await Promise.all([
+ fetch(makeSearchUrl("songs", artist + " " + album + " " + name), requestOptions).then(r => r.json()),
+ fetch(makeSearchUrl("artists", artist.split(/ *[,&] */)[0]), requestOptions).then(r => r.json())
+ ]);
+
+ const appleMusicLink = songData?.songs?.data[0]?.attributes.url;
+ const songLink = songData?.songs?.data[0]?.id ? `https://song.link/i/${songData?.songs?.data[0]?.id}` : undefined;
+
+ const albumArtwork = songData?.songs?.data[0]?.attributes.artwork.url.replace("{w}", "512").replace("{h}", "512");
+ const artistArtwork = artistData?.artists?.data[0]?.attributes.artwork.url.replace("{w}", "512").replace("{h}", "512");
+
+ cachedRemoteData = {
+ id,
+ data: { appleMusicLink, songLink, albumArtwork, artistArtwork }
+ };
+ return cachedRemoteData.data;
+ } catch (e) {
+ console.error("[AppleMusicRichPresence] Failed to fetch remote data:", e);
+ cachedRemoteData = {
+ id,
+ failures: (id === cachedRemoteData?.id && "failures" in cachedRemoteData ? cachedRemoteData.failures : 0) + 1
+ };
+ return null;
+ }
+}
+
+export async function fetchTrackData(): Promise {
+ try {
+ await exec("pgrep", ["^Music$"]);
+ } catch (error) {
+ return null;
+ }
+
+ const playerState = await applescript(['tell application "Music"', "get player state", "end tell"])
+ .then(out => out.trim());
+ if (playerState !== "playing") return null;
+
+ const playerPosition = await applescript(['tell application "Music"', "get player position", "end tell"])
+ .then(text => Number.parseFloat(text.trim()));
+
+ const stdout = await applescript([
+ 'set output to ""',
+ 'tell application "Music"',
+ "set t_id to database id of current track",
+ "set t_name to name of current track",
+ "set t_album to album of current track",
+ "set t_artist to artist of current track",
+ "set t_duration to duration of current track",
+ 'set output to "" & t_id & "\\n" & t_name & "\\n" & t_album & "\\n" & t_artist & "\\n" & t_duration',
+ "end tell",
+ "return output"
+ ]);
+
+ const [id, name, album, artist, durationStr] = stdout.split("\n").filter(k => !!k);
+ const duration = Number.parseFloat(durationStr);
+
+ const remoteData = await fetchRemoteData({ id, name, artist, album });
+
+ return { name, album, artist, playerPosition, duration, ...remoteData };
+}
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/copyEmojiMarkdown/README.md b/src/plugins/copyEmojiMarkdown/README.md
new file mode 100644
index 000000000..9e62e6635
--- /dev/null
+++ b/src/plugins/copyEmojiMarkdown/README.md
@@ -0,0 +1,5 @@
+# CopyEmojiMarkdown
+
+Allows you to copy emojis as formatted string. Custom emojis will be copied as `<:trolley:1024751352028602449>`, default emojis as `🛒`
+
+![](https://github.com/Vendicated/Vencord/assets/45497981/417f345a-7031-4fe7-8e42-e238870cd547)
diff --git a/src/plugins/copyEmojiMarkdown/index.tsx b/src/plugins/copyEmojiMarkdown/index.tsx
new file mode 100644
index 000000000..a9c018a91
--- /dev/null
+++ b/src/plugins/copyEmojiMarkdown/index.tsx
@@ -0,0 +1,75 @@
+/*
+ * Vencord, a Discord client mod
+ * Copyright (c) 2024 Vendicated and contributors
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+import { definePluginSettings } from "@api/Settings";
+import { Devs } from "@utils/constants";
+import { copyWithToast } from "@utils/misc";
+import definePlugin, { OptionType } from "@utils/types";
+import { findByPropsLazy } from "@webpack";
+import { Menu } from "@webpack/common";
+
+const { convertNameToSurrogate } = findByPropsLazy("convertNameToSurrogate");
+
+interface Emoji {
+ type: string;
+ id: string;
+ name: string;
+}
+
+interface Target {
+ dataset: Emoji;
+ firstChild: HTMLImageElement;
+}
+
+function getEmojiMarkdown(target: Target, copyUnicode: boolean): string {
+ const { id: emojiId, name: emojiName } = target.dataset;
+
+ if (!emojiId) {
+ return copyUnicode
+ ? convertNameToSurrogate(emojiName)
+ : `:${emojiName}:`;
+ }
+
+ const extension = target?.firstChild.src.match(
+ /https:\/\/cdn\.discordapp\.com\/emojis\/\d+\.(\w+)/
+ )?.[1];
+
+ return `<${extension === "gif" ? "a" : ""}:${emojiName.replace(/~\d+$/, "")}:${emojiId}>`;
+}
+
+const settings = definePluginSettings({
+ copyUnicode: {
+ type: OptionType.BOOLEAN,
+ description: "Copy the raw unicode character instead of :name: for default emojis (👽)",
+ default: true,
+ },
+});
+
+export default definePlugin({
+ name: "CopyEmojiMarkdown",
+ description: "Allows you to copy emojis as formatted string (<:blobcatcozy:1026533070955872337>)",
+ authors: [Devs.HappyEnderman, Devs.Vishnya],
+ settings,
+
+ contextMenus: {
+ "expression-picker"(children, { target }: { target: Target }) {
+ if (target.dataset.type !== "emoji") return;
+
+ children.push(
+ {
+ copyWithToast(
+ getEmojiMarkdown(target, settings.store.copyUnicode),
+ "Success! Copied emoji markdown."
+ );
+ }}
+ />
+ );
+ },
+ },
+});
diff --git a/src/plugins/experiments/hideBugReport.css b/src/plugins/experiments/hideBugReport.css
new file mode 100644
index 000000000..ff78555d7
--- /dev/null
+++ b/src/plugins/experiments/hideBugReport.css
@@ -0,0 +1,3 @@
+#staff-help-popout-staff-help-bug-reporter {
+ display: none;
+}
diff --git a/src/plugins/experiments/index.tsx b/src/plugins/experiments/index.tsx
index 50b9521f9..cf4dbf249 100644
--- a/src/plugins/experiments/index.tsx
+++ b/src/plugins/experiments/index.tsx
@@ -16,31 +16,22 @@
* along with this program. If not, see .
*/
-import { definePluginSettings } from "@api/Settings";
+import { disableStyle, enableStyle } from "@api/Styles";
import ErrorBoundary from "@components/ErrorBoundary";
import { ErrorCard } from "@components/ErrorCard";
import { Devs } from "@utils/constants";
-import { Logger } from "@utils/Logger";
import { Margins } from "@utils/margins";
-import definePlugin, { OptionType } from "@utils/types";
+import definePlugin from "@utils/types";
import { findByPropsLazy } from "@webpack";
-import { Forms, React, UserStore } from "@webpack/common";
-import { User } from "discord-types/general";
+import { Forms, React } from "@webpack/common";
+
+import hideBugReport from "./hideBugReport.css?managed";
const KbdStyles = findByPropsLazy("key", "removeBuildOverride");
-const settings = definePluginSettings({
- enableIsStaff: {
- description: "Enable isStaff",
- type: OptionType.BOOLEAN,
- default: false,
- restartNeeded: true
- }
-});
-
export default definePlugin({
name: "Experiments",
- description: "Enable Access to Experiments in Discord!",
+ description: "Enable Access to Experiments & other dev-only features in Discord!",
authors: [
Devs.Megu,
Devs.Ven,
@@ -48,7 +39,6 @@ export default definePlugin({
Devs.BanTheNons,
Devs.Nuckyz
],
- settings,
patches: [
{
@@ -65,37 +55,25 @@ export default definePlugin({
replace: "$1=!0;"
}
},
- {
- find: '"isStaff",',
- predicate: () => settings.store.enableIsStaff,
- replacement: [
- {
- match: /(?<=>)(\i)\.hasFlag\((\i\.\i)\.STAFF\)(?=})/,
- replace: (_, user, flags) => `$self.isStaff(${user},${flags})`
- },
- {
- match: /hasFreePremium\(\){return this.isStaff\(\)\s*?\|\|/,
- replace: "hasFreePremium(){return ",
- }
- ]
- },
{
find: 'H1,title:"Experiments"',
replacement: {
match: 'title:"Experiments",children:[',
replace: "$&$self.WarningCard(),"
}
+ },
+ // change top right chat toolbar button from the help one to the dev one
+ {
+ find: "toolbar:function",
+ replacement: {
+ match: /\i\.isStaff\(\)/,
+ replace: "true"
+ }
}
],
- isStaff(user: User, flags: any) {
- try {
- return UserStore.getCurrentUser()?.id === user.id || user.hasFlag(flags.STAFF);
- } catch (err) {
- new Logger("Experiments").error(err);
- return user.hasFlag(flags.STAFF);
- }
- },
+ start: () => enableStyle(hideBugReport),
+ stop: () => disableStyle(hideBugReport),
settingsAboutComponent: () => {
const isMacOS = navigator.platform.includes("Mac");
@@ -105,14 +83,10 @@ export default definePlugin({
More Information
- You can enable client DevTools{" "}
+ You can open Discord's DevTools via {" "}
{modKey} +{" "}
{altKey} +{" "}
O{" "}
- after enabling isStaff
below
-
-
- and then toggling Enable DevTools
in the Developer Options
tab in settings.
);
@@ -128,6 +102,12 @@ export default definePlugin({
Only use experiments if you know what you're doing. Vencord is not responsible for any damage caused by enabling experiments.
+
+ If you don't know what an experiment does, ignore it. Do not ask us what experiments do either, we probably don't know.
+
+
+
+ No, you cannot use server-side features like checking the "Send to Client" box.
), { noop: true })
diff --git a/src/plugins/index.ts b/src/plugins/index.ts
index 53ab7983a..32bfe7e97 100644
--- a/src/plugins/index.ts
+++ b/src/plugins/index.ts
@@ -44,7 +44,6 @@ const settings = Settings.plugins;
export function isPluginEnabled(p: string) {
return (
- IS_REPORTER ||
Plugins[p]?.required ||
Plugins[p]?.isDependency ||
settings[p]?.enabled
diff --git a/src/plugins/noOnboardingDelay/index.ts b/src/plugins/noOnboardingDelay/index.ts
new file mode 100644
index 000000000..6211e97c2
--- /dev/null
+++ b/src/plugins/noOnboardingDelay/index.ts
@@ -0,0 +1,35 @@
+/*
+ * 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: "NoOnboardingDelay",
+ description: "Skips the slow and annoying onboarding delay",
+ authors: [Devs.nekohaxx],
+ patches: [
+ {
+ find: "Messages.ONBOARDING_COVER_WELCOME_SUBTITLE",
+ replacement: {
+ match: "3e3",
+ replace: "0"
+ },
+ },
+ ],
+});
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/platformIndicators/index.tsx b/src/plugins/platformIndicators/index.tsx
index 9fae9adfa..ea2ae125c 100644
--- a/src/plugins/platformIndicators/index.tsx
+++ b/src/plugins/platformIndicators/index.tsx
@@ -51,14 +51,17 @@ const Icons = {
desktop: Icon("M4 2.5c-1.103 0-2 .897-2 2v11c0 1.104.897 2 2 2h7v2H7v2h10v-2h-4v-2h7c1.103 0 2-.896 2-2v-11c0-1.103-.897-2-2-2H4Zm16 2v9H4v-9h16Z"),
web: Icon("M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2Zm-1 17.93c-3.95-.49-7-3.85-7-7.93 0-.62.08-1.21.21-1.79L9 15v1c0 1.1.9 2 2 2v1.93Zm6.9-2.54c-.26-.81-1-1.39-1.9-1.39h-1v-3c0-.55-.45-1-1-1H8v-2h2c.55 0 1-.45 1-1V7h2c1.1 0 2-.9 2-2v-.41c2.93 1.19 5 4.06 5 7.41 0 2.08-.8 3.97-2.1 5.39Z"),
mobile: Icon("M 187 0 L 813 0 C 916.277 0 1000 83.723 1000 187 L 1000 1313 C 1000 1416.277 916.277 1500 813 1500 L 187 1500 C 83.723 1500 0 1416.277 0 1313 L 0 187 C 0 83.723 83.723 0 187 0 Z M 125 1000 L 875 1000 L 875 250 L 125 250 Z M 500 1125 C 430.964 1125 375 1180.964 375 1250 C 375 1319.036 430.964 1375 500 1375 C 569.036 1375 625 1319.036 625 1250 C 625 1180.964 569.036 1125 500 1125 Z", { viewBox: "0 0 1000 1500", height: 17, width: 17 }),
- console: Icon("M14.8 2.7 9 3.1V47h3.3c1.7 0 6.2.3 10 .7l6.7.6V2l-4.2.2c-2.4.1-6.9.3-10 .5zm1.8 6.4c1 1.7-1.3 3.6-2.7 2.2C12.7 10.1 13.5 8 15 8c.5 0 1.2.5 1.6 1.1zM16 33c0 6-.4 10-1 10s-1-4-1-10 .4-10 1-10 1 4 1 10zm15-8v23.3l3.8-.7c2-.3 4.7-.6 6-.6H43V3h-2.2c-1.3 0-4-.3-6-.6L31 1.7V25z", { viewBox: "0 0 50 50" }),
+ embedded: Icon("M14.8 2.7 9 3.1V47h3.3c1.7 0 6.2.3 10 .7l6.7.6V2l-4.2.2c-2.4.1-6.9.3-10 .5zm1.8 6.4c1 1.7-1.3 3.6-2.7 2.2C12.7 10.1 13.5 8 15 8c.5 0 1.2.5 1.6 1.1zM16 33c0 6-.4 10-1 10s-1-4-1-10 .4-10 1-10 1 4 1 10zm15-8v23.3l3.8-.7c2-.3 4.7-.6 6-.6H43V3h-2.2c-1.3 0-4-.3-6-.6L31 1.7V25z", { viewBox: "0 0 50 50" }),
};
type Platform = keyof typeof Icons;
const StatusUtils = findByPropsLazy("useStatusFillColor", "StatusTypes");
const PlatformIcon = ({ platform, status, small }: { platform: Platform, status: string; small: boolean; }) => {
- const tooltip = platform[0].toUpperCase() + platform.slice(1);
+ const tooltip = platform === "embedded"
+ ? "Console"
+ : platform[0].toUpperCase() + platform.slice(1);
+
const Icon = Icons[platform] ?? Icons.desktop;
return ;
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/serverProfile/GuildProfileModal.tsx b/src/plugins/serverInfo/GuildInfoModal.tsx
similarity index 98%
rename from src/plugins/serverProfile/GuildProfileModal.tsx
rename to src/plugins/serverInfo/GuildInfoModal.tsx
index 8e6f60518..bed520b67 100644
--- a/src/plugins/serverProfile/GuildProfileModal.tsx
+++ b/src/plugins/serverInfo/GuildInfoModal.tsx
@@ -20,10 +20,10 @@ const FriendRow = findExportedComponentLazy("FriendRow");
const cl = classNameFactory("vc-gp-");
-export function openGuildProfileModal(guild: Guild) {
+export function openGuildInfoModal(guild: Guild) {
openModal(props =>
-
+
);
}
@@ -53,7 +53,7 @@ function renderTimestamp(timestamp: number) {
);
}
-function GuildProfileModal({ guild }: GuildProps) {
+function GuildInfoModal({ guild }: GuildProps) {
const [friendCount, setFriendCount] = useState();
const [blockedCount, setBlockedCount] = useState();
diff --git a/src/plugins/serverInfo/README.md b/src/plugins/serverInfo/README.md
new file mode 100644
index 000000000..98c9013e0
--- /dev/null
+++ b/src/plugins/serverInfo/README.md
@@ -0,0 +1,7 @@
+# ServerInfo
+
+Allows you to view info about servers and see friends and blocked users
+
+![](https://github.com/Vendicated/Vencord/assets/45497981/a49783b5-e8fc-41d8-968f-58600e9f6580)
+![](https://github.com/Vendicated/Vencord/assets/45497981/5efc158a-e671-4196-a15a-77edf79a2630)
+![Available as "Server Profile" option in the server context menu](https://github.com/Vendicated/Vencord/assets/45497981/f43be943-6dc4-4232-9709-fbeb382d8e54)
diff --git a/src/plugins/serverProfile/index.tsx b/src/plugins/serverInfo/index.tsx
similarity index 65%
rename from src/plugins/serverProfile/index.tsx
rename to src/plugins/serverInfo/index.tsx
index 9d495c9d3..be3172f01 100644
--- a/src/plugins/serverProfile/index.tsx
+++ b/src/plugins/serverInfo/index.tsx
@@ -5,30 +5,32 @@
*/
import { findGroupChildrenByChildId, NavContextMenuPatchCallback } from "@api/ContextMenu";
+import { migratePluginSettings } from "@api/Settings";
import { Devs } from "@utils/constants";
import definePlugin from "@utils/types";
import { Menu } from "@webpack/common";
import { Guild } from "discord-types/general";
-import { openGuildProfileModal } from "./GuildProfileModal";
+import { openGuildInfoModal } from "./GuildInfoModal";
const Patch: NavContextMenuPatchCallback = (children, { guild }: { guild: Guild; }) => {
const group = findGroupChildrenByChildId("privacy", children);
group?.push(
openGuildProfileModal(guild)}
+ action={() => openGuildInfoModal(guild)}
/>
);
};
+migratePluginSettings("ServerInfo", "ServerProfile"); // what was I thinking with this name lmao
export default definePlugin({
- name: "ServerProfile",
- description: "Allows you to view info about a server by right clicking it in the server list",
+ name: "ServerInfo",
+ description: "Allows you to view info about a server",
authors: [Devs.Ven, Devs.Nuckyz],
- tags: ["guild", "info"],
+ tags: ["guild", "info", "ServerProfile"],
contextMenus: {
"guild-context": Patch,
"guild-header-popout": Patch
diff --git a/src/plugins/serverProfile/styles.css b/src/plugins/serverInfo/styles.css
similarity index 100%
rename from src/plugins/serverProfile/styles.css
rename to src/plugins/serverInfo/styles.css
diff --git a/src/plugins/serverProfile/README.md b/src/plugins/serverProfile/README.md
deleted file mode 100644
index 9da70e74e..000000000
--- a/src/plugins/serverProfile/README.md
+++ /dev/null
@@ -1,7 +0,0 @@
-# ServerProfile
-
-Allows you to view info about servers and see friends and blocked users
-
-![image](https://github.com/Vendicated/Vencord/assets/45497981/a49783b5-e8fc-41d8-968f-58600e9f6580)
-![image](https://github.com/Vendicated/Vencord/assets/45497981/5efc158a-e671-4196-a15a-77edf79a2630)
-![image](https://github.com/Vendicated/Vencord/assets/45497981/f43be943-6dc4-4232-9709-fbeb382d8e54)
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/spotifyControls/index.tsx b/src/plugins/spotifyControls/index.tsx
index 06595892f..f7972aa36 100644
--- a/src/plugins/spotifyControls/index.tsx
+++ b/src/plugins/spotifyControls/index.tsx
@@ -77,6 +77,13 @@ export default definePlugin({
match: /repeat:"off"!==(.{1,3}),/,
replace: "actual_repeat:$1,$&"
}
+ },
+ {
+ find: "artists.filter",
+ replacement: {
+ match: /\(0,(\i)\.isNotNullish\)\((\i)\.id\)&&/,
+ replace: ""
+ }
}
],
diff --git a/src/plugins/usrbg/index.tsx b/src/plugins/usrbg/index.tsx
index 1221cb9c5..b8e9f14b3 100644
--- a/src/plugins/usrbg/index.tsx
+++ b/src/plugins/usrbg/index.tsx
@@ -74,15 +74,15 @@ export default definePlugin({
]
},
{
- find: /overrideBannerSrc:\i,profileType:/,
+ find: /overrideBannerSrc:\i,overrideBannerWidth:/,
replacement: [
{
match: /(\i)\.premiumType/,
replace: "$self.premiumHook($1)||$&"
},
{
- match: /(?<=function \i\((\i)\)\{)(?=var.{30,50},overrideBannerSrc:)/,
- replace: "$1.overrideBannerSrc=$self.useBannerHook($1);"
+ match: /function \i\((\i)\)\{/,
+ replace: "$&$1.overrideBannerSrc=$self.useBannerHook($1);"
}
]
},
diff --git a/src/plugins/viewIcons/index.tsx b/src/plugins/viewIcons/index.tsx
index 09254d511..a94689689 100644
--- a/src/plugins/viewIcons/index.tsx
+++ b/src/plugins/viewIcons/index.tsx
@@ -184,16 +184,16 @@ export default definePlugin({
patches: [
// Profiles Modal pfp
- {
- find: "User Profile Modal - Context Menu",
+ ...["User Profile Modal - Context Menu", ".UserProfileTypes.FULL_SIZE,hasProfileEffect:"].map(find => ({
+ find,
replacement: {
match: /\{src:(\i)(?=,avatarDecoration)/,
replace: "{src:$1,onClick:()=>$self.openImage($1)"
}
- },
+ })),
// Banners
- {
- find: ".NITRO_BANNER,",
+ ...[".NITRO_BANNER,", /overrideBannerSrc:\i,overrideBannerWidth:/].map(find => ({
+ find,
replacement: {
// style: { backgroundImage: shouldShowBanner ? "url(".concat(bannerUrl,
match: /style:\{(?=backgroundImage:(null!=\i)\?"url\("\.concat\((\i),)/,
@@ -201,7 +201,7 @@ export default definePlugin({
// onClick: () => shouldShowBanner && ev.target.style.backgroundImage && openImage(bannerUrl), style: { cursor: shouldShowBanner ? "pointer" : void 0,
'onClick:ev=>$1&&ev.target.style.backgroundImage&&$self.openImage($2),style:{cursor:$1?"pointer":void 0,'
}
- },
+ })),
// User DMs "User Profile" popup in the right
{
find: ".avatarPositionPanel",
@@ -210,6 +210,14 @@ export default definePlugin({
replace: "$1style:($2)?{cursor:\"pointer\"}:{},onClick:$2?()=>{$self.openImage($3)}"
}
},
+ {
+ find: ".canUsePremiumProfileCustomization,{avatarSrc:",
+ replacement: {
+ match: /children:\(0,\i\.jsx\)\(\i,{src:(\i)/,
+ replace: "style:{cursor:\"pointer\"},onClick:()=>{$self.openImage($1)},$&"
+
+ }
+ },
// Group DMs top small & large icon
{
find: /\.recipients\.length>=2(?!);
// iife so #__PURE__ works correctly
diff --git a/src/utils/types.ts b/src/utils/types.ts
index fe19a1093..2fa4a826e 100644
--- a/src/utils/types.ts
+++ b/src/utils/types.ts
@@ -85,6 +85,10 @@ export interface PluginDef {
* Whether this plugin is required and forcefully enabled
*/
required?: boolean;
+ /**
+ * Whether this plugin should be hidden from the user
+ */
+ hidden?: boolean;
/**
* Whether this plugin should be enabled by default, but can be disabled
*/