diff --git a/.vscode/tasks.json b/.vscode/tasks.json index 5b160cb84..7706f6e54 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -12,6 +12,25 @@ "isDefault": true } }, + { + // for use with the vencord companion extension + "label": "Build Companion Reporter", + "type": "shell", + "command": "pnpm build --dev --reporter --companion-test", + "presentation": { + "echo": true, + "reveal": "silent", + "panel": "shared", + "showReuseMessage": true, + "clear": true + } + }, + { + "label": "Build Dev", + "type": "shell", + "command": "pnpm build --dev", + "group": "build" + }, { "label": "Watch", "type": "shell", @@ -22,4 +41,4 @@ } } ] -} \ No newline at end of file +} diff --git a/scripts/build/build.mjs b/scripts/build/build.mjs index 623f9f940..5799ce168 100755 --- a/scripts/build/build.mjs +++ b/scripts/build/build.mjs @@ -21,12 +21,13 @@ import esbuild from "esbuild"; import { readdir } from "fs/promises"; import { join } from "path"; -import { BUILD_TIMESTAMP, commonOpts, exists, globPlugins, IS_DEV, IS_REPORTER, IS_STANDALONE, IS_UPDATER_DISABLED, resolvePluginName, VERSION, commonRendererPlugins, watch } from "./common.mjs"; +import { BUILD_TIMESTAMP, commonOpts, exists, globPlugins, IS_DEV, IS_REPORTER, IS_STANDALONE, IS_UPDATER_DISABLED, resolvePluginName, VERSION, commonRendererPlugins, watch, IS_COMPANION_TEST } from "./common.mjs"; const defines = { IS_STANDALONE, IS_DEV, IS_REPORTER, + IS_COMPANION_TEST, IS_UPDATER_DISABLED, IS_WEB: false, IS_EXTENSION: false, diff --git a/scripts/build/common.mjs b/scripts/build/common.mjs index e88f1e2b9..2ac78d579 100644 --- a/scripts/build/common.mjs +++ b/scripts/build/common.mjs @@ -41,6 +41,9 @@ export const watch = process.argv.includes("--watch"); 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_COMPANION_TEST = IS_REPORTER && process.argv.includes("--companion-test"); +if (!IS_COMPANION_TEST && process.argv.includes("--companion-test")) + console.error("--companion-test must be run with --reporter for any effect"); 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(); diff --git a/src/debug/reporterData.ts b/src/debug/reporterData.ts new file mode 100644 index 000000000..1ed7a9cbf --- /dev/null +++ b/src/debug/reporterData.ts @@ -0,0 +1,52 @@ +/* + * Vencord, a Discord client mod + * Copyright (c) 2024 Vendicated and contributors + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +/** + * this file is needed to avoid an import of plugins in ./runReporter.ts + */ +import { Patch } from "@utils/types"; +import { TypeWebpackSearchHistory } from "@webpack"; + +interface EvaledPatch extends Patch { + id: number | string; +} +interface ErroredPatch extends EvaledPatch { + oldModule: string, + newModule: string; +} +interface ReporterData { + failedPatches: { + foundNoModule: Patch[]; + hadNoEffect: EvaledPatch[]; + undoingPatchGroup: EvaledPatch[]; + erroredPatch: ErroredPatch[]; + }; + failedWebpack: Record; +} +export const reporterData: ReporterData = { + failedPatches: { + foundNoModule: [], + hadNoEffect: [], + undoingPatchGroup: [], + erroredPatch: [] + }, + failedWebpack: { + find: [], + findByProps: [], + findByCode: [], + findStore: [], + findComponent: [], + findComponentByCode: [], + findExportedComponent: [], + waitFor: [], + waitForComponent: [], + waitForStore: [], + proxyLazyWebpack: [], + LazyComponentWebpack: [], + extractAndLoadChunks: [], + mapMangledModule: [] + } +}; diff --git a/src/debug/runReporter.ts b/src/debug/runReporter.ts index ddd5e5f18..70a244f43 100644 --- a/src/debug/runReporter.ts +++ b/src/debug/runReporter.ts @@ -7,11 +7,12 @@ import { Logger } from "@utils/Logger"; import * as Webpack from "@webpack"; import { patches } from "plugins"; +import { initWs } from "plugins/devCompanion.dev/initWs"; import { loadLazyChunks } from "./loadLazyChunks"; +import { reporterData } from "./reporterData"; const ReporterLogger = new Logger("Reporter"); - async function runReporter() { try { ReporterLogger.log("Starting test..."); @@ -25,6 +26,8 @@ async function runReporter() { for (const patch of patches) { if (!patch.all) { new Logger("WebpackInterceptor").warn(`Patch by ${patch.plugin} found no module (Module id is -): ${patch.find}`); + if (IS_COMPANION_TEST) + reporterData.failedPatches.foundNoModule.push(patch); } } @@ -70,15 +73,21 @@ async function runReporter() { 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(", ")})`; - + if (IS_COMPANION_TEST) + reporterData.failedWebpack[method].push(args.map(a => String(a))); ReporterLogger.log("Webpack Find Fail:", logMessage); } } + // if we are running the reporter with companion integration, send the list to vscode as soon as we can + if(IS_COMPANION_TEST) + initWs(); ReporterLogger.log("Finished test"); } catch (e) { ReporterLogger.log("A fatal error occurred:", e); } } -runReporter(); +// imported in webpack for reporterData, wrap to avoid running reporter +if (IS_REPORTER) + runReporter(); diff --git a/src/globals.d.ts b/src/globals.d.ts index e20ca4b71..547bcaaeb 100644 --- a/src/globals.d.ts +++ b/src/globals.d.ts @@ -38,6 +38,7 @@ declare global { export var IS_UPDATER_DISABLED: boolean; export var IS_DEV: boolean; export var IS_REPORTER: boolean; + export var IS_COMPANION_TEST: boolean; export var IS_DISCORD_DESKTOP: boolean; export var IS_VESKTOP: boolean; export var VERSION: string; diff --git a/src/plugins/devCompanion.dev/README.md b/src/plugins/devCompanion.dev/README.md new file mode 100644 index 000000000..2385d11c6 --- /dev/null +++ b/src/plugins/devCompanion.dev/README.md @@ -0,0 +1,37 @@ +# Dev Companion + +## Features + +- Testing Patches +- Diffing Patches +- Extracting Webpack Modules + - From Patches + - From Finds +- Disable/Enable Plugin buttons above the definePlugin export +- Automatically run the reporter and have a gui with with the results + +## Images/Videos of the Features + +### Reporter Gui + +https://github.com/user-attachments/assets/71c45fda-5161-43b0-8b2d-6e5fae8267d2 + +### Testing Patches + +https://github.com/user-attachments/assets/99a9157e-89bb-45c7-b780-ffac30cdf4d0 + +### Diffing Patches +#### Only works for patches that are currently applied and have not errored +#### Shows every patch to that webpack module, not just yours + +https://github.com/user-attachments/assets/958f4b61-4390-47fa-9dd3-6fc888dc844d + +### Extracting Webpack Modules +#### Use the toggle in the plugin setting to default to the extracted module or the unpatched module if the module is patched + +https://github.com/user-attachments/assets/bbe308c8-af9a-4141-b387-9dcf175cfd25 + +### Disable/Enable Plugins +#### There is a plugin setting to set auto-reload after a plugin is toggled + +https://github.com/user-attachments/assets/56de9c1d-fb6d-4665-aff0-6429f80d1f15 diff --git a/src/plugins/devCompanion.dev/index.tsx b/src/plugins/devCompanion.dev/index.tsx index a495907b2..988f340dc 100644 --- a/src/plugins/devCompanion.dev/index.tsx +++ b/src/plugins/devCompanion.dev/index.tsx @@ -16,233 +16,40 @@ * along with this program. If not, see . */ -import { showNotification } from "@api/Notifications"; import { definePluginSettings } from "@api/Settings"; import { Devs } from "@utils/constants"; import { Logger } from "@utils/Logger"; -import { canonicalizeMatch, canonicalizeReplace } from "@utils/patches"; import definePlugin, { OptionType, ReporterTestable } from "@utils/types"; -import { filters, findAll, search } from "@webpack"; -const PORT = 8485; +import { initWs, socket, stopWs } from "./initWs"; +console.log("imported"); +export const PORT = 8485; const NAV_ID = "dev-companion-reconnect"; -const logger = new Logger("DevCompanion"); +export const logger = new Logger("DevCompanion"); -let socket: WebSocket | undefined; - -type Node = StringNode | RegexNode | FunctionNode; - -interface StringNode { - type: "string"; - value: string; -} - -interface RegexNode { - type: "regex"; - value: { - pattern: string; - flags: string; - }; -} - -interface FunctionNode { - type: "function"; - value: string; -} - -interface PatchData { - find: string; - replacement: { - match: StringNode | RegexNode; - replace: StringNode | FunctionNode; - }[]; -} - -interface FindData { - type: string; - args: Array; -} - -const settings = definePluginSettings({ +export const settings = definePluginSettings({ notifyOnAutoConnect: { description: "Whether to notify when Dev Companion has automatically connected.", type: OptionType.BOOLEAN, default: true + }, + usePatchedModule: { + description: "On extract requests, reply with the current patched module (if it is patched) instead of the original.", + default: true, + type: OptionType.BOOLEAN, + }, + reloadAfterToggle: { + description: "Reload after a disable/enable plugin command is recived.", + default: true, + type: OptionType.BOOLEAN } }); -function parseNode(node: Node) { - switch (node.type) { - case "string": - return node.value; - case "regex": - return new RegExp(node.value.pattern, node.value.flags); - case "function": - // We LOVE remote code execution - // Safety: This comes from localhost only, which actually means we have less permissions than the source, - // since we're running in the browser sandbox, whereas the sender has host access - return (0, eval)(node.value); - default: - throw new Error("Unknown Node Type " + (node as any).type); - } -} - -function initWs(isManual = false) { - let wasConnected = isManual; - let hasErrored = false; - const ws = socket = new WebSocket(`ws://localhost:${PORT}`); - - ws.addEventListener("open", () => { - wasConnected = true; - - logger.info("Connected to WebSocket"); - - (settings.store.notifyOnAutoConnect || isManual) && showNotification({ - title: "Dev Companion Connected", - body: "Connected to WebSocket", - noPersist: true - }); - }); - - ws.addEventListener("error", e => { - if (!wasConnected) return; - - hasErrored = true; - - logger.error("Dev Companion Error:", e); - - showNotification({ - title: "Dev Companion Error", - body: (e as ErrorEvent).message || "No Error Message", - color: "var(--status-danger, red)", - noPersist: true, - }); - }); - - ws.addEventListener("close", e => { - if (!wasConnected || hasErrored) return; - - logger.info("Dev Companion Disconnected:", e.code, e.reason); - - showNotification({ - title: "Dev Companion Disconnected", - body: e.reason || "No Reason provided", - color: "var(--status-danger, red)", - noPersist: true, - }); - }); - - ws.addEventListener("message", e => { - try { - var { nonce, type, data } = JSON.parse(e.data); - } catch (err) { - logger.error("Invalid JSON:", err, "\n" + e.data); - return; - } - - function reply(error?: string) { - const data = { nonce, ok: !error } as Record; - if (error) data.error = error; - - ws.send(JSON.stringify(data)); - } - - logger.info("Received Message:", type, "\n", data); - - switch (type) { - case "testPatch": { - const { find, replacement } = data as PatchData; - - const candidates = search(find); - const keys = Object.keys(candidates); - if (keys.length !== 1) - return reply("Expected exactly one 'find' matches, found " + keys.length); - - const mod = candidates[keys[0]]; - let src = String(mod.original ?? mod).replaceAll("\n", ""); - - if (src.startsWith("function(")) { - src = "0," + src; - } - - let i = 0; - - for (const { match, replace } of replacement) { - i++; - - try { - const matcher = canonicalizeMatch(parseNode(match)); - const replacement = canonicalizeReplace(parseNode(replace), "PlaceHolderPluginName"); - - const newSource = src.replace(matcher, replacement as string); - - if (src === newSource) throw "Had no effect"; - Function(newSource); - - src = newSource; - } catch (err) { - return reply(`Replacement ${i} failed: ${err}`); - } - } - - reply(); - break; - } - case "testFind": { - const { type, args } = data as FindData; - try { - var parsedArgs = args.map(parseNode); - } catch (err) { - return reply("Failed to parse args: " + err); - } - - try { - let results: any[]; - switch (type.replace("find", "").replace("Lazy", "")) { - case "": - results = findAll(parsedArgs[0]); - break; - case "ByProps": - results = findAll(filters.byProps(...parsedArgs)); - break; - case "Store": - results = findAll(filters.byStoreName(parsedArgs[0])); - break; - case "ByCode": - results = findAll(filters.byCode(...parsedArgs)); - break; - case "ModuleId": - results = Object.keys(search(parsedArgs[0])); - break; - case "ComponentByCode": - results = findAll(filters.componentByCode(...parsedArgs)); - break; - default: - return reply("Unknown Find Type " + type); - } - - const uniqueResultsCount = new Set(results).size; - if (uniqueResultsCount === 0) throw "No results"; - if (uniqueResultsCount > 1) throw "Found more than one result! Make this filter more specific"; - } catch (err) { - return reply("Failed to find: " + err); - } - - reply(); - break; - } - default: - reply("Unknown Type " + type); - break; - } - }); -} - export default definePlugin({ name: "DevCompanion", description: "Dev Companion Plugin", - authors: [Devs.Ven], + authors: [Devs.Ven, Devs.sadan, Devs.Samwich], reporterTestable: ReporterTestable.None, settings, @@ -254,11 +61,11 @@ export default definePlugin({ }, start() { - initWs(); + // if were running the reporter, we need to initws in the reporter file to avoid a race condition + if (!IS_COMPANION_TEST) + initWs(); }, - stop() { - socket?.close(1000, "Plugin Stopped"); - socket = void 0; - } + stop: stopWs, }); + diff --git a/src/plugins/devCompanion.dev/initWs.tsx b/src/plugins/devCompanion.dev/initWs.tsx new file mode 100644 index 000000000..3fee2e150 --- /dev/null +++ b/src/plugins/devCompanion.dev/initWs.tsx @@ -0,0 +1,367 @@ +/* + * Vencord, a Discord client mod + * Copyright (c) 2024 Vendicated and contributors + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +import { showNotification } from "@api/Notifications"; +import { canonicalizeMatch, canonicalizeReplace } from "@utils/patches"; +import { filters, findAll, search, wreq } from "@webpack"; +import { reporterData } from "debug/reporterData"; +import { Settings } from "Vencord"; + +import { logger, PORT, settings } from "."; +import { extractModule, extractOrThrow, FindData, findModuleId, FindType, mkRegexFind, parseNode, PatchData, SendData, toggleEnabled, } from "./util"; + +export function stopWs() { + socket?.close(1000, "Plugin Stopped"); + socket = void 0; +} + +export let socket: WebSocket | undefined; + +export function initWs(isManual = false) { + let wasConnected = isManual; + let hasErrored = false; + const ws = socket = new WebSocket(`ws://localhost:${PORT}`); + + function replyData(data: T) { + ws.send(JSON.stringify(data)); + } + + ws.addEventListener("open", () => { + wasConnected = true; + + logger.info("Connected to WebSocket"); + + // send module cache to vscode + replyData({ + type: "moduleList", + data: Object.keys(wreq.m), + ok: true, + }); + + if (IS_COMPANION_TEST) { + const toSend = JSON.stringify(reporterData, (_k, v) => { + if (v instanceof RegExp) + return String(v); + return v; + }); + + socket?.send(JSON.stringify({ + type: "report", + data: JSON.parse(toSend), + ok: true + })); + } + + + (settings.store.notifyOnAutoConnect || isManual) && showNotification({ + title: "Dev Companion Connected", + body: "Connected to WebSocket", + noPersist: true + }); + }); + + ws.addEventListener("error", e => { + if (!wasConnected) return; + + hasErrored = true; + + logger.error("Dev Companion Error:", e); + + showNotification({ + title: "Dev Companion Error", + body: (e as ErrorEvent).message || "No Error Message", + color: "var(--status-danger, red)", + noPersist: true + }); + }); + + ws.addEventListener("close", e => { + if (!wasConnected || hasErrored) return; + + logger.info("Dev Companion Disconnected:", e.code, e.reason); + + showNotification({ + title: "Dev Companion Disconnected", + body: e.reason || "No Reason provided", + color: "var(--status-danger, red)", + noPersist: true, + onClick() { + setTimeout(() => { + socket?.close(1000, "Reconnecting"); + initWs(true); + }, 2500); + } + }); + }); + + ws.addEventListener("message", e => { + try { + var { nonce, type, data } = JSON.parse(e.data); + } catch (err) { + logger.error("Invalid JSON:", err, "\n" + e.data); + return; + } + function reply(error?: string) { + const data = { nonce, ok: !error } as Record; + if (error) data.error = error; + + ws.send(JSON.stringify(data)); + } + function replyData(data: T) { + data.nonce = nonce; + ws.send(JSON.stringify(data)); + } + + logger.info("Received Message:", type, "\n", data); + + switch (type) { + case "disable": { + const { enabled, pluginName } = data; + const settings = Settings.plugins[pluginName]; + if (enabled !== settings.enabled) + toggleEnabled(pluginName, reply); + break; + } + case "diff": { + try { + const { extractType, idOrSearch } = data; + switch (extractType) { + case "id": { + if (typeof idOrSearch !== "number") + throw new Error("Id is not a number, got :" + typeof idOrSearch); + replyData({ + type: "diff", + ok: true, + data: { + patched: extractOrThrow(idOrSearch), + source: extractModule(idOrSearch, false) + }, + moduleNumber: idOrSearch + }); + break; + } + case "search": { + let moduleId; + if (data.findType === FindType.STRING) + moduleId = +findModuleId([idOrSearch.toString()]); + + else + moduleId = +findModuleId(mkRegexFind(idOrSearch)); + const p = extractOrThrow(moduleId); + const p2 = extractModule(moduleId, false); + console.log(p, p2, "done"); + replyData({ + type: "diff", + ok: true, + data: { + patched: p, + source: p2 + }, + moduleNumber: moduleId + }); + break; + } + } + } catch (error) { + reply(String(error)); + } + break; + } + case "reload": { + reply(); + window.location.reload(); + break; + } + case "extract": { + try { + const { extractType, idOrSearch } = data; + switch (extractType) { + case "id": { + if (typeof idOrSearch !== "number") + throw new Error("Id is not a number, got :" + typeof idOrSearch); + + else + replyData({ + type: "extract", + ok: true, + data: extractModule(idOrSearch), + moduleNumber: idOrSearch + }); + + break; + } + case "search": { + let moduleId; + if (data.findType === FindType.STRING) + moduleId = +findModuleId([idOrSearch.toString()]); + + else + moduleId = +findModuleId(mkRegexFind(idOrSearch)); + replyData({ + type: "extract", + ok: true, + data: extractModule(moduleId), + moduleNumber: moduleId + }); + break; + } + case "find": { + const { findType, findArgs } = data; + try { + var parsedArgs = findArgs.map(parseNode); + } catch (err) { + return reply("Failed to parse args: " + err); + } + + try { + let results: any[]; + switch (findType.replace("find", "").replace("Lazy", "")) { + case "": + case "Component": + results = findAll(parsedArgs[0]); + break; + case "ByProps": + results = findAll(filters.byProps(...parsedArgs)); + break; + case "Store": + results = findAll(filters.byStoreName(parsedArgs[0])); + break; + case "ByCode": + results = findAll(filters.byCode(...parsedArgs)); + break; + case "ModuleId": + results = Object.keys(search(parsedArgs[0])); + break; + case "ComponentByCode": + results = findAll(filters.componentByCode(...parsedArgs)); + break; + default: + return reply("Unknown Find Type " + findType); + } + + const uniqueResultsCount = new Set(results).size; + if (uniqueResultsCount === 0) throw "No results"; + if (uniqueResultsCount > 1) throw "Found more than one result! Make this filter more specific"; + // best name ever + const foundFind: string = [...results][0].toString(); + replyData({ + type: "extract", + ok: true, + find: true, + data: foundFind, + moduleNumber: +findModuleId([foundFind]) + }); + } catch (err) { + return reply("Failed to find: " + err); + } + break; + } + default: + reply(`Unknown Extract type. Got: ${extractType}`); + break; + } + } catch (error) { + reply(String(error)); + } + break; + } + case "testPatch": { + const { find, replacement } = data as PatchData; + + let candidates; + if (data.findType === FindType.STRING) + candidates = search(find.toString()); + + else + candidates = search(...mkRegexFind(find)); + + // const candidates = search(find); + const keys = Object.keys(candidates); + if (keys.length !== 1) + return reply("Expected exactly one 'find' matches, found " + keys.length); + + const mod = candidates[keys[0]]; + let src = String(mod.original ?? mod).replaceAll("\n", ""); + + if (src.startsWith("function(")) { + src = "0," + src; + } + + let i = 0; + + for (const { match, replace } of replacement) { + i++; + + try { + const matcher = canonicalizeMatch(parseNode(match)); + const replacement = canonicalizeReplace(parseNode(replace), "PlaceHolderPluginName"); + + const newSource = src.replace(matcher, replacement as string); + + if (src === newSource) throw "Had no effect"; + Function(newSource); + + src = newSource; + } catch (err) { + return reply(`Replacement ${i} failed: ${err}`); + } + } + + reply(); + break; + } + case "testFind": { + const { type, args } = data as FindData; + let parsedArgs; + try { + parsedArgs = args.map(parseNode); + } catch (err) { + return reply("Failed to parse args: " + err); + } + + try { + let results: any[]; + switch (type.replace("find", "").replace("Lazy", "")) { + case "": + case "Component": + results = findAll(parsedArgs[0]); + break; + case "ByProps": + results = findAll(filters.byProps(...parsedArgs)); + break; + case "Store": + results = findAll(filters.byStoreName(parsedArgs[0])); + break; + case "ByCode": + results = findAll(filters.byCode(...parsedArgs)); + break; + case "ModuleId": + results = Object.keys(search(parsedArgs[0])); + break; + case "ComponentByCode": + results = findAll(filters.componentByCode(...parsedArgs)); + break; + default: + return reply("Unknown Find Type " + type); + } + + const uniqueResultsCount = new Set(results).size; + if (uniqueResultsCount === 0) throw "No results"; + if (uniqueResultsCount > 1) throw "Found more than one result! Make this filter more specific"; + } catch (err) { + return reply("Failed to find: " + err); + } + + reply(); + break; + } + default: + reply("Unknown Type " + type); + break; + } + }); +} + diff --git a/src/plugins/devCompanion.dev/util.tsx b/src/plugins/devCompanion.dev/util.tsx new file mode 100644 index 000000000..f18496aba --- /dev/null +++ b/src/plugins/devCompanion.dev/util.tsx @@ -0,0 +1,199 @@ +/* + * Vencord, a Discord client mod + * Copyright (c) 2024 Vendicated and contributors + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +import { showNotice } from "@api/Notices"; +import { Settings } from "@api/Settings"; +import { canonicalizeMatch } from "@utils/patches"; +import { CodeFilter, stringMatches, wreq } from "@webpack"; +import { Toasts } from "@webpack/common"; + +import { settings as companionSettings } from "."; + +type Node = StringNode | RegexNode | FunctionNode; + + +export interface StringNode { + type: "string"; + value: string; +} +export interface RegexNode { + type: "regex"; + value: { + pattern: string; + flags: string; + }; +} +export enum FindType { + STRING, + REGEX +} +export interface FunctionNode { + type: "function"; + value: string; +} +export interface PatchData { + find: string; + replacement: { + match: StringNode | RegexNode; + replace: StringNode | FunctionNode; + }[]; +} +export interface FindData { + type: string; + args: Array; +}export interface SendData { + type: string; + data: any; + ok: boolean; + nonce?: number; +} +/** + * extracts the patched module, if there is no patched module, throws an error + * @param id module id + */ +export function extractOrThrow(id) { + const module = wreq.m[id]; + if (!module?.$$vencordPatchedSource) + throw new Error("No patched module found for module id " + id); + return module.$$vencordPatchedSource; +} +/** + * attempts to extract the module, throws if not found + * + * + * if patched is true and no patched module is found fallsback to the non-patched module + * @param id module id + * @param patched return the patched module + */ +export function extractModule(id: number, patched = companionSettings.store.usePatchedModule): string { + const module = wreq.m[id]; + if (!module) + throw new Error("No module found for module id:" + id); + return patched ? module.$$vencordPatchedSource ?? module.original.toString() : module.original.toString(); +} +export function parseNode(node: Node) { + switch (node.type) { + case "string": + return node.value; + case "regex": + return new RegExp(node.value.pattern, node.value.flags); + case "function": + // We LOVE remote code execution + // Safety: This comes from localhost only, which actually means we have less permissions than the source, + // since we're running in the browser sandbox, whereas the sender has host access + return (0, eval)(node.value); + default: + throw new Error("Unknown Node Type " + (node as any).type); + } +} +// we need to have our own because the one in webpack returns the first with no handling of more than one module +export function findModuleId(find: CodeFilter) { + const matches: string[] = []; + for (const id in wreq.m) { + if (stringMatches(wreq.m[id].toString(), find)) matches.push(id); + } + if (matches.length === 0) { + throw new Error("No Matches Found"); + } + if (matches.length !== 1) { + throw new Error(`This filter matches ${matches.length} modules. Make it more specific!`); + } + return matches[0]; +} +export function mkRegexFind(idOrSearch: string): RegExp[] { + const regex = idOrSearch.substring(1, idOrSearch.lastIndexOf("/")); + const flags = idOrSearch.substring(idOrSearch.lastIndexOf("/") + 1); + return [canonicalizeMatch(RegExp(regex, flags))]; +} +// the next two functions are copied from components/pluginSettings +function showErrorToast(message: string) { + Toasts.show({ + message, + type: Toasts.Type.FAILURE, + id: Toasts.genId(), + options: { + position: Toasts.Position.BOTTOM + } + }); +} + +export function toggleEnabled(name: string, beforeReload: () => void) { + let restartNeeded = false; + function onRestartNeeded() { + restartNeeded = true; + } + function beforeReturn() { + if (restartNeeded) { + if (companionSettings.store.reloadAfterToggle) { + beforeReload(); + window.location.reload(); + } + Toasts.show({ + id: Toasts.genId(), + message: "Reload Needed", + type: Toasts.Type.MESSAGE, + options: { + duration: 5000, + position: Toasts.Position.TOP + } + }); + } + } + const plugin = Vencord.Plugins.plugins[name]; + + const settings = Settings.plugins[plugin.name]; + + const isEnabled = () => settings.enabled ?? false; + + const wasEnabled = isEnabled(); + + // If we're enabling a plugin, make sure all deps are enabled recursively. + if (!wasEnabled) { + const { restartNeeded, failures } = Vencord.Plugins.startDependenciesRecursive(plugin); + if (failures.length) { + console.error(`Failed to start dependencies for ${plugin.name}: ${failures.join(", ")}`); + showNotice("Failed to start dependencies: " + failures.join(", "), "Close", () => null); + beforeReturn(); + return; + } else if (restartNeeded) { + // If any dependencies have patches, don't start the plugin yet. + settings.enabled = true; + onRestartNeeded(); + beforeReturn(); + return; + } + } + + // if the plugin has patches, dont use stopPlugin/startPlugin. Wait for restart to apply changes. + if (plugin.patches?.length) { + settings.enabled = !wasEnabled; + onRestartNeeded(); + beforeReturn(); + return; + } + + // If the plugin is enabled, but hasn't been started, then we can just toggle it off. + if (wasEnabled && !plugin.started) { + settings.enabled = !wasEnabled; + beforeReturn(); + return; + } + + const result = wasEnabled ? Vencord.Plugins.stopPlugin(plugin) : Vencord.Plugins.startPlugin(plugin); + + if (!result) { + settings.enabled = false; + + const msg = `Error while ${wasEnabled ? "stopping" : "starting"} plugin ${plugin.name}`; + console.error(msg); + showErrorToast(msg); + beforeReturn(); + return; + } + + settings.enabled = !wasEnabled; + beforeReturn(); +} diff --git a/src/webpack/patchWebpack.ts b/src/webpack/patchWebpack.ts index fb640cea8..1aee04822 100644 --- a/src/webpack/patchWebpack.ts +++ b/src/webpack/patchWebpack.ts @@ -20,6 +20,7 @@ import { WEBPACK_CHUNK } from "@utils/constants"; import { Logger } from "@utils/Logger"; import { canonicalizeReplacement } from "@utils/patches"; import { PatchReplacement } from "@utils/types"; +import { reporterData } from "debug/reporterData"; import { WebpackInstance } from "discord-types/other"; import { traceFunction } from "../debug/Tracer"; @@ -287,6 +288,11 @@ function patchFactories(factories: Record; +// FIXME: give this a better name +export type TypeWebpackSearchHistory = "find" | "findByProps" | "findByCode" | "findStore" | "findComponent" | "findComponentByCode" | "findExportedComponent" | "waitFor" | "waitForComponent" | "waitForStore" | "proxyLazyWebpack" | "LazyComponentWebpack" | "extractAndLoadChunks" | "mapMangledModule"; +export const lazyWebpackSearchHistory = [] as Array<[TypeWebpackSearchHistory, any[]]>; /** * This is just a wrapper around {@link proxyLazy} to make our reporter test for your webpack finds.