Compare commits

...

36 commits

Author SHA1 Message Date
sadan4
6ac64e9c6d
Merge 4fb86d8430 into c7e5295da0 2024-09-18 21:34:12 +02:00
Vendicated
c7e5295da0
SearchReply => FullSearchContext ~ now adds all options back
Some checks are pending
Sync to Codeberg / codeberg (push) Waiting to run
test / test (push) Waiting to run
2024-09-18 21:33:46 +02:00
sadan4
4fb86d8430
Merge branch 'dev' into newDevTools 2024-09-17 17:20:05 -04:00
sadan
c7e8f1d7c1
guh 2024-09-17 17:00:38 -04:00
sadan4
6ea1c6e489
Merge branch 'dev' into newDevTools 2024-09-16 17:50:20 -04:00
sadan4
633e526f48
Merge branch 'dev' into newDevTools 2024-09-15 19:02:58 -04:00
sadan4
8bcfcf7ccd
Merge branch 'dev' into newDevTools 2024-09-04 20:40:00 -04:00
sadan
5c232f06a6
volumeBooster got merged, removed dupe devs entry 2024-09-01 12:46:21 -04:00
sadan4
c79e586245
Merge branch 'dev' into newDevTools 2024-09-01 00:02:33 -04:00
sadan4
a6fa38f21f
Merge branch 'dev' into newDevTools 2024-08-31 22:15:23 -04:00
sadan4
7aa32e3d78
Merge branch 'dev' into newDevTools 2024-08-30 16:04:45 -04:00
sadan4
ef21be0abb
add assets to readme.md 2024-08-29 20:29:25 -04:00
sadan
1335f0a946
add readme.md 2024-08-29 19:35:17 -04:00
sadan
eaa3072110
add function to the reporter GUI 2024-08-28 01:37:46 -04:00
sadan4
3798b628dd
Merge branch 'dev' into newDevTools 2024-08-27 17:05:55 -04:00
Sqaaakoi
b6baaf81b1 DevCompanion: opinionated hacky changes 2024-08-27 17:04:27 -04:00
sadan
5851355248
fix formatting 2024-08-27 17:02:04 -04:00
sadan4
5cdfb576f9
Merge branch 'dev' into newDevTools 2024-08-25 16:25:43 -04:00
sadan
c365c86a49
fix serialization and race cond 2024-08-23 13:25:13 -04:00
sadan4
d188a93a6c
Merge branch 'dev' into newDevTools 2024-08-23 02:15:15 -04:00
sadan
27942dc828
fix reporter data not being serializable 2024-08-22 01:43:39 -04:00
sadan
3e79fdab6e
refactor 2024-08-21 01:41:47 -04:00
sadan
4f92052d49
fix imports causing a crash and add missing if statment 2024-08-20 20:58:47 -04:00
sadan
982b52d6e9
add reload, send tasks. add reporter and build tasks 2024-08-20 19:37:44 -04:00
sadan
7fe1b2dbd4
fix after refactor && im stupid 2024-08-20 16:30:50 -04:00
sadan
813ed8af1b
add error on conflicting args 2024-08-20 16:30:23 -04:00
sadan
4a5d480acc
support regex finds and bit more work on reporter gui 2024-08-20 16:03:09 -04:00
sadan4
be739d8774
Merge branch 'dev' into newDevTools 2024-08-20 13:16:03 -04:00
sadan
da01d52e50
add diffing and start work on reporter GUI 2024-08-20 12:37:47 -04:00
sadan4
219c764680
Merge branch 'dev' into newDevTools 2024-08-19 23:15:53 -04:00
sadan
4250424040
add type, formatting and refactor 2024-08-19 23:13:21 -04:00
sadan
6c93b4d7ec
update imports 2024-08-18 10:09:38 -04:00
sadan4
d791eb0e1c
Merge branch 'dev' into newDevTools 2024-08-18 00:04:29 -04:00
sadan4
c2c5046e99
Merge branch 'dev' into newDevTools 2024-08-17 23:39:17 -04:00
sadan
39ce554b26
guh 2024-08-17 23:30:24 -04:00
sadan
49be750a1a
this is horror code 2024-08-15 11:06:16 -04:00
16 changed files with 836 additions and 302 deletions

21
.vscode/tasks.json vendored
View file

@ -12,6 +12,25 @@
"isDefault": true "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", "label": "Watch",
"type": "shell", "type": "shell",
@ -22,4 +41,4 @@
} }
} }
] ]
} }

View file

