import { WebSocketServer, WebSocket } from "ws"; import type { BridgeRequest, RpcResponse } from "../shared/protocol"; import { initDb, insertMessage } from "./db"; import type { MessageDTO } from "../shared/dto"; const PORT = 8787; const wss = new WebSocketServer({ host: "0.0.0.0", port: PORT, }); let whatsapp: WebSocket | undefined; const pending = new Map< string, { resolve: (value: unknown) => void; reject: (reason: unknown) => void; timer: NodeJS.Timeout; } >(); function requestId() { return crypto.randomUUID(); } function call( method: BridgeRequest["method"], params?: unknown, ): Promise { if (!whatsapp || whatsapp.readyState !== WebSocket.OPEN) { throw new Error("WhatsApp Web is not connected"); } const id = requestId(); return new Promise((resolve, reject) => { const timer = setTimeout(() => { pending.delete(id); reject(new Error(`WhatsApp request timed out: ${method}`)); }, 30_000); pending.set(id, { resolve: resolve as never, reject, timer, }); whatsapp!.send( JSON.stringify({ type: "request", id, method, params, }), ); }); } function handleWhatsAppMessage(data: string) { const message = JSON.parse(data) as | RpcResponse | { type: "event"; event: string; data: unknown; }; if (message.type === "response") { const request = pending.get(message.id); if (!request) { return; } pending.delete(message.id); clearTimeout(request.timer); if (message.ok) { const res = JSON.parse(message.result as string); request.resolve(res); } else { request.reject( new Error(message.error?.message ?? "Unknown WhatsApp error"), ); } return; } if (message.type === "event") { // console.log("[WhatsApp event]", message.event, message.data); if (message.event === "message") { const m = message.data as MessageDTO; insertMessage({ id: m.id.id || m.id._serialized || "", chatId: m.chatId || "", from: typeof m.from === "string" ? m.from : m.from?._serialized || "", to: typeof m.to === "string" ? m.to : m.to?._serialized || "", fromMe: !!m.fromMe, data: m, }); } } } wss.on("connection", (ws) => { console.log("[WS] client connected"); /* * First connection from Tampermonkey is the * WhatsApp bridge. */ if (!whatsapp && !ws.url.includes("?client=1")) { whatsapp = ws; console.log("[WS] WhatsApp bridge attached"); ws.on("message", (data) => { handleWhatsAppMessage(data.toString()); }); ws.on("close", () => { if (whatsapp === ws) { whatsapp = undefined; } console.log("[WS] WhatsApp disconnected"); }); return; } /* * This is an external application/client. * * Example: * my backend * CLI * React application * another service */ ws.on("message", async (data) => { let request: any; try { request = JSON.parse(data.toString()) as BridgeRequest; if (request.type !== "request") { return; } let result: unknown; switch (request.method) { case "status": result = await call("status", request.params); break; case "listChats": result = await call("listChats", request.params); break; case "listContacts": result = await call("listContacts", request.params); break; case "listGroups": result = await call("listGroups", request.params); break; case "getMessages": result = await call("getMessages", request.params); break; case "searchMessages": result = await call("searchMessages", request.params); break; case "sendText": result = await call("sendText", request.params); break; default: throw new Error(`Unsupported method: ${(request as any).method}`); } ws.send( JSON.stringify({ type: "response", id: request.id, ok: true, result, }), ); } catch (err) { ws.send( JSON.stringify({ type: "response", id: request?.id || null, ok: false, error: { code: "ERROR", message: err instanceof Error ? err.message : String(err), }, }), ); } }); ws.on("close", () => { console.log("[WS] external client disconnected"); }); }); initDb(); console.log(`WhatsApp bridge listening on ws://127.0.0.1:${PORT}`);