mirror of
https://github.com/Vendicated/Vencord.git
synced 2024-09-19 22:20:34 +00:00
Compare commits
36 commits
b73ef62747
...
6ac64e9c6d
Author | SHA1 | Date | |
---|---|---|---|
|
6ac64e9c6d | ||
|
c7e5295da0 | ||
|
4fb86d8430 | ||
|
c7e8f1d7c1 | ||
|
6ea1c6e489 | ||
|
633e526f48 | ||
|
8bcfcf7ccd | ||
|
5c232f06a6 | ||
|
c79e586245 | ||
|
a6fa38f21f | ||
|
7aa32e3d78 | ||
|
ef21be0abb | ||
|
1335f0a946 | ||
|
eaa3072110 | ||
|
3798b628dd | ||
|
b6baaf81b1 | ||
|
5851355248 | ||
|
5cdfb576f9 | ||
|
c365c86a49 | ||
|
d188a93a6c | ||
|
27942dc828 | ||
|
3e79fdab6e | ||
|
4f92052d49 | ||
|
982b52d6e9 | ||
|
7fe1b2dbd4 | ||
|
813ed8af1b | ||
|
4a5d480acc | ||
|
be739d8774 | ||
|
da01d52e50 | ||
|
219c764680 | ||
|
4250424040 | ||
|
6c93b4d7ec | ||
|
d791eb0e1c | ||
|
c2c5046e99 | ||
|
39ce554b26 | ||
|
49be750a1a |
16 changed files with 836 additions and 302 deletions
21
.vscode/tasks.json
vendored
21
.vscode/tasks.json
vendored
|
@ -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 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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
52
src/debug/reporterData.ts
Normal 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: []
|
||||||
|
}
|
||||||
|
};
|
|
@ -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
1
src/globals.d.ts
vendored
|
@ -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;
|
||||||
|
|
37
src/plugins/devCompanion.dev/README.md
Normal file
37
src/plugins/devCompanion.dev/README.md
Normal 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
|
|
@ -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;
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
376
src/plugins/devCompanion.dev/initWs.tsx
Normal file
376
src/plugins/devCompanion.dev/initWs.tsx
Normal 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;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
199
src/plugins/devCompanion.dev/util.tsx
Normal file
199
src/plugins/devCompanion.dev/util.tsx
Normal 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();
|
||||||
|
}
|
5
src/plugins/fullSearchContext/README.md
Normal file
5
src/plugins/fullSearchContext/README.md
Normal 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)
|
82
src/plugins/fullSearchContext/index.tsx
Normal file
82
src/plugins/fullSearchContext/index.tsx
Normal 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}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
|
@ -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)
|
|
||||||
|
|
|
@ -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
|
|
||||||
}
|
|
||||||
});
|
|
|
@ -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;
|
||||||
|
|
|
@ -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.
|
||||||
|
|
Loading…
Reference in a new issue