Rework core integration: validated capability layer, docs, and session state
This commit is contained in:
@@ -0,0 +1,102 @@
|
||||
/*
|
||||
* 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();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user