import type { BridgeRequest } from "../shared/protocol"; import { serialize, serializeChat, serializeContact, serializeGroupMetadata, serializeMessage, } from "./serializer"; const WS_URL = "wss://wa-bridge.rul.sh/ws"; let socket: WebSocket | undefined; function send(message: unknown) { if (socket?.readyState !== WebSocket.OPEN) { throw new Error("WebSocket is not connected"); } socket.send(JSON.stringify(message)); } function reply(id: string, result: unknown) { send({ type: "response", id, ok: true, result: serialize(result), }); } function error(id: string, err: unknown) { send({ type: "response", id, ok: false, error: { code: "ERROR", message: err instanceof Error ? err.message : String(err), }, }); } async function handleRequest(request: BridgeRequest) { switch (request.method) { case "status": return { authenticated: WPP.conn.isAuthenticated(), ready: WPP.isReady, }; case "listChats": { const chats = await WPP.chat.list({ ...(request.params?.limit != null ? { count: request.params.limit } : {}), ...(request.params?.groupsOnly ? { onlyGroups: true } : {}), ...(request.params?.usersOnly ? { onlyUsers: true } : {}), }); return chats.filter(Boolean).map(serializeChat); } case "listContacts": { const contacts = await WPP.contact.getAllContacts(); const result = request.params?.limit ? contacts.slice(0, request.params.limit) : contacts; return result.filter(Boolean).map(serializeContact); } case "listGroups": { const groups = await WPP.group.getAllGroups(); return groups.filter(Boolean).map(serializeGroupMetadata); } case "getMessages": { const messages = await WPP.chat.getMessages(request.params.chatId, { count: request.params.count ?? 50, direction: request.params.direction, id: request.params.id, }); return messages.filter(Boolean).map(serializeMessage); } case "searchMessages": return searchMessages( request.params.query, request.params.chatId, request.params.limit ?? 100, ); case "sendText": { const result = await WPP.chat.sendTextMessage( request.params.chatId, request.params.text, { delay: request.params.options?.delay, quotedMsg: request.params.options?.quotedMsg, mentionedList: request.params.options?.mentionedList, waitForAck: request.params.options?.waitForAck, }, ); return serialize(result); } default: throw new Error(`Unknown method: ${(request as any).method}`); } } /** * WA-JS doesn't need a separate "global search" API for our * first implementation. Search the messages loaded from chats. * * Later we can optimize this using WhatsApp's internal message * store/index if necessary. */ async function searchMessages(query: string, chatId?: string, limit = 100) { const normalized = query.toLowerCase(); let chats; if (chatId) { const chat = WPP.chat.get(chatId); chats = chat ? [chat] : []; } else { chats = await WPP.chat.list(); } const results: any[] = []; for (const chat of chats) { if (results.length >= limit) { break; } const id = chat?.id?._serialized ?? chat?.id ?? chat?.wid?._serialized; if (!id) { continue; } try { const messages = await WPP.chat.getMessages(id, { count: 100, }); for (const message of messages) { const body = String(message.body ?? message.caption ?? ""); if (body.toLowerCase().includes(normalized)) { results.push({ chatId: id, message: serialize(message), }); if (results.length >= limit) { break; } } } } catch (err) { console.warn("Failed searching chat", id, err); } } return results; } function connect() { if ( socket && (socket.readyState === WebSocket.OPEN || socket.readyState === WebSocket.CONNECTING) ) { return; } socket = new WebSocket(WS_URL); socket.onopen = () => { console.log("[WA Bridge] connected"); send({ type: "event", event: "ready", data: { authenticated: WPP.conn.isAuthenticated(), }, }); }; socket.onmessage = async (event) => { try { const request = JSON.parse(event.data) as BridgeRequest; if (request.type !== "request") { return; } try { const result = await handleRequest(request); reply(request.id, JSON.stringify(result)); } catch (err) { error(request.id, err); } } catch (err) { console.error("[WA Bridge] invalid message", err); } }; socket.onclose = (event) => { console.log("[WA Bridge] disconnected", { code: event.code, reason: event.reason, wasClean: event.wasClean, }); socket = undefined; setTimeout(connect, 3000); }; socket.onerror = (event) => { console.error("[WA Bridge] websocket error", event); }; } function start() { WPP.loader.onReady(() => { console.log("[WA Bridge] WA-JS ready"); connect(); /* * Forward new-message events to Node. */ WPP.chat.on("chat.new_message", (message: unknown) => { if (socket?.readyState !== WebSocket.OPEN) { return; } send({ type: "event", event: "message", data: serialize(message), }); }); }); } start();