import type { GetMessagesParams, ListChatsParams, SendFileParams, SendTextParams, } from "../shared/protocol"; import type { ChatDTO, ContactDTO, GroupMetadataDTO, MessageDTO, } from "../shared/dto"; export class WhatsAppClient { isConnected: boolean = false; private url: string = ""; private socket: WebSocket | null = null; private deviceId: string | undefined = undefined; private pending = new Map< string, { resolve: (value: any) => void; reject: (error: Error) => void; timer: any; } >(); private listeners: Map void> = new Map(); constructor(url = "ws://127.0.0.1:8787", deviceId?: string) { this.url = url; this.deviceId = deviceId; } setDeviceId(id?: string | null) { this.deviceId = id || undefined; } async connect() { return new Promise((resolve) => { if (this.socket) return; this.emit("connecting"); this.socket = new WebSocket(this.url + "?client=1"); this.socket.addEventListener("open", () => { console.log("Connected to bridge"); this.isConnected = true; this.emit("status", true); resolve(); }); this.socket.addEventListener("message", (data) => { const message = JSON.parse(data.data); if (message.type === "event") { this.emit(message.event, message.data); return; } if (message.type !== "response") { return; } const pending = this.pending.get(message.id); if (!pending) { return; } this.pending.delete(message.id); clearTimeout(pending.timer); if (message.ok) { pending.resolve(message.result); } else { pending.reject(new Error(message.error?.message ?? "Unknown error")); } }); this.socket.addEventListener("close", () => { this.socket = null; this.isConnected = false; this.emit("status", false); setTimeout(() => this.connect(), 3000); }); }); } private request(method: string, params?: unknown): Promise { if (!this.isConnected || !this.socket) { throw new Error("WhatsApp is not connected"); } const id = crypto.randomUUID(); const deviceId = this.deviceId; return new Promise((resolve, reject) => { const timer = setTimeout(() => { this.pending.delete(id); reject(new Error(`Request timeout: ${method}`)); }, 30_000); this.pending.set(id, { resolve, reject, timer, }); this.socket!.send( JSON.stringify({ type: "request", deviceId, id, method, params, }), ); }); } on(event: string, callback: (...args: any[]) => void) { this.listeners.set(event, callback); } private emit(event: string, ...args: any[]) { this.listeners.get(event)?.(...args); } status() { if (!this.isConnected) { return null; } return this.request<{ authenticated: boolean; ready: boolean; }>("status"); } listDevices() { return this.request<{ id: string; name: string; lastAlive: number }[]>( "listDevices", ); } listChats(params?: ListChatsParams) { return this.request("listChats", params); } listContacts(params?: { limit?: number }) { return this.request("listContacts", params); } listGroups() { return this.request("listGroups"); } getMessages(params: GetMessagesParams) { return this.request("getMessages", params); } sendText(params: SendTextParams) { return this.request("sendText", params); } sendFile(params: SendFileParams) { return this.request("sendFile", params); } getMedia(msgId: string) { return this.request("getMedia", msgId); } exec(cmd: string) { return this.request("exec", cmd); } }