2023-03-10 23:25:49 +00:00
|
|
|
/*
|
|
|
|
* 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 { addContextMenuPatch } from "@api/ContextMenu";
|
|
|
|
import { showNotification } from "@api/Notifications";
|
|
|
|
import { Devs } from "@utils/constants";
|
|
|
|
import Logger from "@utils/Logger";
|
|
|
|
import { canonicalizeMatch, canonicalizeReplace } from "@utils/patches";
|
|
|
|
import definePlugin from "@utils/types";
|
|
|
|
import { filters, findAll, search } from "@webpack";
|
|
|
|
import { Menu } from "@webpack/common";
|
|
|
|
|
|
|
|
const PORT = 8485;
|
|
|
|
const NAV_ID = "dev-companion-reconnect";
|
|
|
|
|
|
|
|
const logger = new Logger("DevCompanion");
|
|
|
|
|
|
|
|
let socket: WebSocket | undefined;
|
|
|
|
|
|
|
|
type Node = StringNode | RegexNode | FunctionNode;
|
|
|
|
|
|
|
|
interface StringNode {
|
|
|
|
type: "string";
|
|
|
|
value: string;
|
|
|
|
}
|
|
|
|
|
|
|
|
interface RegexNode {
|
|
|
|
type: "regex";
|
|
|
|
value: {
|
|
|
|
pattern: string;
|
|
|
|
flags: string;
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
interface FunctionNode {
|
|
|
|
type: "function";
|
|
|
|
value: string;
|
|
|
|
}
|
|
|
|
|
|
|
|
interface PatchData {
|
|
|
|
find: string;
|
|
|
|
replacement: {
|
|
|
|
match: StringNode | RegexNode;
|
|
|
|
replace: StringNode | FunctionNode;
|
|
|
|
}[];
|
|
|
|
}
|
|
|
|
|
|
|
|
interface FindData {
|
|
|
|
type: string;
|
|
|
|
args: Array<StringNode | FunctionNode>;
|
|
|
|
}
|
|
|
|
|
|
|
|
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");
|
|
|
|
|
|
|
|
showNotification({
|
|
|
|
title: "Dev Companion Connected",
|
|
|
|
body: "Connected to WebSocket"
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
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)"
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
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)"
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
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);
|
|
|
|
|
|
|
|
let src = String(candidates[keys[0]]);
|
|
|
|
|
|
|
|
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;
|
|
|
|
default:
|
|
|
|
return reply("Unknown Find Type " + type);
|
|
|
|
}
|
|
|
|
|
|
|
|
if (results.length === 0) throw "No results";
|
|
|
|
if (results.length > 1) throw "Found more than one result! Make this filter more specific";
|
|
|
|
} catch (err) {
|
|
|
|
return reply("Failed to find: " + err);
|
|
|
|
}
|
|
|
|
|
|
|
|
reply();
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
default:
|
|
|
|
reply("Unknown Type " + type);
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
export default definePlugin({
|
|
|
|
name: "DevCompanion",
|
|
|
|
description: "Dev Companion Plugin",
|
|
|
|
authors: [Devs.Ven],
|
2023-03-10 23:28:27 +00:00
|
|
|
dependencies: ["ContextMenuAPI"],
|
2023-03-10 23:25:49 +00:00
|
|
|
|
|
|
|
start() {
|
|
|
|
initWs();
|
|
|
|
addContextMenuPatch("user-settings-cog", kids => {
|
|
|
|
if (kids.some(k => k?.props?.id === NAV_ID)) return;
|
|
|
|
|
|
|
|
kids.unshift(
|
|
|
|
<Menu.MenuItem
|
|
|
|
id={NAV_ID}
|
|
|
|
label="Reconnect Dev Companion"
|
|
|
|
action={() => {
|
|
|
|
socket?.close(1000, "Reconnecting");
|
|
|
|
initWs(true);
|
|
|
|
}}
|
|
|
|
/>
|
|
|
|
);
|
|
|
|
});
|
|
|
|
},
|
|
|
|
|
|
|
|
stop() {
|
|
|
|
socket?.close(1000, "Plugin Stopped");
|
|
|
|
socket = void 0;
|
|
|
|
}
|
|
|
|
});
|