103 lines
3.3 KiB
TypeScript
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();
|
|
}
|
|
}
|