Files
Veskslop-t/poc/src/cdp-client.ts
T

103 lines
3.3 KiB
TypeScript

/*
* Veskslop — minimal Chrome DevTools Protocol client.
*
* Connects to the Discord page target exposed by Vesktop's
* --remote-debugging-port, evaluates expressions in the page, and receives
* pushed events through a CDP binding. This is the entire integration surface:
* Vesktop itself is unmodified.
*/
import WebSocket from "ws";
export type CdpListener = (event: any) => void;
interface PendingRequest {
resolve: (value: any) => void;
reject: (error: Error) => void;
}
export class CdpClient {
private ws!: WebSocket;
private nextId = 0;
private readonly pending = new Map<number, PendingRequest>();
private readonly listeners = new Set<CdpListener>();
static async connect(cdpUrl = process.env.CDP_URL ?? "http://127.0.0.1:9222"): Promise<CdpClient> {
const targets = (await (await fetch(`${cdpUrl}/json`)).json()) as any[];
const page = targets.find(target => target.type === "page" && target.url.includes("discord.com"));
if (!page) throw new Error(`No Discord page target at ${cdpUrl}`);
const client = new CdpClient();
client.ws = new WebSocket(page.webSocketDebuggerUrl);
await new Promise<void>((resolve, reject) => {
client.ws.once("open", resolve);
client.ws.once("error", reject);
});
client.ws.on("message", (raw: WebSocket.RawData) => client.onMessage(raw));
await client.send("Runtime.enable");
await client.send("Runtime.addBinding", { name: "veskslopEmit" });
return client;
}
private onMessage(raw: WebSocket.RawData) {
const message = JSON.parse(raw.toString());
if (message.id && this.pending.has(message.id)) {
const request = this.pending.get(message.id)!;
this.pending.delete(message.id);
if (message.error) request.reject(new Error(message.error.message));
else request.resolve(message.result);
return;
}
if (message.method === "Runtime.bindingCalled" && message.params?.name === "veskslopEmit") {
try {
const event = JSON.parse(message.params.payload);
for (const listener of this.listeners) listener(event);
} catch {
// ignore malformed payloads
}
}
}
send(method: string, params?: unknown): Promise<any> {
const id = ++this.nextId;
return new Promise((resolve, reject) => {
this.pending.set(id, { resolve, reject });
this.ws.send(JSON.stringify({ id, method, params }));
});
}
async evaluate<T = unknown>(expression: string): Promise<T> {
const result = await this.send("Runtime.evaluate", {
expression,
returnByValue: true,
awaitPromise: true
});
if (result.exceptionDetails) {
const description =
result.exceptionDetails.exception?.description ?? result.exceptionDetails.text ?? "evaluation failed";
throw new Error(description);
}
return result.result?.value as T;
}
onEvent(listener: CdpListener): () => void {
this.listeners.add(listener);
return () => this.listeners.delete(listener);
}
close() {
this.ws.close();
}
}