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/.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/.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/README.md b/README.md index a43c9f834..61575d4bb 100644 --- a/README.md +++ b/README.md @@ -5,8 +5,8 @@ The cutest Discord client mod | ![image](https://github.com/Vendicated/Vencord/assets/45497981/706722b1-32de-4d99-bee9-93993b504334) | -|:--:| -| A screenshot of vencord showcasing the [vencord-theme](https://github.com/synqat/vencord-theme) | +| :--------------------------------------------------------------------------------------------------: | +| A screenshot of vencord showcasing the [vencord-theme](https://github.com/synqat/vencord-theme) | ## Features @@ -32,9 +32,9 @@ https://discord.gg/D9uwnFnqmd ## Sponsors -| **Thanks a lot to all Vencord [sponsors](https://github.com/sponsors/Vendicated)!!** | -|:--:| -| [![](https://meow.vendicated.dev/sponsors.png)](https://github.com/sponsors/Vendicated) | +| **Thanks a lot to all Vencord [sponsors](https://github.com/sponsors/Vendicated)!!** | +| :------------------------------------------------------------------------------------------: | +| [![](https://meow.vendicated.dev/sponsors.png)](https://github.com/sponsors/Vendicated) | | *generated using [github-sponsor-graph](https://github.com/Vendicated/github-sponsor-graph)* | diff --git a/browser/content.js b/browser/content.js index 4810fe3c8..57964af64 100644 --- a/browser/content.js +++ b/browser/content.js @@ -2,23 +2,22 @@ if (typeof browser === "undefined") { var browser = chrome; } -const script = document.createElement("script"); -script.src = browser.runtime.getURL("dist/Vencord.js"); -script.id = "vencord-script"; -Object.assign(script.dataset, { - extensionBaseUrl: browser.runtime.getURL(""), - version: browser.runtime.getManifest().version -}); - const style = document.createElement("link"); style.type = "text/css"; style.rel = "stylesheet"; style.href = browser.runtime.getURL("dist/Vencord.css"); -document.documentElement.append(script); - document.addEventListener( "DOMContentLoaded", - () => document.documentElement.append(style), + () => { + document.documentElement.append(style); + window.postMessage({ + type: "vencord:meta", + meta: { + EXTENSION_VERSION: browser.runtime.getManifest().version, + EXTENSION_BASE_URL: browser.runtime.getURL(""), + } + }); + }, { once: true } ); diff --git a/browser/manifest.json b/browser/manifest.json index 69bf0cec0..c527c75b8 100644 --- a/browser/manifest.json +++ b/browser/manifest.json @@ -22,7 +22,15 @@ "run_at": "document_start", "matches": ["*://*.discord.com/*"], "js": ["content.js"], - "all_frames": true + "all_frames": true, + "world": "ISOLATED" + }, + { + "run_at": "document_start", + "matches": ["*://*.discord.com/*"], + "js": ["dist/Vencord.js"], + "all_frames": true, + "world": "MAIN" } ], diff --git a/browser/manifestv2.json b/browser/manifestv2.json index 3cac9450a..f5b08571a 100644 --- a/browser/manifestv2.json +++ b/browser/manifestv2.json @@ -22,7 +22,15 @@ "run_at": "document_start", "matches": ["*://*.discord.com/*"], "js": ["content.js"], - "all_frames": true + "all_frames": true, + "world": "ISOLATED" + }, + { + "run_at": "document_start", + "matches": ["*://*.discord.com/*"], + "js": ["dist/Vencord.js"], + "all_frames": true, + "world": "MAIN" } ], diff --git a/package.json b/package.json index 29b1506e2..2fcd8be4a 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "vencord", "private": "true", - "version": "1.8.6", + "version": "1.9.1", "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", @@ -39,7 +43,7 @@ "eslint-plugin-simple-header": "^1.0.2", "fflate": "^0.7.4", "gifenc": "github:mattdesl/gifenc#64842fca317b112a8590f8fef2bf3825da8f6fe3", - "monaco-editor": "^0.43.0", + "monaco-editor": "^0.50.0", "nanoid": "^4.0.2", "virtual-merge": "^1.0.1" }, @@ -103,6 +107,6 @@ }, "engines": { "node": ">=18", - "pnpm": ">=8" + "pnpm": ">=9" } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b03585799..19295325f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -35,8 +35,8 @@ importers: specifier: github:mattdesl/gifenc#64842fca317b112a8590f8fef2bf3825da8f6fe3 version: https://codeload.github.com/mattdesl/gifenc/tar.gz/64842fca317b112a8590f8fef2bf3825da8f6fe3 monaco-editor: - specifier: ^0.43.0 - version: 0.43.0 + specifier: ^0.50.0 + version: 0.50.0 nanoid: specifier: ^4.0.2 version: 4.0.2 @@ -1531,6 +1531,10 @@ packages: is-core-module@2.13.1: resolution: {integrity: sha512-hHrIjvZsftOsvKSn2TRYl63zvxsgE0K+0mYMoH6gD4omR5IWB2KynivBQczo3+wF1cCkjzvptnI9Q0sPU66ilw==} + is-core-module@2.14.0: + resolution: {integrity: sha512-a5dFJih5ZLYlRtDc0dZWP7RiKr6xIKzmn/oAYCDvdLThadVgyJwlaoQPmRtMSpz+rk0OGAgIu+TcM9HUF0fk1A==} + engines: {node: '>= 0.4'} + is-data-descriptor@0.1.4: resolution: {integrity: sha512-+w9D5ulSoBNlmw9OHn3U2v51SyoCd0he+bB3xMl62oijhrspxowjU+AIcDY0N3iEJbUEkB15IlMASQsxYigvXg==} engines: {node: '>=0.10.0'} @@ -1791,8 +1795,8 @@ packages: moment@2.29.4: resolution: {integrity: sha512-5LC9SOxjSc2HF6vO2CyuTDNivEdoz2IvyJJGj6X8DJ0eFyfszE0QiEd+iXmBvUP3WHxSjFH/vIsA0EN00cgr8w==} - monaco-editor@0.43.0: - resolution: {integrity: sha512-cnoqwQi/9fml2Szamv1XbSJieGJ1Dc8tENVMD26Kcfl7xGQWp7OBKMjlwKVGYFJ3/AXJjSOGvcqK7Ry/j9BM1Q==} + monaco-editor@0.50.0: + resolution: {integrity: sha512-8CclLCmrRRh+sul7C08BmPBP3P8wVWfBHomsTcndxg5NRCEPfu/mc2AGU8k37ajjDVXcXFc12ORAMUkmk+lkFA==} ms@2.0.0: resolution: {integrity: sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==} @@ -3463,7 +3467,7 @@ snapshots: eslint-import-resolver-node@0.3.9: dependencies: debug: 3.2.7 - is-core-module: 2.13.1 + is-core-module: 2.14.0 resolve: 1.22.8 transitivePeerDependencies: - supports-color @@ -3490,7 +3494,7 @@ snapshots: eslint-import-resolver-node: 0.3.9 eslint-module-utils: 2.8.1(@typescript-eslint/parser@5.59.1(eslint@8.46.0(patch_hash=xm46kqcmdgzlmm4aifkfpxaho4))(typescript@5.4.5))(eslint-import-resolver-node@0.3.9)(eslint@8.46.0(patch_hash=xm46kqcmdgzlmm4aifkfpxaho4)) hasown: 2.0.2 - is-core-module: 2.13.1 + is-core-module: 2.14.0 is-glob: 4.0.3 minimatch: 3.1.2 object.fromentries: 2.0.8 @@ -3934,6 +3938,10 @@ snapshots: dependencies: hasown: 2.0.2 + is-core-module@2.14.0: + dependencies: + hasown: 2.0.2 + is-data-descriptor@0.1.4: dependencies: kind-of: 3.2.2 @@ -4169,7 +4177,7 @@ snapshots: moment@2.29.4: {} - monaco-editor@0.43.0: {} + monaco-editor@0.50.0: {} ms@2.0.0: {} diff --git a/scripts/build/build.mjs b/scripts/build/build.mjs index 0c2a930a0..817c2cec3 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, resolvePluginName, 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,23 +75,21 @@ const globNativesPlugin = { let i = 0; for (const dir of pluginDirs) { const dirPath = join("src", dir); - if (!await existsAsync(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 exists(dirPath)) continue; + const plugins = await readdir(dirPath, { withFileTypes: true }); + for (const file of plugins) { + const fileName = file.name; + const nativePath = join(dirPath, fileName, "native.ts"); + const indexNativePath = join(dirPath, fileName, "native/index.ts"); - if (!(await existsAsync(nativePath)) && !(await existsAsync(indexNativePath))) + if (!(await exists(nativePath)) && !(await exists(indexNativePath))) continue; - const nameParts = p.split("."); - const namePartsWithoutTarget = nameParts.length === 1 ? nameParts : nameParts.slice(0, -1); - // pluginName.thing.desktop -> PluginName.thing - const cleanPluginName = p[0].toUpperCase() + namePartsWithoutTarget.join(".").slice(1); + const pluginName = await resolvePluginName(dirPath, file); const mod = `p${i}`; - code += `import * as ${mod} from "./${dir}/${p}/native";\n`; - natives += `${JSON.stringify(cleanPluginName)}:${mod},\n`; + code += `import * as ${mod} from "./${dir}/${fileName}/native";\n`; + natives += `${JSON.stringify(pluginName)}:${mod},\n`; i++; } } 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..c46a559a7 100644 --- a/scripts/build/common.mjs +++ b/scripts/build/common.mjs @@ -35,24 +35,52 @@ 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")); +const PluginDefinitionNameMatcher = /definePlugin\(\{\s*(["'])?name\1:\s*(["'`])(.+?)\2/; +/** + * @param {string} base + * @param {import("fs").Dirent} dirent + */ +export async function resolvePluginName(base, dirent) { + const fullPath = join(base, dirent.name); + const content = dirent.isFile() + ? await readFile(fullPath, "utf-8") + : await (async () => { + for (const file of ["index.ts", "index.tsx"]) { + try { + return await readFile(join(fullPath, file), "utf-8"); + } catch { + continue; + } + } + throw new Error(`Invalid plugin ${fullPath}: could not resolve entry point`); + })(); -export function existsAsync(path) { - return access(path, FsConstants.F_OK) + return PluginDefinitionNameMatcher.exec(content)?.[3] + ?? (() => { + throw new Error(`Invalid plugin ${fullPath}: must contain definePlugin call with simple string name property as first property`); + })(); +} + +export async function exists(path) { + return await access(path, FsConstants.F_OK) .then(() => true) .catch(() => false); } @@ -66,7 +94,7 @@ export const makeAllPackagesExternalPlugin = { setup(build) { const filter = /^[^./]|^\.[^./]|^\.\.[^/]/; // Must not start with "/" or "./" or "../" build.onResolve({ filter }, args => ({ path: args.path, external: true })); - }, + } }; /** @@ -86,31 +114,48 @@ export const globPlugins = kind => ({ build.onLoad({ filter, namespace: "import-plugins" }, async () => { const pluginDirs = ["plugins/_api", "plugins/_core", "plugins", "userplugins"]; let code = ""; - let plugins = "\n"; + let pluginsCode = "\n"; + let metaCode = "\n"; + let excludedCode = "\n"; let i = 0; for (const dir of pluginDirs) { - if (!await existsAsync(`./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 userPlugin = dir === "userplugins"; - const target = getPluginTarget(file); - if (target) { - if (target === "dev" && !watch) continue; - if (target === "web" && kind === "discordDesktop") continue; - if (target === "desktop" && kind === "web") continue; - if (target === "discordDesktop" && kind !== "discordDesktop") continue; - if (target === "vencordDesktop" && kind !== "vencordDesktop") continue; + const fullDir = `./src/${dir}`; + if (!await exists(fullDir)) continue; + const files = await readdir(fullDir, { withFileTypes: true }); + for (const file of files) { + const fileName = file.name; + if (fileName.startsWith("_") || fileName.startsWith(".")) continue; + if (fileName === "index.ts") continue; + + const target = getPluginTarget(fileName); + + if (target && !IS_REPORTER) { + const excluded = + (target === "dev" && !IS_DEV) || + (target === "web" && kind === "discordDesktop") || + (target === "desktop" && kind === "web") || + (target === "discordDesktop" && kind !== "discordDesktop") || + (target === "vencordDesktop" && kind !== "vencordDesktop"); + + if (excluded) { + const name = await resolvePluginName(fullDir, file); + excludedCode += `${JSON.stringify(name)}:${JSON.stringify(target)},\n`; + continue; + } } + const folderName = `src/${dir}/${fileName}`.replace(/^src\/plugins\//, ""); + const mod = `p${i}`; - code += `import ${mod} from "./${dir}/${file.replace(/\.tsx?$/, "")}";\n`; - plugins += `[${mod}.name]:${mod},\n`; + code += `import ${mod} from "./${dir}/${fileName.replace(/\.tsx?$/, "")}";\n`; + pluginsCode += `[${mod}.name]:${mod},\n`; + metaCode += `[${mod}.name]:${JSON.stringify({ folderName, userPlugin })},\n`; // TODO: add excluded plugins to display in the UI? i++; } } - code += `export default {${plugins}};`; + code += `export default {${pluginsCode}};export const PluginMeta={${metaCode}};export const ExcludedPlugins={${excludedCode}};`; return { contents: code, resolveDir: "./src" @@ -178,7 +223,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/generatePluginList.ts b/scripts/generatePluginList.ts index e8aa33a46..3d7c16c01 100644 --- a/scripts/generatePluginList.ts +++ b/scripts/generatePluginList.ts @@ -39,7 +39,7 @@ interface PluginData { hasCommands: boolean; required: boolean; enabledByDefault: boolean; - target: "discordDesktop" | "vencordDesktop" | "web" | "dev"; + target: "discordDesktop" | "vencordDesktop" | "desktop" | "web" | "dev"; filePath: string; } diff --git a/scripts/generateReport.ts b/scripts/generateReport.ts index 8bb87d812..d8cbb44a0 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,12 @@ 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()) + .catch(() => undefined); } const report = { @@ -59,6 +63,7 @@ const report = { error: string; }[], otherErrors: [] as string[], + ignoredErrors: [] as string[], badWebpackFinds: [] as string[] }; @@ -71,9 +76,11 @@ const IGNORED_DISCORD_ERRORS = [ "Attempting to set fast connect zstd when unsupported" ] as Array; -function toCodeBlock(s: string) { +function toCodeBlock(s: string, indentation = 0, isDiscord = false) { s = s.replace(/```/g, "`\u200B`\u200B`"); - return "```" + s + " ```"; + + const indentationStr = Array(!isDiscord ? indentation : 0).fill(" ").join(""); + return `\`\`\`\n${s.split("\n").map(s => indentationStr + s).join("\n")}\n${indentationStr}\`\`\``; } async function printReport() { @@ -87,44 +94,35 @@ async function printReport() { report.badPatches.forEach(p => { console.log(`- ${p.plugin} (${p.type})`); console.log(` - ID: \`${p.id}\``); - console.log(` - Match: ${toCodeBlock(p.match)}`); - if (p.error) console.log(` - Error: ${toCodeBlock(p.error)}`); + console.log(` - Match: ${toCodeBlock(p.match, " - Match: ".length)}`); + if (p.error) console.log(` - Error: ${toCodeBlock(p.error, " - Error: ".length)}`); }); console.log(); console.log("## Bad Webpack Finds"); - report.badWebpackFinds.forEach(p => console.log("- " + p)); + report.badWebpackFinds.forEach(p => console.log("- " + toCodeBlock(p, "- ".length))); console.log(); console.log("## Bad Starts"); report.badStarts.forEach(p => { console.log(`- ${p.plugin}`); - console.log(` - Error: ${toCodeBlock(p.error)}`); + console.log(` - Error: ${toCodeBlock(p.error, " - Error: ".length)}`); }); 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)}`); + console.log(`- ${toCodeBlock(e, "- ".length)}`); }); console.log(); console.log("## Ignored Discord Errors"); - ignoredErrors.forEach(e => { - console.log(`- ${toCodeBlock(e)}`); + report.ignoredErrors.forEach(e => { + console.log(`- ${toCodeBlock(e, "- ".length)}`); }); console.log(); @@ -146,16 +144,16 @@ async function printReport() { const lines = [ `**__${p.plugin} (${p.type}):__**`, `ID: \`${p.id}\``, - `Match: ${toCodeBlock(p.match)}` + `Match: ${toCodeBlock(p.match, "Match: ".length, true)}` ]; - if (p.error) lines.push(`Error: ${toCodeBlock(p.error)}`); + if (p.error) lines.push(`Error: ${toCodeBlock(p.error, "Error: ".length, true)}`); return lines.join("\n"); }).join("\n\n") || "None", color: report.badPatches.length ? 0xff0000 : 0x00ff00 }, { title: "Bad Webpack Finds", - description: report.badWebpackFinds.map(toCodeBlock).join("\n") || "None", + description: report.badWebpackFinds.map(f => toCodeBlock(f, 0, true)).join("\n") || "None", color: report.badWebpackFinds.length ? 0xff0000 : 0x00ff00 }, { @@ -163,7 +161,7 @@ async function printReport() { description: report.badStarts.map(p => { const lines = [ `**__${p.plugin}:__**`, - toCodeBlock(p.error) + toCodeBlock(p.error, 0, true) ]; return lines.join("\n"); } @@ -172,7 +170,7 @@ async function printReport() { }, { title: "Discord Errors", - description: report.otherErrors.length ? toCodeBlock(report.otherErrors.join("\n")) : "None", + description: report.otherErrors.length ? toCodeBlock(report.otherErrors.join("\n"), 0, true) : "None", color: report.otherErrors.length ? 0xff0000 : 0x00ff00 } ] @@ -188,61 +186,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 +198,121 @@ 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); - -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); - } - - 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); +page.on("error", e => console.error("[Error]", e.message)); +page.on("pageerror", e => { + if (!e.message.startsWith("Object") && !e.message.includes("Cannot find module")) { + console.error("[Page Error]", e.message); + report.otherErrors.push(e.message); + } else { + report.ignoredErrors.push(e.message); } +}); + +async function reporterRuntime(token: string) { + Vencord.Webpack.waitFor( + "loginToken", + m => { + console.log("[PUP_DEBUG]", "Logging in with token..."); + m.loginToken(token); + } + ); } 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/Badges.ts b/src/api/Badges.ts index 061bdeb8a..24c68c4ed 100644 --- a/src/api/Badges.ts +++ b/src/api/Badges.ts @@ -17,7 +17,6 @@ */ import ErrorBoundary from "@components/ErrorBoundary"; -import { User } from "discord-types/general"; import { ComponentType, HTMLProps } from "react"; import Plugins from "~plugins"; @@ -79,14 +78,14 @@ export function _getBadges(args: BadgeUserArgs) { : badges.push({ ...badge, ...args }); } } - const donorBadges = (Plugins.BadgeAPI as unknown as typeof import("../plugins/_api/badges").default).getDonorBadges(args.user.id); + const donorBadges = (Plugins.BadgeAPI as unknown as typeof import("../plugins/_api/badges").default).getDonorBadges(args.userId); if (donorBadges) badges.unshift(...donorBadges); return badges; } export interface BadgeUserArgs { - user: User; + userId: string; guildId: string; } diff --git a/src/api/Commands/commandHelpers.ts b/src/api/Commands/commandHelpers.ts index 2f7039137..4ae022c59 100644 --- a/src/api/Commands/commandHelpers.ts +++ b/src/api/Commands/commandHelpers.ts @@ -17,14 +17,14 @@ */ import { mergeDefaults } from "@utils/mergeDefaults"; -import { findByPropsLazy } from "@webpack"; +import { findByCodeLazy } from "@webpack"; import { MessageActions, SnowflakeUtils } from "@webpack/common"; import { Message } from "discord-types/general"; import type { PartialDeep } from "type-fest"; import { Argument } from "./types"; -const MessageCreator = findByPropsLazy("createBotMessage"); +const createBotMessage = findByCodeLazy('username:"Clyde"'); export function generateId() { return `-${SnowflakeUtils.fromTimestamp(Date.now())}`; @@ -37,7 +37,7 @@ export function generateId() { * @returns {Message} */ export function sendBotMessage(channelId: string, message: PartialDeep): Message { - const botMessage = MessageCreator.createBotMessage({ channelId, content: "", embeds: [] }); + const botMessage = createBotMessage({ channelId, content: "", embeds: [] }); MessageActions.receiveMessage(channelId, mergeDefaults(message, botMessage)); 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..284a20886 --- /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/UserSettings.ts b/src/api/UserSettings.ts new file mode 100644 index 000000000..4de92a81a --- /dev/null +++ b/src/api/UserSettings.ts @@ -0,0 +1,81 @@ +/* + * Vencord, a modification for Discord's desktop app + * Copyright (c) 2023 Vendicated and contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . +*/ + +import { proxyLazy } from "@utils/lazy"; +import { Logger } from "@utils/Logger"; +import { findModuleId, proxyLazyWebpack, wreq } from "@webpack"; + +interface UserSettingDefinition { + /** + * Get the setting value + */ + getSetting(): T; + /** + * Update the setting value + * @param value The new value + */ + updateSetting(value: T): Promise; + /** + * Update the setting value + * @param value A callback that accepts the old value as the first argument, and returns the new value + */ + updateSetting(value: (old: T) => T): Promise; + /** + * Stateful React hook for this setting value + */ + useSetting(): T; + userSettingsAPIGroup: string; + userSettingsAPIName: string; +} + +export const UserSettings: Record> | undefined = proxyLazyWebpack(() => { + const modId = findModuleId('"textAndImages","renderSpoilers"'); + if (modId == null) return new Logger("UserSettingsAPI ").error("Didn't find settings module."); + + return wreq(modId as any); +}); + +/** + * Get the setting with the given setting group and name. + * + * @param group The setting group + * @param name The name of the setting + */ +export function getUserSetting(group: string, name: string): UserSettingDefinition | undefined { + if (!Vencord.Plugins.isPluginEnabled("UserSettingsAPI")) throw new Error("Cannot use UserSettingsAPI without setting as dependency."); + + for (const key in UserSettings) { + const userSetting = UserSettings[key]; + + if (userSetting.userSettingsAPIGroup === group && userSetting.userSettingsAPIName === name) { + return userSetting; + } + } +} + +/** + * {@link getUserSettingDefinition}, lazy. + * + * Get the setting with the given setting group and name. + * + * @param group The setting group + * @param name The name of the setting + */ +export function getUserSettingLazy(group: string, name: string) { + return proxyLazy(() => getUserSetting(group, name)); +} diff --git a/src/api/index.ts b/src/api/index.ts index 5dca63105..d4d7b4614 100644 --- a/src/api/index.ts +++ b/src/api/index.ts @@ -26,11 +26,13 @@ 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"; import * as $Settings from "./Settings"; import * as $Styles from "./Styles"; +import * as $UserSettings from "./UserSettings"; /** * An API allowing you to listen to Message Clicks or run your own logic @@ -110,3 +112,13 @@ 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; + +/** + * An API allowing you to get an user setting + */ +export const UserSettings = $UserSettings; diff --git a/src/components/PluginSettings/ContributorModal.tsx b/src/components/PluginSettings/ContributorModal.tsx index 99a8da168..c3c36f1e6 100644 --- a/src/components/PluginSettings/ContributorModal.tsx +++ b/src/components/PluginSettings/ContributorModal.tsx @@ -11,20 +11,16 @@ import { classNameFactory } from "@api/Styles"; import ErrorBoundary from "@components/ErrorBoundary"; import { Link } from "@components/Link"; import { DevsById } from "@utils/constants"; -import { fetchUserProfile, getTheme, Theme } from "@utils/discord"; -import { pluralise } from "@utils/misc"; +import { fetchUserProfile } from "@utils/discord"; +import { classes, pluralise } from "@utils/misc"; import { ModalContent, ModalRoot, openModal } from "@utils/modal"; -import { Forms, MaskedLink, showToast, Tooltip, useEffect, useMemo, UserProfileStore, useStateFromStores } from "@webpack/common"; +import { Forms, showToast, useEffect, useMemo, UserProfileStore, useStateFromStores } from "@webpack/common"; import { User } from "discord-types/general"; import Plugins from "~plugins"; import { PluginCard } from "."; - -const WebsiteIconDark = "/assets/e1e96d89e192de1997f73730db26e94f.svg"; -const WebsiteIconLight = "/assets/730f58bcfd5a57a5e22460c445a0c6cf.svg"; -const GithubIconLight = "/assets/3ff98ad75ac94fa883af5ed62d17c459.svg"; -const GithubIconDark = "/assets/6a853b4c87fce386cbfef4a2efbacb09.svg"; +import { GithubButton, WebsiteButton } from "./LinkIconButton"; const cl = classNameFactory("vc-author-modal-"); @@ -40,16 +36,6 @@ export function openContributorModal(user: User) { ); } -function GithubIcon() { - const src = getTheme() === Theme.Light ? GithubIconLight : GithubIconDark; - return GitHub; -} - -function WebsiteIcon() { - const src = getTheme() === Theme.Light ? WebsiteIconLight : WebsiteIconDark; - return Website; -} - function ContributorModal({ user }: { user: User; }) { useSettings(); @@ -86,24 +72,18 @@ function ContributorModal({ user }: { user: User; }) { /> {user.username} -
+
{website && ( - - {props => ( - - - - )} - + )} {githubName && ( - - {props => ( - - - - )} - + )}
diff --git a/src/components/PluginSettings/LinkIconButton.css b/src/components/PluginSettings/LinkIconButton.css new file mode 100644 index 000000000..1055d6c70 --- /dev/null +++ b/src/components/PluginSettings/LinkIconButton.css @@ -0,0 +1,12 @@ +.vc-settings-modal-link-icon { + height: 32px; + width: 32px; + border-radius: 50%; + border: 4px solid var(--background-tertiary); + box-sizing: border-box +} + +.vc-settings-modal-links { + display: flex; + gap: 0.2em; +} diff --git a/src/components/PluginSettings/LinkIconButton.tsx b/src/components/PluginSettings/LinkIconButton.tsx new file mode 100644 index 000000000..ea36dda24 --- /dev/null +++ b/src/components/PluginSettings/LinkIconButton.tsx @@ -0,0 +1,45 @@ +/* + * Vencord, a Discord client mod + * Copyright (c) 2024 Vendicated and contributors + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +import "./LinkIconButton.css"; + +import { getTheme, Theme } from "@utils/discord"; +import { MaskedLink, Tooltip } from "@webpack/common"; + +const WebsiteIconDark = "/assets/e1e96d89e192de1997f73730db26e94f.svg"; +const WebsiteIconLight = "/assets/730f58bcfd5a57a5e22460c445a0c6cf.svg"; +const GithubIconLight = "/assets/3ff98ad75ac94fa883af5ed62d17c459.svg"; +const GithubIconDark = "/assets/6a853b4c87fce386cbfef4a2efbacb09.svg"; + +export function GithubIcon() { + const src = getTheme() === Theme.Light ? GithubIconLight : GithubIconDark; + return ; +} + +export function WebsiteIcon() { + const src = getTheme() === Theme.Light ? WebsiteIconLight : WebsiteIconDark; + return ; +} + +interface Props { + text: string; + href: string; +} + +function LinkIcon({ text, href, Icon }: Props & { Icon: React.ComponentType; }) { + return ( + + {props => ( + + + + )} + + ); +} + +export const WebsiteButton = (props: Props) => ; +export const GithubButton = (props: Props) => ; diff --git a/src/components/PluginSettings/PluginModal.css b/src/components/PluginSettings/PluginModal.css new file mode 100644 index 000000000..1f4b9aaad --- /dev/null +++ b/src/components/PluginSettings/PluginModal.css @@ -0,0 +1,7 @@ +.vc-plugin-modal-info { + align-items: center; +} + +.vc-plugin-modal-description { + flex-grow: 1; +} diff --git a/src/components/PluginSettings/PluginModal.tsx b/src/components/PluginSettings/PluginModal.tsx index 34de43c2d..e5da01f36 100644 --- a/src/components/PluginSettings/PluginModal.tsx +++ b/src/components/PluginSettings/PluginModal.tsx @@ -16,10 +16,14 @@ * along with this program. If not, see . */ +import "./PluginModal.css"; + import { generateId } from "@api/Commands"; import { useSettings } from "@api/Settings"; +import { classNameFactory } from "@api/Styles"; import ErrorBoundary from "@components/ErrorBoundary"; import { Flex } from "@components/Flex"; +import { gitRemote } from "@shared/vencordUserAgent"; import { proxyLazy } from "@utils/lazy"; import { Margins } from "@utils/margins"; import { classes, isObjectEmpty } from "@utils/misc"; @@ -30,6 +34,8 @@ import { Button, Clickable, FluxDispatcher, Forms, React, Text, Tooltip, UserSto import { User } from "discord-types/general"; import { Constructor } from "type-fest"; +import { PluginMeta } from "~plugins"; + import { ISettingElementProps, SettingBooleanComponent, @@ -40,6 +46,9 @@ import { SettingTextComponent } from "./components"; import { openContributorModal } from "./ContributorModal"; +import { GithubButton, WebsiteButton } from "./LinkIconButton"; + +const cl = classNameFactory("vc-plugin-modal-"); const UserSummaryItem = findComponentByCodeLazy("defaultRenderUser", "showDefaultAvatarsForNullUsers"); const AvatarStyles = findByPropsLazy("moreUsers", "emptyUser", "avatarContainer", "clickableAvatar"); @@ -180,16 +189,54 @@ export default function PluginModal({ plugin, onRestartNeeded, onClose, transiti ); } + /* + function switchToPopout() { + onClose(); + + const PopoutKey = `DISCORD_VENCORD_PLUGIN_SETTINGS_MODAL_${plugin.name}`; + PopoutActions.open( + PopoutKey, + () => PopoutActions.close(PopoutKey)} + /> + ); + } + */ + + const pluginMeta = PluginMeta[plugin.name]; + return ( {plugin.name} + + {/* + + */} - About {plugin.name} - {plugin.description} + + {plugin.description} + {!pluginMeta.userPlugin && ( +
+ + +
+ )} +
Authors
require("../../plugins")); @@ -69,7 +69,7 @@ function ReloadRequiredCard({ required }: { required: boolean; }) { Restart now to apply new plugins and their settings - @@ -177,6 +177,37 @@ const enum SearchStatus { NEW } +function ExcludedPluginsList({ search }: { search: string; }) { + const matchingExcludedPlugins = Object.entries(ExcludedPlugins) + .filter(([name]) => name.toLowerCase().includes(search)); + + const ExcludedReasons: Record<"web" | "discordDesktop" | "vencordDesktop" | "desktop" | "dev", string> = { + desktop: "Discord Desktop app or Vesktop", + discordDesktop: "Discord Desktop app", + vencordDesktop: "Vesktop app", + web: "Vesktop app and the Web version of Discord", + dev: "Developer version of Vencord" + }; + + return ( + + {matchingExcludedPlugins.length + ? <> + Are you looking for: +
    + {matchingExcludedPlugins.map(([name, reason]) => ( +
  • + {name}: Only available on the {ExcludedReasons[reason]} +
  • + ))} +
+ + : "No plugins meet the search criteria." + } +
+ ); +} + export default function PluginSettings() { const settings = useSettings(); const changes = React.useMemo(() => new ChangeList(), []); @@ -215,26 +246,27 @@ export default function PluginSettings() { return o; }, []); - const sortedPlugins = React.useMemo(() => Object.values(Plugins) + const sortedPlugins = useMemo(() => Object.values(Plugins) .sort((a, b) => a.name.localeCompare(b.name)), []); const [searchValue, setSearchValue] = React.useState({ value: "", status: SearchStatus.ALL }); + const search = searchValue.value.toLowerCase(); const onSearch = (query: string) => setSearchValue(prev => ({ ...prev, value: query })); const onStatusChange = (status: SearchStatus) => setSearchValue(prev => ({ ...prev, status })); const pluginFilter = (plugin: typeof Plugins[keyof typeof Plugins]) => { - const enabled = settings.plugins[plugin.name]?.enabled; - if (enabled && searchValue.status === SearchStatus.DISABLED) return false; - if (!enabled && searchValue.status === SearchStatus.ENABLED) return false; - if (searchValue.status === SearchStatus.NEW && !newPlugins?.includes(plugin.name)) return false; - if (!searchValue.value.length) return true; + const { status } = searchValue; + const enabled = Vencord.Plugins.isPluginEnabled(plugin.name); + if (enabled && status === SearchStatus.DISABLED) return false; + if (!enabled && status === SearchStatus.ENABLED) return false; + if (status === SearchStatus.NEW && !newPlugins?.includes(plugin.name)) return false; + if (!search.length) return true; - const v = searchValue.value.toLowerCase(); return ( - plugin.name.toLowerCase().includes(v) || - plugin.description.toLowerCase().includes(v) || - plugin.tags?.some(t => t.toLowerCase().includes(v)) + plugin.name.toLowerCase().includes(search) || + plugin.description.toLowerCase().includes(search) || + plugin.tags?.some(t => t.toLowerCase().includes(search)) ); }; @@ -255,53 +287,48 @@ export default function PluginSettings() { return lodash.isEqual(newPlugins, sortedPluginNames) ? [] : newPlugins; })); - type P = JSX.Element | JSX.Element[]; - let plugins: P, requiredPlugins: P; - if (sortedPlugins?.length) { - plugins = []; - requiredPlugins = []; + const plugins = [] as JSX.Element[]; + const requiredPlugins = [] as JSX.Element[]; - for (const p of sortedPlugins) { - if (!p.options && p.name.endsWith("API") && searchValue.value !== "API") - continue; + const showApi = searchValue.value.includes("API"); + for (const p of sortedPlugins) { + if (p.hidden || (!p.options && p.name.endsWith("API") && !showApi)) + continue; - if (!pluginFilter(p)) continue; + if (!pluginFilter(p)) continue; - const isRequired = p.required || depMap[p.name]?.some(d => settings.plugins[d].enabled); + const isRequired = p.required || depMap[p.name]?.some(d => settings.plugins[d].enabled); - if (isRequired) { - const tooltipText = p.required - ? "This plugin is required for Vencord to function." - : makeDependencyList(depMap[p.name]?.filter(d => settings.plugins[d].enabled)); - - requiredPlugins.push( - - {({ onMouseLeave, onMouseEnter }) => ( - changes.handleChange(name)} - disabled={true} - plugin={p} - /> - )} - - ); - } else { - plugins.push( - changes.handleChange(name)} - disabled={false} - plugin={p} - isNew={newPlugins?.includes(p.name)} - key={p.name} - /> - ); - } + if (isRequired) { + const tooltipText = p.required + ? "This plugin is required for Vencord to function." + : makeDependencyList(depMap[p.name]?.filter(d => settings.plugins[d].enabled)); + requiredPlugins.push( + + {({ onMouseLeave, onMouseEnter }) => ( + changes.handleChange(name)} + disabled={true} + plugin={p} + key={p.name} + /> + )} + + ); + } else { + plugins.push( + changes.handleChange(name)} + disabled={false} + plugin={p} + isNew={newPlugins?.includes(p.name)} + key={p.name} + /> + ); } - } else { - plugins = requiredPlugins = No plugins meet search criteria.; } return ( @@ -332,9 +359,18 @@ export default function PluginSettings() { Plugins -
- {plugins} -
+ {plugins.length || requiredPlugins.length + ? ( +
+ {plugins.length + ? plugins + : No plugins meet the search criteria. + } +
+ ) + : + } + @@ -342,7 +378,10 @@ export default function PluginSettings() { Required Plugins
- {requiredPlugins} + {requiredPlugins.length + ? requiredPlugins + : No plugins meet the search criteria. + }
); diff --git a/src/components/PluginSettings/styles.css b/src/components/PluginSettings/styles.css index 66b2a2158..d3d182e58 100644 --- a/src/components/PluginSettings/styles.css +++ b/src/components/PluginSettings/styles.css @@ -78,6 +78,7 @@ .vc-plugins-restart-card button { margin-top: 0.5em; + background: var(--info-warning-foreground) !important; } .vc-plugins-info-button svg:not(:hover, :focus) { 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..64c3e0ead --- /dev/null +++ b/src/debug/loadLazyChunks.ts @@ -0,0 +1,169 @@ +/* + * 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 isWorkerAsset = await fetch(wreq.p + wreq.u(id)) + .then(r => r.text()) + .then(t => t.includes("importScripts(")); + + if (isWorkerAsset) { + 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 isWorkerAsset = await fetch(wreq.p + wreq.u(id)) + .then(r => r.text()) + .then(t => t.includes("importScripts(")); + + // Loads and requires a chunk + if (!isWorkerAsset) { + await wreq.e(id as any); + // Technically, the id of the chunk does not match the entry point + // But, still try it because we have no way to get the actual entry point + 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..ddd5e5f18 --- /dev/null +++ b/src/debug/runReporter.ts @@ -0,0 +1,84 @@ +/* + * 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"; + + let result: any; + try { + 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 if (method === "mapMangledModule") { + const [code, mapper] = args; + + result = Webpack.mapMangledModule(code, mapper); + if (Object.keys(result).length !== Object.keys(mapper).length) throw new Error("Webpack Find Fail"); + } else { + // @ts-ignore + result = Webpack[method](...args); + } + + if (result == null || (result.$$vencordInternal != null && result.$$vencordInternal() == null)) throw new Error("Webpack Find Fail"); + } 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 if (method === "mapMangledModule") { + const failedMappings = Object.keys(args[1]).filter(key => result?.[key] == null); + + logMessage += `("${args[0]}", {\n${failedMappings.map(mapping => `\t${mapping}: ${args[1][mapping].toString().slice(0, 147)}...`).join(",\n")}\n})`; + } + 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/monacoWin.html b/src/main/monacoWin.html index 61d075ff2..ca7d0a78c 100644 --- a/src/main/monacoWin.html +++ b/src/main/monacoWin.html @@ -5,8 +5,8 @@ Vencord QuickCSS Editor @@ -29,8 +29,8 @@
@@ -38,7 +38,7 @@