@ -21,12 +21,13 @@ import esbuild from "esbuild";
import { readdir } from "fs/promises"; import { readdir } from "fs/promises";
import { join } from "path"; 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 = { const defines = {
IS_STANDALONE, IS_STANDALONE,
IS_DEV, IS_DEV,
IS_REPORTER, IS_REPORTER,
IS_COMPANION_TEST,
IS_UPDATER_DISABLED, IS_UPDATER_DISABLED,
IS_WEB: false, IS_WEB: false,
IS_EXTENSION: false, IS_EXTENSION: false,

View file

@ -41,6 +41,9 @@ export const watch = process.argv.includes("--watch");
export const IS_DEV = watch || process.argv.includes("--dev"); export const IS_DEV = watch || process.argv.includes("--dev");
export const IS_REPORTER = process.argv.includes("--reporter"); export const IS_REPORTER = process.argv.includes("--reporter");
export const IS_STANDALONE = process.argv.includes("--standalone"); 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 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 gitHash = process.env.VENCORD_HASH || execSync("git rev-parse --short HEAD", { encoding: "utf-8" }).trim();

52
src/debug/reporterData.ts Normal file
View file

@ -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<TypeWebpackSearchHistory, string[][]>;
}
export const reporterData: ReporterData = {
failedPatches: {
foundNoModule: [],
hadNoEffect: [],
undoingPatchGroup: [],
erroredPatch: []
},
failedWebpack: {
find: [],
findByProps: [],
findByCode: [],
findStore: [],
findComponent: [],
findComponentByCode: [],
findExportedComponent: [],
waitFor: [],
waitForComponent: [],
waitForStore: [],
proxyLazyWebpack: [],
LazyComponentWebpack: [],
extractAndLoadChunks: [],
mapMangledModule: []
}
};

View file

@ -7,11 +7,12 @@
import { Logger } from "@utils/Logger"; import { Logger } from "@utils/Logger";
import * as Webpack from "@webpack"; import * as Webpack from "@webpack";
import { patches } from "plugins"; import { patches } from "plugins";
import { initWs } from "plugins/devCompanion.dev/initWs";
import { loadLazyChunks } from "./loadLazyChunks"; import { loadLazyChunks } from "./loadLazyChunks";
import { reporterData } from "./reporterData";
const ReporterLogger = new Logger("Reporter"); const ReporterLogger = new Logger("Reporter");
async function runReporter() { async function runReporter() {
try { try {
ReporterLogger.log("Starting test..."); ReporterLogger.log("Starting test...");
@ -25,6 +26,8 @@ async function runReporter() {
for (const patch of patches) { for (const patch of patches) {
if (!patch.all) { if (!patch.all) {
new Logger("WebpackInterceptor").warn(`Patch by ${patch.plugin} found no module (Module id is -): ${patch.find}`); 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})`; 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(", ")})`; 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); 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"); ReporterLogger.log("Finished test");
} catch (e) { } catch (e) {
ReporterLogger.log("A fatal error occurred:", e); ReporterLogger.log("A fatal error occurred:", e);
} }
} }
runReporter(); // imported in webpack for reporterData, wrap to avoid running reporter
if (IS_REPORTER)
runReporter();

1
src/globals.d.ts vendored
View file

@ -38,6 +38,7 @@ declare global {
export var IS_UPDATER_DISABLED: boolean; export var IS_UPDATER_DISABLED: boolean;
export var IS_DEV: boolean; export var IS_DEV: boolean;
export var IS_REPORTER: boolean; export var IS_REPORTER: boolean;
export var IS_COMPANION_TEST: boolean;
export var IS_DISCORD_DESKTOP: boolean; export var IS_DISCORD_DESKTOP: boolean;
export var IS_VESKTOP: boolean; export var IS_VESKTOP: boolean;
export var VERSION: string; export var VERSION: string;

View file

@ -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

View file

@ -16,233 +16,40 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
import { showNotification } from "@api/Notifications";
import { definePluginSettings } from "@api/Settings"; import { definePluginSettings } from "@api/Settings";
import { Devs } from "@utils/constants"; import { Devs } from "@utils/constants";
import { Logger } from "@utils/Logger"; import { Logger } from "@utils/Logger";
import { canonicalizeMatch, canonicalizeReplace } from "@utils/patches";
import definePlugin, { OptionType, ReporterTestable } from "@utils/types"; 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 NAV_ID = "dev-companion-reconnect";
const logger = new Logger("DevCompanion"); export const logger = new Logger("DevCompanion");
let socket: WebSocket | undefined; export const settings = definePluginSettings({
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<StringNode | FunctionNode>;
}
const settings = definePluginSettings({
notifyOnAutoConnect: { notifyOnAutoConnect: {
description: "Whether to notify when Dev Companion has automatically connected.", description: "Whether to notify when Dev Companion has automatically connected.",
type: OptionType.BOOLEAN, type: OptionType.BOOLEAN,
default: true 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<string, unknown>;
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({ export default definePlugin({
name: "DevCompanion", name: "DevCompanion",
description: "Dev Companion Plugin", description: "Dev Companion Plugin",
authors: [Devs.Ven], authors: [Devs.Ven, Devs.sadan, Devs.Samwich],
reporterTestable: ReporterTestable.None, reporterTestable: ReporterTestable.None,
settings, settings,
@ -254,11 +61,11 @@ export default definePlugin({
}, },
start() { start() {
initWs(); // if we're running the reporter, we need to initws in the reporter file to avoid a race condition
if (!IS_COMPANION_TEST)
initWs();
}, },
stop() { stop: stopWs,
socket?.close(1000, "Plugin Stopped");
socket = void 0;
}
}); });

View file

@ -0,0 +1,376 @@
/*
* 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<T extends SendData>(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<string, unknown>;
if (error) data.error = error;
ws.send(JSON.stringify(data));
}
function replyData<T extends SendData>(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 "rawId": {
const { id } = data;
replyData({
ok: true,
data: extractModule(id),
type: "ret"
});
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;
}
});
}

View file

@ -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<StringNode | FunctionNode>;
}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();
}

View file

@ -0,0 +1,5 @@
# FullSearchContext
Makes the message context menu in message search results have all options you'd expect.
![](https://github.com/user-attachments/assets/472d1327-3935-44c7-b7c4-0978b5348550)

View file

@ -0,0 +1,82 @@
/*
* 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 <https://www.gnu.org/licenses/>.
*/
import { migratePluginSettings } from "@api/Settings";
import { Devs } from "@utils/constants";
import definePlugin from "@utils/types";
import { findByPropsLazy } from "@webpack";
import { ChannelStore, ContextMenuApi, i18n, UserStore } from "@webpack/common";
import { Message } from "discord-types/general";
import type { MouseEvent } from "react";
const { useMessageMenu } = findByPropsLazy("useMessageMenu");
function MessageMenu({ message, channel, onHeightUpdate }) {
const canReport = message.author &&
!(message.author.id === UserStore.getCurrentUser().id || message.author.system);
return useMessageMenu({
navId: "message-actions",
ariaLabel: i18n.Messages.MESSAGE_UTILITIES_A11Y_LABEL,
message,
channel,
canReport,
onHeightUpdate,
onClose: () => ContextMenuApi.closeContextMenu(),
textSelection: "",
favoriteableType: null,
favoriteableId: null,
favoriteableName: null,
itemHref: void 0,
itemSrc: void 0,
itemSafeSrc: void 0,
itemTextContent: void 0,
});
}
migratePluginSettings("FullSearchContext", "SearchReply");
export default definePlugin({
name: "FullSearchContext",
description: "Makes the message context menu in message search results have all options you'd expect",
authors: [Devs.Ven, Devs.Aria],
patches: [{
find: "onClick:this.handleMessageClick,",
replacement: {
match: /this(?=\.handleContextMenu\(\i,\i\))/,
replace: "$self"
}
}],
handleContextMenu(event: MouseEvent, message: Message) {
const channel = ChannelStore.getChannel(message.channel_id);
if (!channel) return;
event.stopPropagation();
ContextMenuApi.openContextMenu(event, contextMenuProps =>
<MessageMenu
message={message}
channel={channel}
onHeightUpdate={contextMenuProps.onHeightUpdate}
/>
);
}
});

View file

@ -1,6 +0,0 @@
# SearchReply
Adds a reply button to search results.
![the plugin in action](https://github.com/Vendicated/Vencord/assets/45497981/07e741d3-0f97-4e5c-82b0-80712ecf2cbb)

View file

@ -1,75 +0,0 @@
/*
* 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 <https://www.gnu.org/licenses/>.
*/
import { findGroupChildrenByChildId, NavContextMenuPatchCallback } from "@api/ContextMenu";
import { ReplyIcon } from "@components/Icons";
import { Devs } from "@utils/constants";
import definePlugin from "@utils/types";
import { findByCodeLazy } from "@webpack";
import { ChannelStore, i18n, Menu, PermissionsBits, PermissionStore, SelectedChannelStore } from "@webpack/common";
import { Message } from "discord-types/general";
const replyToMessage = findByCodeLazy(".TEXTAREA_FOCUS)", "showMentionToggle:");
const messageContextMenuPatch: NavContextMenuPatchCallback = (children, { message }: { message: Message; }) => {
// make sure the message is in the selected channel
if (SelectedChannelStore.getChannelId() !== message.channel_id) return;
const channel = ChannelStore.getChannel(message?.channel_id);
if (!channel) return;
if (channel.guild_id && !PermissionStore.can(PermissionsBits.SEND_MESSAGES, channel)) return;
// dms and group chats
const dmGroup = findGroupChildrenByChildId("pin", children);
if (dmGroup && !dmGroup.some(child => child?.props?.id === "reply")) {
const pinIndex = dmGroup.findIndex(c => c?.props.id === "pin");
dmGroup.splice(pinIndex + 1, 0, (
<Menu.MenuItem
id="reply"
label={i18n.Messages.MESSAGE_ACTION_REPLY}
icon={ReplyIcon}
action={(e: React.MouseEvent) => replyToMessage(channel, message, e)}
/>
));
return;
}
// servers
const serverGroup = findGroupChildrenByChildId("mark-unread", children);
if (serverGroup && !serverGroup.some(child => child?.props?.id === "reply")) {
serverGroup.unshift((
<Menu.MenuItem
id="reply"
label={i18n.Messages.MESSAGE_ACTION_REPLY}
icon={ReplyIcon}
action={(e: React.MouseEvent) => replyToMessage(channel, message, e)}
/>
));
return;
}
};
export default definePlugin({
name: "SearchReply",
description: "Adds a reply button to search results",
authors: [Devs.Aria],
contextMenus: {
"message": messageContextMenuPatch
}
});

View file

@ -20,6 +20,7 @@ import { WEBPACK_CHUNK } from "@utils/constants";
import { Logger } from "@utils/Logger"; import { Logger } from "@utils/Logger";
import { canonicalizeReplacement } from "@utils/patches"; import { canonicalizeReplacement } from "@utils/patches";
import { PatchReplacement } from "@utils/types"; import { PatchReplacement } from "@utils/types";
import { reporterData } from "debug/reporterData";
import { WebpackInstance } from "discord-types/other"; import { WebpackInstance } from "discord-types/other";
import { traceFunction } from "../debug/Tracer"; import { traceFunction } from "../debug/Tracer";
@ -287,6 +288,11 @@ function patchFactories(factories: Record<string, (module: any, exports: any, re
if (IS_DEV) { if (IS_DEV) {
logger.debug("Function Source:\n", code); logger.debug("Function Source:\n", code);
} }
if (IS_COMPANION_TEST)
reporterData.failedPatches.hadNoEffect.push({
...patch,
id
});
} }
if (patch.group) { if (patch.group) {
@ -294,6 +300,11 @@ function patchFactories(factories: Record<string, (module: any, exports: any, re
mod = previousMod; mod = previousMod;
code = previousCode; code = previousCode;
patchedBy.delete(patch.plugin); patchedBy.delete(patch.plugin);
if (IS_COMPANION_TEST)
reporterData.failedPatches.undoingPatchGroup.push({
...patch,
id
});
break; break;
} }
@ -304,7 +315,13 @@ function patchFactories(factories: Record<string, (module: any, exports: any, re
mod = (0, eval)(`// Webpack Module ${id} - Patched by ${[...patchedBy].join(", ")}\n${newCode}\n//# sourceURL=WebpackModule${id}`); mod = (0, eval)(`// Webpack Module ${id} - Patched by ${[...patchedBy].join(", ")}\n${newCode}\n//# sourceURL=WebpackModule${id}`);
} catch (err) { } catch (err) {
logger.error(`Patch by ${patch.plugin} errored (Module id is ${id}): ${replacement.match}\n`, err); logger.error(`Patch by ${patch.plugin} errored (Module id is ${id}): ${replacement.match}\n`, err);
if (IS_COMPANION_TEST)
reporterData.failedPatches.erroredPatch.push({
...patch,
oldModule: lastCode,
newModule: code,
id
});
if (IS_DEV) { if (IS_DEV) {
const changeSize = code.length - lastCode.length; const changeSize = code.length - lastCode.length;
const match = lastCode.match(replacement.match)!; const match = lastCode.match(replacement.match)!;
@ -342,6 +359,11 @@ function patchFactories(factories: Record<string, (module: any, exports: any, re
if (patch.group) { if (patch.group) {
logger.warn(`Undoing patch group ${patch.find} by ${patch.plugin} because replacement ${replacement.match} errored`); logger.warn(`Undoing patch group ${patch.find} by ${patch.plugin} because replacement ${replacement.match} errored`);
if (IS_COMPANION_TEST)
reporterData.failedPatches.undoingPatchGroup.push({
...patch,
id
});
mod = previousMod; mod = previousMod;
code = previousCode; code = previousCode;
break; break;

View file

@ -287,7 +287,9 @@ export function findModuleFactory(...code: CodeFilter) {
return wreq.m[id]; return wreq.m[id];
} }
export const lazyWebpackSearchHistory = [] as Array<["find" | "findByProps" | "findByCode" | "findStore" | "findComponent" | "findComponentByCode" | "findExportedComponent" | "waitFor" | "waitForComponent" | "waitForStore" | "proxyLazyWebpack" | "LazyComponentWebpack" | "extractAndLoadChunks" | "mapMangledModule", any[]]>; // 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. * This is just a wrapper around {@link proxyLazy} to make our reporter test for your webpack finds.