Update Dev Companion

This commit is contained in:
thororen1234 2025-01-09 02:58:32 -05:00
parent c4d64a6043
commit 872f67c19e
6 changed files with 372 additions and 133 deletions

View file

@ -47,7 +47,7 @@ export const settings = definePluginSettings({
}); });
export default definePlugin({ export default definePlugin({
name: "DevCompanion", name: "UserDevCompanion",
description: "Dev Companion Plugin", description: "Dev Companion Plugin",
authors: [Devs.Ven, Devs.sadan, Devs.Samwich], authors: [Devs.Ven, Devs.sadan, Devs.Samwich],
reporterTestable: ReporterTestable.None, reporterTestable: ReporterTestable.None,
@ -62,8 +62,9 @@ export default definePlugin({
start() { start() {
// if we're running the reporter, we need to initws in the reporter file to avoid a race condition // if we're running the reporter, we need to initws in the reporter file to avoid a race condition
if (!IS_COMPANION_TEST) if (!IS_DEV) throw new Error("This plugin requires dev mode to run, please build with pnpm build --dev");
initWs(); if (Vencord.Settings.plugins.DevCompanion?.enabled) throw new Error("Disable DevCompanion");
initWs();
}, },
stop: stopWs, stop: stopWs,

View file

@ -4,14 +4,18 @@
* SPDX-License-Identifier: GPL-3.0-or-later * SPDX-License-Identifier: GPL-3.0-or-later
*/ */
import { popNotice, showNotice } from "@api/Notices";
import ErrorBoundary from "@components/ErrorBoundary";
import { canonicalizeMatch, canonicalizeReplace } from "@utils/patches"; import { canonicalizeMatch, canonicalizeReplace } from "@utils/patches";
import { filters, findAll, search, wreq } from "@webpack"; import { filters, findAll, search, wreq } from "@webpack";
import { Toasts } from "@webpack/common"; import { React, Toasts, useState } from "@webpack/common";
import { reporterData } from "debug/reporterData"; import { loadLazyChunks } from "debug/loadLazyChunks";
import { Settings } from "Vencord"; import { Settings } from "Vencord";
import { logger, PORT, settings } from "."; import { logger, PORT, settings } from ".";
import { extractModule, extractOrThrow, FindData, findModuleId, FindType, mkRegexFind, parseNode, PatchData, SendData, toggleEnabled, } from "./util"; import { Recieve } from "./types";
import { FullOutgoingMessage, OutgoingMessage } from "./types/send";
import { extractModule, extractOrThrow, findModuleId, mkRegexFind, parseNode, toggleEnabled, } from "./util";
export function stopWs() { export function stopWs() {
socket?.close(1000, "Plugin Stopped"); socket?.close(1000, "Plugin Stopped");
@ -25,7 +29,7 @@ export function initWs(isManual = false) {
let hasErrored = false; let hasErrored = false;
const ws = socket = new WebSocket(`ws://localhost:${PORT}`); const ws = socket = new WebSocket(`ws://localhost:${PORT}`);
function replyData<T extends SendData>(data: T) { function replyData(data: OutgoingMessage) {
ws.send(JSON.stringify(data)); ws.send(JSON.stringify(data));
} }
@ -37,33 +41,27 @@ export function initWs(isManual = false) {
// send module cache to vscode // send module cache to vscode
replyData({ replyData({
type: "moduleList", type: "moduleList",
data: Object.keys(wreq.m), data: {
ok: true, modules: Object.keys(wreq.m)
},
ok: true
}); });
if (IS_COMPANION_TEST) { try {
const toSend = JSON.stringify(reporterData, (_k, v) => { if (settings.store.notifyOnAutoConnect || isManual) {
if (v instanceof RegExp) Toasts.show({
return String(v); message: "Connected to WebSocket",
return v; id: Toasts.genId(),
}); type: Toasts.Type.SUCCESS,
options: {
socket?.send(JSON.stringify({ position: Toasts.Position.TOP
type: "report", }
data: JSON.parse(toSend), });
ok: true
}));
}
(settings.store.notifyOnAutoConnect || isManual) && Toasts.show({
message: "Connected to WebSocket",
id: Toasts.genId(),
type: Toasts.Type.SUCCESS,
options: {
position: Toasts.Position.TOP
} }
}); }
catch (e) {
console.error(e);
}
}); });
ws.addEventListener("error", e => { ws.addEventListener("error", e => {
@ -100,77 +98,83 @@ export function initWs(isManual = false) {
ws.addEventListener("message", e => { ws.addEventListener("message", e => {
try { try {
var { nonce, type, data } = JSON.parse(e.data); var d = JSON.parse(e.data) as Recieve.FullIncomingMessage;
} catch (err) { } catch (err) {
logger.error("Invalid JSON:", err, "\n" + e.data); logger.error("Invalid JSON:", err, "\n" + e.data);
return; return;
} }
/**
* @param error the error to reply with. if there is no error, the reply is a sucess
*/
function reply(error?: string) { function reply(error?: string) {
const data = { nonce, ok: !error } as Record<string, unknown>; const data = { nonce: d.nonce, ok: !error } as Record<string, unknown>;
if (error) data.error = error; if (error) data.error = error;
ws.send(JSON.stringify(data)); ws.send(JSON.stringify(data));
} }
function replyData<T extends SendData>(data: T) { function replyData(data: OutgoingMessage) {
data.nonce = nonce; const toSend: FullOutgoingMessage = {
ws.send(JSON.stringify(data)); ...data,
nonce: d.nonce
};
// data.nonce = d.nonce;
ws.send(JSON.stringify(toSend));
} }
logger.info("Received Message:", type, "\n", data); logger.info("Received Message:", d.type, "\n", d.data);
switch (type) { switch (d.type) {
case "disable": { case "disable": {
const { enabled, pluginName } = data; const m = d.data;
const settings = Settings.plugins[pluginName]; const settings = Settings.plugins[m.pluginName];
if (enabled !== settings.enabled) if (m.enabled !== settings.enabled)
toggleEnabled(pluginName, reply); toggleEnabled(m.pluginName, reply);
break; break;
} }
case "rawId": { case "rawId": {
const { id } = data; const m = d.data;
replyData({ replyData({
type: "rawId",
ok: true, ok: true,
data: extractModule(id), data: extractModule(m.id),
type: "ret"
}); });
break; break;
} }
case "diff": { case "diff": {
try { try {
const { extractType, idOrSearch } = data; const m = d.data;
switch (extractType) { switch (m.extractType) {
case "id": { case "id": {
if (typeof idOrSearch !== "number") if (typeof m.idOrSearch !== "number")
throw new Error("Id is not a number, got :" + typeof idOrSearch); throw new Error("Id is not a number, got :" + typeof m.idOrSearch);
replyData({ replyData({
type: "diff", type: "diff",
ok: true, ok: true,
data: { data: {
patched: extractOrThrow(idOrSearch), patched: extractOrThrow(m.idOrSearch),
source: extractModule(idOrSearch, false) source: extractModule(m.idOrSearch, false),
moduleNumber: m.idOrSearch
}, },
moduleNumber: idOrSearch
}); });
break; break;
} }
case "search": { case "search": {
let moduleId; let moduleId: number;
if (data.findType === FindType.STRING) if (m.findType === "string")
moduleId = +findModuleId([idOrSearch.toString()]); moduleId = +findModuleId([m.idOrSearch.toString()]);
else else
moduleId = +findModuleId(mkRegexFind(idOrSearch)); moduleId = +findModuleId(mkRegexFind(m.idOrSearch));
const p = extractOrThrow(moduleId); const p = extractOrThrow(moduleId);
const p2 = extractModule(moduleId, false); const p2 = extractModule(moduleId, false);
console.log(p, p2, "done");
replyData({ replyData({
type: "diff", type: "diff",
ok: true, ok: true,
data: { data: {
patched: p, patched: p,
source: p2 source: p2,
moduleNumber: moduleId
}, },
moduleNumber: moduleId
}); });
break; break;
} }
@ -187,48 +191,51 @@ export function initWs(isManual = false) {
} }
case "extract": { case "extract": {
try { try {
const { extractType, idOrSearch } = data; const m = d.data;
switch (extractType) { switch (m.extractType) {
case "id": { case "id": {
if (typeof idOrSearch !== "number") if (typeof m.idOrSearch !== "number")
throw new Error("Id is not a number, got :" + typeof idOrSearch); throw new Error("Id is not a number, got :" + typeof m.idOrSearch);
else else
replyData({ replyData({
type: "extract", type: "extract",
ok: true, ok: true,
data: extractModule(idOrSearch), data: {
moduleNumber: idOrSearch module: extractModule(m.idOrSearch),
moduleNumber: m.idOrSearch,
},
}); });
break; break;
} }
case "search": { case "search": {
let moduleId; let moduleId;
if (data.findType === FindType.STRING) if (m.findType === "string")
moduleId = +findModuleId([idOrSearch.toString()]); moduleId = +findModuleId([m.idOrSearch.toString()]);
else else
moduleId = +findModuleId(mkRegexFind(idOrSearch)); moduleId = +findModuleId(mkRegexFind(m.idOrSearch));
replyData({ replyData({
type: "extract", type: "extract",
ok: true, ok: true,
data: extractModule(moduleId), data: {
moduleNumber: moduleId module: extractModule(moduleId),
moduleNumber: moduleId
},
}); });
break; break;
} }
case "find": { case "find": {
const { findType, findArgs } = data;
try { try {
var parsedArgs = findArgs.map(parseNode); var parsedArgs = m.findArgs.map(parseNode);
} catch (err) { } catch (err) {
return reply("Failed to parse args: " + err); return reply("Failed to parse args: " + err);
} }
try { try {
let results: any[]; let results: any[];
switch (findType.replace("find", "").replace("Lazy", "")) { switch (m.findType.replace("find", "").replace("Lazy", "")) {
case "": case "":
case "Component": case "Component":
results = findAll(parsedArgs[0]); results = findAll(parsedArgs[0]);
@ -249,7 +256,7 @@ export function initWs(isManual = false) {
results = findAll(filters.componentByCode(...parsedArgs)); results = findAll(filters.componentByCode(...parsedArgs));
break; break;
default: default:
return reply("Unknown Find Type " + findType); return reply("Unknown Find Type " + m.findType);
} }
const uniqueResultsCount = new Set(results).size; const uniqueResultsCount = new Set(results).size;
@ -260,9 +267,11 @@ export function initWs(isManual = false) {
replyData({ replyData({
type: "extract", type: "extract",
ok: true, ok: true,
find: true, data: {
data: foundFind, module: foundFind,
moduleNumber: +findModuleId([foundFind]) find: true,
moduleNumber: +findModuleId([foundFind])
},
}); });
} catch (err) { } catch (err) {
return reply("Failed to find: " + err); return reply("Failed to find: " + err);
@ -270,7 +279,7 @@ export function initWs(isManual = false) {
break; break;
} }
default: default:
reply(`Unknown Extract type. Got: ${extractType}`); reply(`Unknown Extract type. Got: ${d.data.extractType}`);
break; break;
} }
} catch (error) { } catch (error) {
@ -279,14 +288,14 @@ export function initWs(isManual = false) {
break; break;
} }
case "testPatch": { case "testPatch": {
const { find, replacement } = data as PatchData; const m = d.data;
let candidates; let candidates;
if (data.findType === FindType.REGEX) console.log(m.find.toString());
candidates = search(...mkRegexFind(find)); if (d.data.findType === "string")
candidates = search(m.find.toString());
else else
candidates = search(find.toString()); candidates = search(...mkRegexFind(m.find));
// const candidates = search(find); // const candidates = search(find);
const keys = Object.keys(candidates); const keys = Object.keys(candidates);
@ -302,7 +311,7 @@ export function initWs(isManual = false) {
let i = 0; let i = 0;
for (const { match, replace } of replacement) { for (const { match, replace } of m.replacement) {
i++; i++;
try { try {
@ -319,22 +328,20 @@ export function initWs(isManual = false) {
return reply(`Replacement ${i} failed: ${err}`); return reply(`Replacement ${i} failed: ${err}`);
} }
} }
reply(); reply();
break; break;
} }
case "testFind": { case "testFind": {
const { type, args } = data as FindData; const m = d.data;
let parsedArgs;
try { try {
parsedArgs = args.map(parseNode); var parsedArgs = m.args.map(parseNode);
} catch (err) { } catch (err) {
return reply("Failed to parse args: " + err); return reply("Failed to parse args: " + err);
} }
try { try {
let results: any[]; let results: any[];
switch (type.replace("find", "").replace("Lazy", "")) { switch (m.type.replace("find", "").replace("Lazy", "")) {
case "": case "":
case "Component": case "Component":
results = findAll(parsedArgs[0]); results = findAll(parsedArgs[0]);
@ -355,7 +362,7 @@ export function initWs(isManual = false) {
results = findAll(filters.componentByCode(...parsedArgs)); results = findAll(filters.componentByCode(...parsedArgs));
break; break;
default: default:
return reply("Unknown Find Type " + type); return reply("Unknown Find Type " + m.type);
} }
const uniqueResultsCount = new Set(results).size; const uniqueResultsCount = new Set(results).size;
@ -368,9 +375,67 @@ export function initWs(isManual = false) {
reply(); reply();
break; break;
} }
case "allModules": {
const { promise, resolve, reject } = Promise.withResolvers<void>();
// wrap in try/catch to prevent crashing if notice api is not loaded
try {
let closed = false;
const close = () => {
if (closed) return;
closed = true;
popNotice();
};
// @ts-expect-error it accepts react components
showNotice(<AllModulesNoti done={promise} close={close} />, "OK", () => {
closed = true;
popNotice();
});
} catch (e) {
console.error(e);
}
loadLazyChunks()
.then(() => {
resolve();
replyData({
type: "moduleList",
data: {
modules: Object.keys(wreq.m)
},
ok: true
});
})
.catch(e => {
console.error(e);
replyData({
type: "moduleList",
ok: false,
error: String(e),
data: null
});
reject(e);
});
break;
}
default: default:
reply("Unknown Type " + type); reply("Unknown Type " + (d as any).type);
break; break;
} }
}); });
} }
interface AllModulesNotiProps {
done: Promise<unknown>;
close: () => void;
}
const AllModulesNoti = ErrorBoundary.wrap(function ({ done, close }: AllModulesNotiProps) {
const [state, setState] = useState<0 | 1 | -1>(0);
done.then(setState.bind(null, 1)).catch(setState.bind(null, -1));
console.log("test");
if (state === 1) setTimeout(close, 5000);
return (<>
{state === 0 && "Loading lazy modules, restarting could lead to errors"}
{state === 1 && "Loaded all lazy modules"}
{state === -1 && "Failed to load lazy modules, check console for errors"}
</>);
}, { noop: true });

View file

@ -0,0 +1,7 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2024 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
export * as Recieve from "./recieve";

View file

@ -0,0 +1,140 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2024 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
// should be the same types as ./server/types/send.ts in the companion
export type SearchData =
| {
extractType: "id";
idOrSearch: number;
}
| (
| {
extractType: "search";
/**
* stringified regex
*/
idOrSearch: string;
findType: "regex";
}
| {
extractType: "search";
idOrSearch: string;
findType: "string";
}
);
export type FindOrSearchData =
| SearchData
| ({
extractType: "find";
} & _PrefixKeys<_CapitalizeKeys<FindData>, "find">);
export type AnyFindType =
`find${"Component" | "ByProps" | "Store" | "ByCode" | "ModuleId" | "ComponentByCode" | ""}${"Lazy" | ""}`;
export type StringNode = {
type: "string";
value: string;
};
export type RegexNode = {
type: "regex";
value: {
pattern: string;
flags: string;
};
};
export type FunctionNode = {
type: "function";
value: string;
};
export type FindNode = StringNode | RegexNode | FunctionNode;
export type FindData = {
type: AnyFindType;
args: FindNode[];
};
export type IncomingMessage = DisablePlugin | RawId | DiffPatch | Reload | ExtractModule | TestPatch | TestFind | AllModules;
export type FullIncomingMessage = IncomingMessage & { nonce: number; };
export type DisablePlugin = {
type: "disable";
data: {
enabled: boolean;
pluginName: string;
};
};
export type RawId = {
type: "rawId";
data: {
id: number;
};
};
export type DiffPatch = {
type: "diff";
data: SearchData;
};
export type Reload = {
type: "reload";
data: null;
};
export type ExtractModule = {
type: "extract";
// FIXME: update client code so you can just pass FindData here
data: FindOrSearchData;
};
export type TestPatch = {
type: "testPatch";
data: (
| {
findType: "string";
find: string;
}
| {
findType: "regex";
/**
* stringified regex
*/
find: string;
}
) & {
replacement: {
match: StringNode | RegexNode;
replace: StringNode | RegexNode;
}[];
};
};
export type TestFind = {
type: "testFind";
data: FindData;
};
export type AllModules = {
type: "allModules";
data: null;
};
type _PrefixKeys<
T extends Record<string, any>,
P extends string,
> = string extends P
? never
: {
[K in keyof T as K extends string ? `${P}${K}` : never]: T[K];
};
type _CapitalizeKeys<T extends Record<string, any>> = {
[K in keyof T as K extends string ? Capitalize<K> : never]: T[K];
};

View file

@ -0,0 +1,63 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2024 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
// should be the same types as src/server/types/recieve.ts in the companion
import { reporterData } from "debug/reporterData";
export type ReporterData = typeof reporterData;
export type OutgoingMessage = (Report | DiffModule | ExtractModule | ModuleList | RawId) & Base;
export type FullOutgoingMessage = OutgoingMessage & Nonce;
export type Base = {
ok: true;
} | {
ok: false;
data?: any;
error: string;
};
export type Nonce = {
nonce: number;
};
export type ModuleResult = {
moduleNumber: number;
};
// #region valid payloads
export type Report = {
type: "report";
data: ReporterData;
};
export type DiffModule = {
type: "diff";
data: {
source: string;
patched: string;
} & ModuleResult;
};
export type ExtractModule = {
type: "extract";
data: {
module: string;
/**
* if the module is incomplete. ie: from a find
*/
find?: boolean;
} & ModuleResult;
};
export type ModuleList = {
type: "moduleList";
data: {
modules: string[];
};
};
export type RawId = {
type: "rawId";
data: string;
};
// #endregion

View file

@ -11,50 +11,13 @@ import { CodeFilter, stringMatches, wreq } from "@webpack";
import { Toasts } from "@webpack/common"; import { Toasts } from "@webpack/common";
import { settings as companionSettings } from "."; import { settings as companionSettings } from ".";
import { FindNode } from "./types/recieve";
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 * extracts the patched module, if there is no patched module, throws an error
* @param id module id * @param id module id
*/ */
export function extractOrThrow(id) { export function extractOrThrow(id: number): string {
const module = wreq.m[id]; const module = wreq.m[id];
if (!module?.$$vencordPatchedSource) if (!module?.$$vencordPatchedSource)
throw new Error("No patched module found for module id " + id); throw new Error("No patched module found for module id " + id);
@ -74,7 +37,7 @@ export function extractModule(id: number, patched = companionSettings.store.useP
throw new Error("No module found for module id:" + id); throw new Error("No module found for module id:" + id);
return patched ? module.$$vencordPatchedSource ?? module.original.toString() : module.original.toString(); return patched ? module.$$vencordPatchedSource ?? module.original.toString() : module.original.toString();
} }
export function parseNode(node: Node) { export function parseNode(node: FindNode): any {
switch (node.type) { switch (node.type) {
case "string": case "string":
return node.value; return node.value;
@ -120,7 +83,7 @@ function showErrorToast(message: string) {
}); });
} }
export function toggleEnabled(name: string, beforeReload: () => void) { export function toggleEnabled(name: string, beforeReload: (error?: string) => void) {
let restartNeeded = false; let restartNeeded = false;
function onRestartNeeded() { function onRestartNeeded() {
restartNeeded = true; restartNeeded = true;