equicord-companion
Version:
Equicord Companion is a vscode extension to test Equicord patches & webpack finds right from the comfort of your IDE
231 lines (184 loc) • 7.3 kB
text/typescript
import { reloadDiagnostics } from "@ast/vencord/diagnostics";
import { format } from "@modules/format";
import { outputChannel } from "@modules/logging";
import { formatModule, mkStringUri } from "@modules/util";
import { Base, DiffModule, Discriminate, ExtractModuleR, FullIncomingMessage, IncomingMessage, OutgoingMessage } from "@type/server";
import { handleReporterData } from "../reporter";
import { commands, workspace } from "vscode";
import { RawData, WebSocket, WebSocketServer } from "ws";
export let wss: WebSocketServer | undefined;
export const sockets = new Set<WebSocket>();
export function hasConnectons() {
return sockets.size > 0;
}
const onConnectCbs: ((sock: WebSocket) => void)[] = [];
export function onConnect(cb: (sock: WebSocket) => void) {
onConnectCbs.push(cb);
}
export function removeOnConnect(cb: (sock: WebSocket) => void) {
const index = onConnectCbs.indexOf(cb);
if (index !== -1)
onConnectCbs.splice(index, 1);
}
onConnectCbs.push(reloadDiagnostics);
const enum CloseCode {
POLICY_VIOLATION = 1008,
}
export const moduleCache: string[] = [];
let nonceCounter = 8485;
const defaultOpts: SendToSocketsOpts = {
timeout: 5000,
};
// there is no autocomplete for this, https://github.com/microsoft/TypeScript/issues/52898
export function sendAndGetData<T extends IncomingMessage["type"] = never>(data: OutgoingMessage, opts?: SendToSocketsOpts): Promise<[T] extends [never] ? Base & { ok: true; } : Discriminate<IncomingMessage, T>> {
const { timeout } = opts ?? defaultOpts;
return new Promise((res, rej) => {
setTimeout(rej.bind(null, "Timed Out"), timeout);
sendToSockets(data, res, opts)
.catch(rej);
});
}
export interface SendToSocketsOpts {
/**
* in ms, defaults to 5000
*/
timeout: number;
}
export async function sendToSockets(data: OutgoingMessage, dataCb?: (data: any) => void, opts?: SendToSocketsOpts) {
const { timeout } = opts ?? defaultOpts;
if (sockets.size === 0) {
throw new Error("No Discord Clients Connected! Make sure you have Discord open, and have the DevCompanion plugin enabled (see README for more info!)");
}
const nonce = nonceCounter++;
(data as any).nonce = nonce;
const promises = Array.from(sockets, (sock) => new Promise<void>((resolve, reject) => {
function onMessage(data: RawData) {
const msg = data.toString("utf-8");
let parsed: FullIncomingMessage;
try {
parsed = JSON.parse(msg);
} catch {
outputChannel.error(`[WS] Invalid Response: ${msg}`);
return reject(new Error(`Got Invalid Response: ${msg}`));
}
if (parsed.nonce !== nonce)
return;
cleanup();
if (parsed.ok) {
resolve();
dataCb && dataCb(parsed);
} else {
reject(parsed.error);
}
}
function onError(err: Error) {
cleanup();
reject(err);
}
function cleanup() {
sock.off("message", onMessage);
sock.off("error", onError);
}
sock.on("message", onMessage);
sock.once("error", onError);
setTimeout(() => {
cleanup();
reject(new Error("Timed out")); // Throw a new Error object instead of a string
}, timeout);
sock.send(JSON.stringify(data));
}));
await Promise.all(promises);
return true;
}
export function startWebSocketServer() {
wss = new WebSocketServer({
port: 8485,
});
wss.on("connection", (sock, req) => {
if (req.headers.origin) {
try {
switch (new URL(req.headers.origin).hostname) {
case "discord.com":
case "canary.discord.com":
case "ptb.discord.com":
break;
default:
throw new Error("Invalid origin");
}
} catch {
outputChannel.error(`[WS] Rejected request from invalid or disallowed origin: ${req.headers.origin}`);
sock.close(CloseCode.POLICY_VIOLATION, "Invalid or disallowed origin");
return;
}
}
outputChannel.info(`[WS] New Connection (Origin: ${req.headers.origin || "-"})`);
sockets.add(sock);
onConnectCbs.forEach((cb) => cb(sock));
sock.on("close", () => {
outputChannel.warn("[WS] Connection Closed");
sockets.delete(sock);
});
sock.on("message", (msg) => {
try {
outputChannel.trace(`[WS] RECV: ${msg.toString()}`);
const rec: FullIncomingMessage = JSON.parse(msg.toString());
switch (rec.type) {
case "report": {
handleReporterData(rec.data);
break;
}
// even if this is sent, we always want to update our internal list
case "moduleList": {
const m = rec.data;
// should be something like ["123", "58913"]
moduleCache.length = 0;
moduleCache.push(...m.modules);
break;
}
default:
}
} catch (e) {
outputChannel.error(String(e));
}
});
sock.on("error", (err) => {
console.error("[Equicord Companion WS", err);
outputChannel.error(`[WS] Error: ${err}`);
});
const originalSend = sock.send;
sock.send = function (data) {
outputChannel.trace(`[WS] SEND: ${data}`);
// @ts-expect-error "Expected 3-4 arguments but got 2?????" No bestie it expects 2-3....
originalSend.call(this, data);
};
});
wss.on("error", (err) => {
console.error("[Equicord Companion WS", err);
outputChannel.error(`[WS] Error: ${err}`);
});
wss.once("listening", () => {
outputChannel.info("[WS] Listening on port 8485");
});
wss.on("close", () => {
outputChannel.warn("[WS] Closed");
});
}
export async function handleDiffPayload({ data }: DiffModule) {
const sourceUri = mkStringUri(await format(formatModule(data.source, data.moduleNumber)));
const patchedUri = mkStringUri(await format(formatModule(data.patched, data.moduleNumber)));
commands.executeCommand("vscode.diff", sourceUri, patchedUri, `Patch Diff: ${data.moduleNumber}`);
}
export async function handleExtractPayload({ data }: ExtractModuleR): Promise<void> {
if (!data.module)
return;
const moduleText = formatModule(data.module, data.moduleNumber, data.find);
workspace.openTextDocument({
content: await format(moduleText || "//ERROR: NO DATA RECIVED\n//This module may be lazy loaded"),
language: "javascript",
})
.then((e) => commands.executeCommand("vscode.open", e.uri));
}
export function stopWebSocketServer() {
wss?.close();
wss = undefined;
}