commit c78b75297f63d6693a76e898a3d7f62fe70f3ce7 Author: Khairul Hidayat Date: Mon Sep 14 17:15:00 2026 +0800 initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..780ef44 --- /dev/null +++ b/.gitignore @@ -0,0 +1,34 @@ +# dependencies (bun install) +node_modules + +# output +out +# dist +*.tgz + +# code coverage +coverage +*.lcov + +# logs +logs +_.log +report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json + +# dotenv environment variable files +.env +.env.development.local +.env.test.local +.env.production.local +.env.local + +# caches +.eslintcache +.cache +*.tsbuildinfo + +# IntelliJ based IDEs +.idea + +# Finder (MacOS) folder config +.DS_Store diff --git a/README.md b/README.md new file mode 100644 index 0000000..a7ed192 --- /dev/null +++ b/README.md @@ -0,0 +1,15 @@ +# whatsapp-bridge + +To install dependencies: + +```bash +bun install +``` + +To run: + +```bash +bun run index.ts +``` + +This project was created using `bun init` in bun v1.3.12. [Bun](https://bun.com) is a fast all-in-one JavaScript runtime. diff --git a/bun.lock b/bun.lock new file mode 100644 index 0000000..3fcfd6a --- /dev/null +++ b/bun.lock @@ -0,0 +1,26 @@ +{ + "lockfileVersion": 1, + "configVersion": 1, + "workspaces": { + "": { + "name": "whatsapp-bridge", + "devDependencies": { + "@types/bun": "latest", + }, + "peerDependencies": { + "typescript": "^5", + }, + }, + }, + "packages": { + "@types/bun": ["@types/bun@1.4.2", "", { "dependencies": { "bun-types": "1.4.2" } }, "sha512-GimotNn7+ZV0uVArItBbriZsR1oNf0+WTzPkdcFrzShI7k2norL0uzEaJT8T33dWr7O/c9ZDuAFQrctKCi72oQ=="], + + "@types/node": ["@types/node@26.5.1", "", { "dependencies": { "undici-types": "~8.9.0" } }, "sha512-CzNm2FezW4VR/LjG6yUdiEgLE/rAQ9Slj5gCu/C2VrdcW7I0ahNZ8DRbHT7zOZ6r3ONgd/bsQIeSaoDGrd1C6g=="], + + "bun-types": ["bun-types@1.4.2", "", { "dependencies": { "@types/node": "*" } }, "sha512-bxV1FgK7yBIzjRe5zBozIM4Bem11ZJcCXSrjWRG3YWLt8yFDePu4cLjpebO8OvPeIE9trbyPF4fuj3Cia4Fj3w=="], + + "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], + + "undici-types": ["undici-types@8.9.0", "", {}, "sha512-KTDyRTYX8sWmKXAikPHHSyc63CRPETMctyjKFupcC6OBLXT3xsN0e9aF7m+mIXutFWpUXuedtowG7iLOzp0kQg=="], + } +} diff --git a/dist/whatsapp-bridge.user.js b/dist/whatsapp-bridge.user.js new file mode 100644 index 0000000..6b104f2 --- /dev/null +++ b/dist/whatsapp-bridge.user.js @@ -0,0 +1,413 @@ + +// ==UserScript== +// @name WhatsApp Web Bridge +// @namespace https://rul.sh/ +// @version 0.0.1 +// @description WhatsApp Web WebSocket bridge +// @match https://web.whatsapp.com/* +// @updateURL https://example.com/my-script.user.js +// @downloadURL https://example.com/my-script.user.js +// @require https://github.com/wppconnect-team/wa-js/releases/download/nightly/wppconnect-wa.js +// @grant none +// @run-at document-idle +// ==/UserScript== + +"use strict"; +(() => { + // src/userscript/serializer.ts + function serialize(value) { + if (value == null) { + return value; + } + if (Array.isArray(value)) { + return value.map(serialize); + } + if (typeof value !== "object") { + return value; + } + const result = {}; + for (const key of Object.keys(value)) { + const v = value[key]; + if (typeof v === "string" || typeof v === "number" || typeof v === "boolean" || v === null) { + result[key] = v; + } + } + return result; + } + function safeProperty(object, key) { + try { + if (object && typeof object === "object") { + return object[key]; + } + } catch { + return void 0; + } + return void 0; + } + function safeGet(object, key) { + if (object == null || typeof object !== "object") { + return void 0; + } + try { + return object[key]; + } catch { + return void 0; + } + } + function serializeId(id) { + if (id == null) { + return null; + } + if (typeof id === "string") { + return id; + } + if (typeof id !== "object") { + return String(id); + } + const result = {}; + for (const key of [ + "_serialized", + "user", + "server", + "domain", + "device", + "agent", + "fromMe", + "remote", + "participant" + ]) { + const value = safeGet(id, key); + if (value !== void 0) { + result[key] = serialize(value); + } + } + if (!result._serialized && typeof safeGet(id, "toString") === "function") { + try { + const serialized = id.toString(); + if (serialized && serialized !== "[object Object]") { + result._serialized = serialized; + } + } catch { + } + } + return Object.keys(result).length ? result : String(id); + } + function serializeChat(chat) { + return { + id: serializeId(chat.id), + accountLid: chat.accountLid, + name: chat.name, + formattedTitle: chat.formattedTitle, + isGroup: chat.isGroup, + isUser: chat.isUser, + isReadOnly: chat.isReadOnly, + archived: chat.archived, + unreadCount: chat.unreadCount, + timestamp: chat.t, + lastMessage: chat.lastMessage ? serialize(chat.lastMessage) : null + }; + } + function serializeContact(contact) { + return { + id: serializeId(contact.id), + accountLid: contact.accountLid ?? null, + name: contact.name ?? null, + pushname: contact.pushname ?? null, + shortName: contact.shortName ?? null, + formattedName: contact.formattedName ?? null, + number: contact.number ?? null, + isMe: contact.isMe ?? false, + isUser: contact.isUser ?? false, + isBusiness: contact.isBusiness ?? false, + isEnterprise: contact.isEnterprise ?? false, + isGroup: contact.isGroup ?? false, + isMyContact: contact.isMyContact ?? false, + isBlocked: contact.isBlocked ?? false, + profilePicThumbObj: contact.profilePicThumbObj ? serialize(contact.profilePicThumbObj) : void 0 + }; + } + function serializeGroupParticipant(participant) { + return { + id: serializeId(participant.id), + isAdmin: participant.isAdmin ?? false, + isSuperAdmin: participant.isSuperAdmin ?? false, + isSuperParticipant: participant.isSuperParticipant ?? false, + isBusiness: participant.isBusiness ?? false + }; + } + function serializeGroupMetadata(metadata) { + return { + id: serializeId(metadata.id), + subject: metadata.subject ?? null, + subjectOwner: serializeId(metadata.subjectOwner), + subjectTime: metadata.subjectTime ?? null, + creation: metadata.creation ?? null, + owner: serializeId(metadata.owner), + participants: Array.isArray(metadata.participants) ? metadata.participants.map(serializeGroupParticipant).filter(Boolean) : [], + size: metadata.size ?? metadata.participants?.length ?? 0, + restrict: metadata.restrict ?? false, + announce: metadata.announce ?? false, + isCommunity: metadata.isCommunity ?? false, + isCommunityAnnounce: metadata.isCommunityAnnounce ?? false + }; + } + function serializeMessageId(id) { + if (!id) { + return null; + } + return { + id: id.id ?? null, + _serialized: id._serialized ?? null, + fromMe: id.fromMe ?? false, + remote: serializeId(id.remote), + participant: serializeId(id.participant) + }; + } + function serializeLocation(location) { + if (!location) { + return null; + } + return { + latitude: location.latitude ?? null, + longitude: location.longitude ?? null, + description: location.description ?? null, + address: location.address ?? null, + url: location.url ?? null, + name: location.name ?? null + }; + } + function serializeMessageLink(link) { + return { + link: link?.link ?? null, + isSuspicious: link?.isSuspicious ?? false + }; + } + function serializeMessage(message) { + const quotedMsgId = safeProperty(message, "quotedMsgId"); + const hasQuoted = !!quotedMsgId; + let quotedMsg = null; + if (hasQuoted) { + const rawQuoted = safeProperty(message, "quotedMsg"); + if (rawQuoted) { + quotedMsg = serializeMessage(rawQuoted); + } + } + return { + id: serializeMessageId(message.id), + chatId: message.chatId ?? message.id?.remote?._serialized ?? null, + from: serializeId(message.from), + to: serializeId(message.to), + author: serializeId(message.author), + fromMe: message.fromMe ?? false, + body: message.body ?? null, + caption: message.caption ?? null, + type: message.type ?? null, + subtype: message.subtype ?? null, + timestamp: message.timestamp ?? null, + ack: message.ack ?? null, + hasMedia: message.hasMedia ?? false, + mediaKey: message.mediaKey ?? null, + isForwarded: message.isForwarded ?? false, + forwardingScore: message.forwardingScore ?? 0, + isStarred: message.isStarred ?? false, + broadcast: message.broadcast ?? false, + mentionedIds: Array.isArray(message.mentionedIds) ? message.mentionedIds.map(serializeId) : [], + quotedMsgId: serializeMessageId(quotedMsgId), + quotedMsg, + links: Array.isArray(message.links) ? message.links.map(serializeMessageLink) : [], + location: serializeLocation(message.location), + vCard: message.vCard ?? null, + vCards: Array.isArray(message.vCards) ? message.vCards : [], + filename: message.filename ?? null, + mimetype: message.mimetype ?? null, + size: message.size ?? null, + duration: message.duration ?? null + }; + } + + // src/userscript/index.ts + var WS_URL = "ws://127.0.0.1:8787"; + var socket; + function send(message) { + if (socket?.readyState !== WebSocket.OPEN) { + throw new Error("WebSocket is not connected"); + } + socket.send(JSON.stringify(message)); + } + function reply(id, result) { + send({ + type: "response", + id, + ok: true, + result: serialize(result) + }); + } + function error(id, err) { + send({ + type: "response", + id, + ok: false, + error: { + code: "ERROR", + message: err instanceof Error ? err.message : String(err) + } + }); + } + async function handleRequest(request) { + 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.method}`); + } + } + async function searchMessages(query, chatId, 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 = []; + 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); + 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 = void 0; + setTimeout(connect, 3e3); + }; + socket.onerror = (event) => { + console.error("[WA Bridge] websocket error", event); + }; + } + function start() { + WPP.loader.onReady(() => { + console.log("[WA Bridge] WA-JS ready"); + connect(); + WPP.chat.on("chat.new_message", (message) => { + if (socket?.readyState !== WebSocket.OPEN) { + return; + } + send({ + type: "event", + event: "message", + data: serialize(message) + }); + }); + }); + } + start(); +})(); +//# sourceMappingURL=whatsapp-bridge.user.js.map diff --git a/dist/whatsapp-bridge.user.js.map b/dist/whatsapp-bridge.user.js.map new file mode 100644 index 0000000..7c5f379 --- /dev/null +++ b/dist/whatsapp-bridge.user.js.map @@ -0,0 +1,7 @@ +{ + "version": 3, + "sources": ["../src/userscript/serializer.ts", "../src/userscript/index.ts"], + "sourcesContent": ["import type {\r\n ChatDTO,\r\n ContactDTO,\r\n GroupMetadataDTO,\r\n GroupParticipantDTO,\r\n LocationDTO,\r\n MessageDTO,\r\n MessageIdDTO,\r\n SerializedId,\r\n} from \"../shared/dto\";\r\n\r\n/**\r\n * WA models contain a lot of internal properties and sometimes\r\n * circular references. Never JSON.stringify them directly.\r\n */\r\nexport function serialize(value: any): any {\r\n if (value == null) {\r\n return value;\r\n }\r\n\r\n if (Array.isArray(value)) {\r\n return value.map(serialize);\r\n }\r\n\r\n if (typeof value !== \"object\") {\r\n return value;\r\n }\r\n\r\n const result: Record = {};\r\n\r\n for (const key of Object.keys(value)) {\r\n const v = value[key];\r\n\r\n if (\r\n typeof v === \"string\" ||\r\n typeof v === \"number\" ||\r\n typeof v === \"boolean\" ||\r\n v === null\r\n ) {\r\n result[key] = v;\r\n }\r\n }\r\n\r\n return result;\r\n}\r\n\r\nfunction safeProperty(object: unknown, key: string): T | undefined {\r\n try {\r\n if (object && typeof object === \"object\") {\r\n return (object as any)[key];\r\n }\r\n } catch {\r\n return undefined;\r\n }\r\n\r\n return undefined;\r\n}\r\n\r\nexport function safeGet(\r\n object: unknown,\r\n key: string,\r\n): T | undefined {\r\n if (object == null || typeof object !== \"object\") {\r\n return undefined;\r\n }\r\n\r\n try {\r\n return (object as any)[key];\r\n } catch {\r\n return undefined;\r\n }\r\n}\r\n\r\nexport function pick(\r\n source: any,\r\n keys: readonly string[],\r\n): Partial {\r\n const result: Record = {};\r\n\r\n for (const key of keys) {\r\n const value = safeGet(source, key);\r\n\r\n if (value !== undefined) {\r\n result[key] = serialize(value);\r\n }\r\n }\r\n\r\n return result as Partial;\r\n}\r\n\r\nexport function serializeId(id: any): SerializedId | string | null {\r\n if (id == null) {\r\n return null;\r\n }\r\n\r\n if (typeof id === \"string\") {\r\n return id;\r\n }\r\n\r\n if (typeof id !== \"object\") {\r\n return String(id);\r\n }\r\n\r\n const result: SerializedId = {};\r\n\r\n for (const key of [\r\n \"_serialized\",\r\n \"user\",\r\n \"server\",\r\n \"domain\",\r\n \"device\",\r\n \"agent\",\r\n \"fromMe\",\r\n \"remote\",\r\n \"participant\",\r\n ]) {\r\n const value = safeGet(id, key);\r\n\r\n if (value !== undefined) {\r\n result[key] = serialize(value) as any;\r\n }\r\n }\r\n\r\n // Some WA objects expose useful fields that aren't\r\n // enumerable.\r\n if (!result._serialized && typeof safeGet(id, \"toString\") === \"function\") {\r\n try {\r\n const serialized = id.toString();\r\n\r\n if (serialized && serialized !== \"[object Object]\") {\r\n result._serialized = serialized;\r\n }\r\n } catch {}\r\n }\r\n\r\n return Object.keys(result).length ? result : String(id);\r\n}\r\n\r\nexport function serializeChat(chat: any): ChatDTO {\r\n return {\r\n id: serializeId(chat.id),\r\n accountLid: chat.accountLid,\r\n name: chat.name,\r\n formattedTitle: chat.formattedTitle,\r\n\r\n isGroup: chat.isGroup,\r\n isUser: chat.isUser,\r\n isReadOnly: chat.isReadOnly,\r\n archived: chat.archived,\r\n unreadCount: chat.unreadCount,\r\n\r\n timestamp: chat.t,\r\n lastMessage: chat.lastMessage ? serialize(chat.lastMessage) : null,\r\n };\r\n}\r\n\r\nexport function serializeContact(contact: any): ContactDTO {\r\n return {\r\n id: serializeId(contact.id),\r\n\r\n accountLid: contact.accountLid ?? null,\r\n name: contact.name ?? null,\r\n pushname: contact.pushname ?? null,\r\n shortName: contact.shortName ?? null,\r\n formattedName: contact.formattedName ?? null,\r\n number: contact.number ?? null,\r\n\r\n isMe: contact.isMe ?? false,\r\n isUser: contact.isUser ?? false,\r\n isBusiness: contact.isBusiness ?? false,\r\n isEnterprise: contact.isEnterprise ?? false,\r\n isGroup: contact.isGroup ?? false,\r\n isMyContact: contact.isMyContact ?? false,\r\n isBlocked: contact.isBlocked ?? false,\r\n\r\n profilePicThumbObj: contact.profilePicThumbObj\r\n ? serialize(contact.profilePicThumbObj)\r\n : undefined,\r\n };\r\n}\r\n\r\nexport function serializeGroupParticipant(\r\n participant: any,\r\n): GroupParticipantDTO {\r\n return {\r\n id: serializeId(participant.id),\r\n\r\n isAdmin: participant.isAdmin ?? false,\r\n isSuperAdmin: participant.isSuperAdmin ?? false,\r\n isSuperParticipant: participant.isSuperParticipant ?? false,\r\n isBusiness: participant.isBusiness ?? false,\r\n };\r\n}\r\n\r\nexport function serializeGroupMetadata(metadata: any): GroupMetadataDTO {\r\n return {\r\n id: serializeId(metadata.id),\r\n\r\n subject: metadata.subject ?? null,\r\n subjectOwner: serializeId(metadata.subjectOwner),\r\n subjectTime: metadata.subjectTime ?? null,\r\n creation: metadata.creation ?? null,\r\n owner: serializeId(metadata.owner),\r\n\r\n participants: Array.isArray(metadata.participants)\r\n ? (metadata.participants\r\n .map(serializeGroupParticipant)\r\n .filter(Boolean) as GroupParticipantDTO[])\r\n : [],\r\n\r\n size: metadata.size ?? metadata.participants?.length ?? 0,\r\n restrict: metadata.restrict ?? false,\r\n announce: metadata.announce ?? false,\r\n isCommunity: metadata.isCommunity ?? false,\r\n isCommunityAnnounce: metadata.isCommunityAnnounce ?? false,\r\n };\r\n}\r\nexport function serializeMessageId(id: any): MessageIdDTO | null {\r\n if (!id) {\r\n return null;\r\n }\r\n\r\n return {\r\n id: id.id ?? null,\r\n _serialized: id._serialized ?? null,\r\n fromMe: id.fromMe ?? false,\r\n remote: serializeId(id.remote),\r\n participant: serializeId(id.participant),\r\n };\r\n}\r\nexport function serializeLocation(location: any): LocationDTO | null {\r\n if (!location) {\r\n return null;\r\n }\r\n\r\n return {\r\n latitude: location.latitude ?? null,\r\n longitude: location.longitude ?? null,\r\n description: location.description ?? null,\r\n address: location.address ?? null,\r\n url: location.url ?? null,\r\n name: location.name ?? null,\r\n };\r\n}\r\nexport function serializeMessageLink(link: any): any {\r\n return {\r\n link: link?.link ?? null,\r\n isSuspicious: link?.isSuspicious ?? false,\r\n };\r\n}\r\nexport function serializeMessage(message: any): MessageDTO {\r\n const quotedMsgId = safeProperty(message, \"quotedMsgId\");\r\n const hasQuoted = !!quotedMsgId;\r\n let quotedMsg = null;\r\n\r\n if (hasQuoted) {\r\n const rawQuoted = safeProperty(message, \"quotedMsg\");\r\n if (rawQuoted) {\r\n quotedMsg = serializeMessage(rawQuoted);\r\n }\r\n }\r\n\r\n return {\r\n id: serializeMessageId(message.id)!,\r\n chatId: message.chatId ?? message.id?.remote?._serialized ?? null,\r\n from: serializeId(message.from),\r\n to: serializeId(message.to),\r\n author: serializeId(message.author),\r\n fromMe: message.fromMe ?? false,\r\n body: message.body ?? null,\r\n caption: message.caption ?? null,\r\n type: message.type ?? null,\r\n subtype: message.subtype ?? null,\r\n timestamp: message.timestamp ?? null,\r\n ack: message.ack ?? null,\r\n hasMedia: message.hasMedia ?? false,\r\n mediaKey: message.mediaKey ?? null,\r\n isForwarded: message.isForwarded ?? false,\r\n forwardingScore: message.forwardingScore ?? 0,\r\n isStarred: message.isStarred ?? false,\r\n broadcast: message.broadcast ?? false,\r\n mentionedIds: Array.isArray(message.mentionedIds)\r\n ? message.mentionedIds.map(serializeId)\r\n : [],\r\n quotedMsgId: serializeMessageId(quotedMsgId),\r\n quotedMsg,\r\n links: Array.isArray(message.links)\r\n ? message.links.map(serializeMessageLink)\r\n : [],\r\n location: serializeLocation(message.location),\r\n vCard: message.vCard ?? null,\r\n vCards: Array.isArray(message.vCards) ? message.vCards : [],\r\n filename: message.filename ?? null,\r\n mimetype: message.mimetype ?? null,\r\n size: message.size ?? null,\r\n duration: message.duration ?? null,\r\n };\r\n}\r\n", "import type { BridgeRequest } from \"../shared/protocol\";\r\nimport {\r\n serialize,\r\n serializeChat,\r\n serializeContact,\r\n serializeGroupMetadata,\r\n serializeMessage,\r\n} from \"./serializer\";\r\n\r\nconst WS_URL = \"ws://127.0.0.1:8787\";\r\n\r\nlet socket: WebSocket | undefined;\r\n\r\nfunction send(message: unknown) {\r\n if (socket?.readyState !== WebSocket.OPEN) {\r\n throw new Error(\"WebSocket is not connected\");\r\n }\r\n\r\n socket.send(JSON.stringify(message));\r\n}\r\n\r\nfunction reply(id: string, result: unknown) {\r\n send({\r\n type: \"response\",\r\n id,\r\n ok: true,\r\n result: serialize(result),\r\n });\r\n}\r\n\r\nfunction error(id: string, err: unknown) {\r\n send({\r\n type: \"response\",\r\n id,\r\n ok: false,\r\n error: {\r\n code: \"ERROR\",\r\n message: err instanceof Error ? err.message : String(err),\r\n },\r\n });\r\n}\r\n\r\nasync function handleRequest(request: BridgeRequest) {\r\n switch (request.method) {\r\n case \"status\":\r\n return {\r\n authenticated: WPP.conn.isAuthenticated(),\r\n ready: WPP.isReady,\r\n };\r\n\r\n case \"listChats\": {\r\n const chats = await WPP.chat.list({\r\n ...(request.params?.limit != null\r\n ? { count: request.params.limit }\r\n : {}),\r\n ...(request.params?.groupsOnly ? { onlyGroups: true } : {}),\r\n ...(request.params?.usersOnly ? { onlyUsers: true } : {}),\r\n });\r\n\r\n return chats.filter(Boolean).map(serializeChat);\r\n }\r\n\r\n case \"listContacts\": {\r\n const contacts = await WPP.contact.getAllContacts();\r\n\r\n const result = request.params?.limit\r\n ? contacts.slice(0, request.params.limit)\r\n : contacts;\r\n\r\n return result.filter(Boolean).map(serializeContact);\r\n }\r\n\r\n case \"listGroups\": {\r\n const groups = await WPP.group.getAllGroups();\r\n\r\n return groups.filter(Boolean).map(serializeGroupMetadata);\r\n }\r\n\r\n case \"getMessages\": {\r\n const messages = await WPP.chat.getMessages(request.params.chatId, {\r\n count: request.params.count ?? 50,\r\n direction: request.params.direction,\r\n id: request.params.id,\r\n });\r\n\r\n return messages.filter(Boolean).map(serializeMessage);\r\n }\r\n\r\n case \"searchMessages\":\r\n return searchMessages(\r\n request.params.query,\r\n request.params.chatId,\r\n request.params.limit ?? 100,\r\n );\r\n\r\n case \"sendText\": {\r\n const result = await WPP.chat.sendTextMessage(\r\n request.params.chatId,\r\n request.params.text,\r\n {\r\n delay: request.params.options?.delay,\r\n quotedMsg: request.params.options?.quotedMsg,\r\n mentionedList: request.params.options?.mentionedList,\r\n waitForAck: request.params.options?.waitForAck,\r\n },\r\n );\r\n\r\n return serialize(result);\r\n }\r\n\r\n default:\r\n throw new Error(`Unknown method: ${(request as any).method}`);\r\n }\r\n}\r\n\r\n/**\r\n * WA-JS doesn't need a separate \"global search\" API for our\r\n * first implementation. Search the messages loaded from chats.\r\n *\r\n * Later we can optimize this using WhatsApp's internal message\r\n * store/index if necessary.\r\n */\r\nasync function searchMessages(query: string, chatId?: string, limit = 100) {\r\n const normalized = query.toLowerCase();\r\n\r\n let chats;\r\n\r\n if (chatId) {\r\n const chat = WPP.chat.get(chatId);\r\n chats = chat ? [chat] : [];\r\n } else {\r\n chats = await WPP.chat.list();\r\n }\r\n\r\n const results: any[] = [];\r\n\r\n for (const chat of chats) {\r\n if (results.length >= limit) {\r\n break;\r\n }\r\n\r\n const id = chat?.id?._serialized ?? chat?.id ?? chat?.wid?._serialized;\r\n\r\n if (!id) {\r\n continue;\r\n }\r\n\r\n try {\r\n const messages = await WPP.chat.getMessages(id, {\r\n count: 100,\r\n });\r\n\r\n for (const message of messages) {\r\n const body = String(message.body ?? message.caption ?? \"\");\r\n\r\n if (body.toLowerCase().includes(normalized)) {\r\n results.push({\r\n chatId: id,\r\n message: serialize(message),\r\n });\r\n\r\n if (results.length >= limit) {\r\n break;\r\n }\r\n }\r\n }\r\n } catch (err) {\r\n console.warn(\"Failed searching chat\", id, err);\r\n }\r\n }\r\n\r\n return results;\r\n}\r\n\r\nfunction connect() {\r\n if (\r\n socket &&\r\n (socket.readyState === WebSocket.OPEN ||\r\n socket.readyState === WebSocket.CONNECTING)\r\n ) {\r\n return;\r\n }\r\n\r\n socket = new WebSocket(WS_URL);\r\n\r\n socket.onopen = () => {\r\n console.log(\"[WA Bridge] connected\");\r\n\r\n send({\r\n type: \"event\",\r\n event: \"ready\",\r\n data: {\r\n authenticated: WPP.conn.isAuthenticated(),\r\n },\r\n });\r\n };\r\n\r\n socket.onmessage = async (event) => {\r\n try {\r\n const request = JSON.parse(event.data) as BridgeRequest;\r\n\r\n if (request.type !== \"request\") {\r\n return;\r\n }\r\n\r\n try {\r\n const result = await handleRequest(request);\r\n\r\n reply(request.id, JSON.stringify(result));\r\n } catch (err) {\r\n error(request.id, err);\r\n }\r\n } catch (err) {\r\n console.error(\"[WA Bridge] invalid message\", err);\r\n }\r\n };\r\n\r\n socket.onclose = (event) => {\r\n console.log(\"[WA Bridge] disconnected\", {\r\n code: event.code,\r\n reason: event.reason,\r\n wasClean: event.wasClean,\r\n });\r\n\r\n socket = undefined;\r\n setTimeout(connect, 3000);\r\n };\r\n\r\n socket.onerror = (event) => {\r\n console.error(\"[WA Bridge] websocket error\", event);\r\n };\r\n}\r\n\r\nfunction start() {\r\n WPP.loader.onReady(() => {\r\n console.log(\"[WA Bridge] WA-JS ready\");\r\n\r\n connect();\r\n\r\n /*\r\n * Forward new-message events to Node.\r\n */\r\n WPP.chat.on(\"chat.new_message\", (message: unknown) => {\r\n if (socket?.readyState !== WebSocket.OPEN) {\r\n return;\r\n }\r\n\r\n send({\r\n type: \"event\",\r\n event: \"message\",\r\n data: serialize(message),\r\n });\r\n });\r\n });\r\n}\r\n\r\nstart();\r\n"], + "mappings": "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", + "names": [] +} diff --git a/esbuild.mjs b/esbuild.mjs new file mode 100644 index 0000000..1eb5a7d --- /dev/null +++ b/esbuild.mjs @@ -0,0 +1,32 @@ +import * as esbuild from "esbuild"; + +await esbuild.build({ + entryPoints: ["src/userscript/index.ts"], + outfile: "dist/whatsapp-bridge.user.js", + + bundle: true, + format: "iife", + platform: "browser", + target: "es2020", + + banner: { + js: ` +// ==UserScript== +// @name WhatsApp Web Bridge +// @namespace https://rul.sh/ +// @version 0.0.1 +// @description WhatsApp Web WebSocket bridge +// @match https://web.whatsapp.com/* +// @updateURL https://example.com/my-script.user.js +// @downloadURL https://example.com/my-script.user.js +// @require https://github.com/wppconnect-team/wa-js/releases/download/nightly/wppconnect-wa.js +// @grant none +// @run-at document-idle +// ==/UserScript== +`, + }, + + sourcemap: true, +}); + +console.log("Built dist/whatsapp-bridge.user.js"); diff --git a/package.json b/package.json new file mode 100644 index 0000000..f267e00 --- /dev/null +++ b/package.json @@ -0,0 +1,24 @@ +{ + "name": "whatsapp-bridge", + "module": "index.ts", + "type": "module", + "private": true, + "scripts": { + "build": "node esbuild.mjs", + "server": "bun run src/server/main.ts" + }, + "devDependencies": { + "@types/bun": "latest", + "@types/node": "^26.5.1", + "@types/ws": "^8.18.1", + "@wppconnect/wa-js": "^4.6.0", + "esbuild": "^0.28.2", + "typescript": "^7.0.2" + }, + "peerDependencies": { + "typescript": "^5" + }, + "dependencies": { + "ws": "^8.21.3" + } +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml new file mode 100644 index 0000000..5427c13 --- /dev/null +++ b/pnpm-lock.yaml @@ -0,0 +1,565 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + dependencies: + ws: + specifier: ^8.21.3 + version: 8.21.3 + devDependencies: + '@types/bun': + specifier: latest + version: 1.4.2 + '@types/node': + specifier: ^26.5.1 + version: 26.5.1 + '@types/ws': + specifier: ^8.18.1 + version: 8.18.1 + '@wppconnect/wa-js': + specifier: ^4.6.0 + version: 4.6.0 + esbuild: + specifier: ^0.28.2 + version: 0.28.2 + typescript: + specifier: ^7.0.2 + version: 7.0.2 + +packages: + + '@esbuild/aix-ppc64@0.28.2': + resolution: {integrity: sha512-XExcO+dvLKvVtNTibSTBej1NCAbaGhWn9Ww1ZPx80qsahhPFe/8jgWP0IchNe0F3HwkU7n8ejhH8bjonqht8mQ==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + + '@esbuild/android-arm64@0.28.2': + resolution: {integrity: sha512-5YfKeeI8qWfBZIX+u2xZC3Zlb3Os/gLS2sbEKM+I4ZOcsWmHS2WLysCcQZDAFRslDUU5Oiq44gf6PYN1vGwG5A==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm@0.28.2': + resolution: {integrity: sha512-kXXoiPVVGQcnIYGOeaovwOURpniDBpSq4A03qkQ+BMQqtGG6HYap3xne9C1O1yo4TR3qxlCX5IqqmX6fFo2Lqg==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + + '@esbuild/android-x64@0.28.2': + resolution: {integrity: sha512-O387ite7SzUyCcy3JQX4P4bLtEA7bLLkx+esve5JHnyYfNTxcVpXZo9jhdB0lTKN44gztELTdU7nS8Nr16Fs1Q==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + + '@esbuild/darwin-arm64@0.28.2': + resolution: {integrity: sha512-n4KqkOQrraxHJcgjM1RvwbigfQKIKJVpM7xp+KsxiyUSrRdIXnt73VhrPAx0fV44hgfmIVKjxMN9J1t5jySVkw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-x64@0.28.2': + resolution: {integrity: sha512-uq6suIWYP37qzGddBKPw5QEQPi6HiLGsO7UmkpfyaYNQ3D+rN6w6WfwH+nuqcGXWvawGwxOEroO4YGnFh95azw==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + + '@esbuild/freebsd-arm64@0.28.2': + resolution: {integrity: sha512-n+I0BTSRIoy+d6RPKnEVwql5UwBJolytvY4mAOIEJorKlqgPII8ix6slVVrfZ5Tnj7glIZvloylbB/EJPMWEXw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.28.2': + resolution: {integrity: sha512-78XJTJkvPs0kz2w61301PJjXl4g7q3JqiYMZ/M/yVI73EHBrCRTgkhu9oqG7vPqq+a/yadEW8aD+agKlk5xrmg==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + + '@esbuild/linux-arm64@0.28.2': + resolution: {integrity: sha512-pW4AC0P3it8c7do9MVM4p51FzHzdM/TZrerurgRcHJ2WTa1VQ1CIq18xncfpBJw4ojkiZZrKW2yIBWBP92j6Ug==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm@0.28.2': + resolution: {integrity: sha512-XlDnu2q5yoqems+xay6wSAcg9DDD7K9RLKZEBOMZm3ckNpJBvOX20tSfby8KfrrhINDyv9V2YVZKY/SpoGJI8w==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-ia32@0.28.2': + resolution: {integrity: sha512-CYbnj78HsIeA+DhgUKgFCfvNsTHFhMMrinUrMZpDXJXKN8T3XViTZ/+wtHeVxEWY8ewSzTFN+nRmSwO2tZaLUQ==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-loong64@0.28.2': + resolution: {integrity: sha512-buwkd8nsph4R+ajRvw0qM5Hja/TXQow3ptzWO2EbG/cqcIkHloRrdlBtQlshyYGTNFvfkfJ5tpPLVkY4DtsPfQ==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-mips64el@0.28.2': + resolution: {integrity: sha512-ZVykbDyk7519VwiNb9Lcj9m8XM6v5V9uKPvrEMkkEedVewf+0itkhahp4HDpgERXhwLRpWFypsGbG/J8s0QjJA==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-ppc64@0.28.2': + resolution: {integrity: sha512-CAXl+Dtd9UUuJd8pKKdwh6MLm3MUMiqMPmhZ3tTSXPqfyQ3vDl6R5hZdZ/kYojK4ofXtdfSv1tFq8XzWx3heNQ==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-riscv64@0.28.2': + resolution: {integrity: sha512-GeXCej4IQtU1B+QlDV8W/RRvbzI3O/Stss+/bCXv4lZls5WGRtu2a+3JkA3i4qIUlMXpcHebWpF8AkJhATowuA==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-s390x@0.28.2': + resolution: {integrity: sha512-3H1weTYZPxt/WOhByszQZybS9w5lKzUn1FDMsgEChbHWQwHYQQRfBxgCcZvPhjHfKyJjIievvMmEUawJrdY9Dg==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-x64@0.28.2': + resolution: {integrity: sha512-4xTZr1FUmSoQW4XIWmit3tzQrUTZM+N3P0XV8xROKYF50XfI7xeO90+1bZvNwxIufQ9hDQVRJH5YhgPVF8A/HQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + + '@esbuild/netbsd-arm64@0.28.2': + resolution: {integrity: sha512-sSATRjPeDBg3pdgHoQfoYBob11Kk1FGa9lui5RIHZCoCkJa9QKlvl3/vKz2usCmYYjs7ymJR/2Nnsqe+Hjt5nw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] + + '@esbuild/netbsd-x64@0.28.2': + resolution: {integrity: sha512-lqnzCV+mM0gIADaKihiCg6ifgfU2L3h5E33rNQBN1Y4MaVGnzryzmvvf7UHxprpQdE8hpqLolJ9Rl+SkIRDpyw==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + + '@esbuild/openbsd-arm64@0.28.2': + resolution: {integrity: sha512-AL2qJILH7lNjrDmCQDvdxMfAUIv8KMNZOvrwAQ8i8//ntL9FflhOyMJ8OZSMBb8/AWXe3/5v5S20y3zCoZWKoQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + + '@esbuild/openbsd-x64@0.28.2': + resolution: {integrity: sha512-QtiuPytchRyC4rwUKhexJdQKvDuZ6hWloi3igqPQNUJCS1/v9EiO3UTOXR6A3FoMo4fnAKbWJdqaIwhOzh8qEw==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + + '@esbuild/openharmony-arm64@0.28.2': + resolution: {integrity: sha512-WkhYDmpTjLvGlScA1rwjRUmhl4k8oXR3cIbtqWmELgU/dFeHHlEllxDvdWcNJV9rbzCexB5vz8gtNewWLgCT7Q==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openharmony] + + '@esbuild/sunos-x64@0.28.2': + resolution: {integrity: sha512-GPMSkTOtMnv2U2F8gxe4Io6qmVs+YKyp832Etqqxr0hFngmXQ3rzwytelm3GIn7T4VviRUlf3sOgBOiTdvaf7g==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + + '@esbuild/win32-arm64@0.28.2': + resolution: {integrity: sha512-PIhhEkE9uPBleRBrQEJpUn7MBnibZzbGzYWPmY3x+YoVg/95zbjB4CxPPOQ8l5tYYM4mMaCthF8/1DIfBQQyWQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-ia32@0.28.2': + resolution: {integrity: sha512-YmJbfTlvU7Sdn9BB+4PRES4oB6pxgS37MAONj+hBr/cpXS1aBPKXxNnDbu+QCWPj0o9dgyxeq79g6c5P8KeuYA==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-x64@0.28.2': + resolution: {integrity: sha512-5ebpxr3nWMzrL/rnUI755Jkuee0bHL/Gq0WTF9lvcpv73wAp5eu8MfBUgWK9bhWvZjj7yX8etf/8tI8Ney695g==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + + '@types/bun@1.4.2': + resolution: {integrity: sha512-GimotNn7+ZV0uVArItBbriZsR1oNf0+WTzPkdcFrzShI7k2norL0uzEaJT8T33dWr7O/c9ZDuAFQrctKCi72oQ==} + + '@types/node@26.5.1': + resolution: {integrity: sha512-CzNm2FezW4VR/LjG6yUdiEgLE/rAQ9Slj5gCu/C2VrdcW7I0ahNZ8DRbHT7zOZ6r3ONgd/bsQIeSaoDGrd1C6g==} + + '@types/ws@8.18.1': + resolution: {integrity: sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==} + + '@typescript/typescript-aix-ppc64@7.0.2': + resolution: {integrity: sha512-MTKKkWB7p/0E9xi1d1tHtZ5PiLkGEMIq88pK2CubZjOsLtYTLqhgIgi6zepFa+9GHZ6h05NMCkQxGKiPXMxXtQ==} + engines: {node: '>=16.20.0'} + cpu: [ppc64] + os: [aix] + + '@typescript/typescript-darwin-arm64@7.0.2': + resolution: {integrity: sha512-gowzar9MwS/aRWp6f3a4KUqzRjAZjOsmGNCM6LcTgXum+dBfgsBVMN+AgvOCCbguXyick6LJhpBszxMebJ8syA==} + engines: {node: '>=16.20.0'} + cpu: [arm64] + os: [darwin] + + '@typescript/typescript-darwin-x64@7.0.2': + resolution: {integrity: sha512-SZ9xZInqApNlNGc9s0W1VSsktYSOe9cFqNOIqmN1Gs8SmkjKZYFt017G4VwPxASInODuAdbTW7sXiFUf893RgA==} + engines: {node: '>=16.20.0'} + cpu: [x64] + os: [darwin] + + '@typescript/typescript-freebsd-arm64@7.0.2': + resolution: {integrity: sha512-W5NH4y/J0plIIS5b2xvTEkU7JFxyqdMAOgf+Ilhl0vHQXKO5dZoxd+C/jEtq56c4F3wk71RB4BMRQ2XdI+bwYQ==} + engines: {node: '>=16.20.0'} + cpu: [arm64] + os: [freebsd] + + '@typescript/typescript-freebsd-x64@7.0.2': + resolution: {integrity: sha512-UMGDx5sTpzNw3WiPebH7l90IWfJggEd+egHt/q6p7/Cm3zqoV7VxkGXt+3DxPIw8CcmvAB0j3sVVfbhX+M4Tpw==} + engines: {node: '>=16.20.0'} + cpu: [x64] + os: [freebsd] + + '@typescript/typescript-linux-arm64@7.0.2': + resolution: {integrity: sha512-Qh4eU4/y3yDjnfjjyPYihMj5/ODIlmt+Bzu17OI+fiSRDW57QmU5SiN63exPRNJPKUzcc1INa1NXdrJ+MqHjUQ==} + engines: {node: '>=16.20.0'} + cpu: [arm64] + os: [linux] + + '@typescript/typescript-linux-arm@7.0.2': + resolution: {integrity: sha512-gffT3xPz9sR7j/YJExkyPntrI0P2EP9XbOyWzth2/Gs0RstK+90RBcO0ncXoXy/beYll1SXw846Nf2zdnEz0QQ==} + engines: {node: '>=16.20.0'} + cpu: [arm] + os: [linux] + + '@typescript/typescript-linux-loong64@7.0.2': + resolution: {integrity: sha512-uEHck9i8hoAzXPiYRib1O7miOnz23SxIeVl6F4LXox+qov1K35jHcEW6VHKvZI+pyvl7fZEP4MCU5LYvIq1GuQ==} + engines: {node: '>=16.20.0'} + cpu: [loong64] + os: [linux] + + '@typescript/typescript-linux-mips64el@7.0.2': + resolution: {integrity: sha512-R4KvAMnE43W5Qeqb0Ly56O3mWMWIAgsMyz36DCaycd5nbg/9kzm0liw3JocfRqyJY0KPmzFjbswozXyW0DnIYA==} + engines: {node: '>=16.20.0'} + cpu: [mips64el] + os: [linux] + + '@typescript/typescript-linux-ppc64@7.0.2': + resolution: {integrity: sha512-DORx5b3sd/4S7eayxm4FQv+A7CrkUIGRaHiwI8oiHTAI1fAPWhF4J0vAlkC8biAlHSVVwxMQ3tjZ2/DVbnQiiA==} + engines: {node: '>=16.20.0'} + cpu: [ppc64] + os: [linux] + + '@typescript/typescript-linux-riscv64@7.0.2': + resolution: {integrity: sha512-wf0jqEDOjrPRnKwYRyyJDRo11KMbvMFrU+q4zqKyChODBzvlkbhNQfKvLxQCcwTpdDaXSHZTVuh0JoCrKCUMHQ==} + engines: {node: '>=16.20.0'} + cpu: [riscv64] + os: [linux] + + '@typescript/typescript-linux-s390x@7.0.2': + resolution: {integrity: sha512-IkwJc3L7yhytWd/ewjyxNDfOmswCm9GWMJT/ue/dU4aZNbwZeYAetq42VyLmsmSjvoX7z74X6ZaYCtzAr0EuGw==} + engines: {node: '>=16.20.0'} + cpu: [s390x] + os: [linux] + + '@typescript/typescript-linux-x64@7.0.2': + resolution: {integrity: sha512-EYdf2cNg7rgCWJnxCdJ+F3V39O8ihb37eHAu1LK8oAFizgTQbPOK7zHHXbPt8rX24COqODXeI3sIf0fCXG7H/A==} + engines: {node: '>=16.20.0'} + cpu: [x64] + os: [linux] + + '@typescript/typescript-netbsd-arm64@7.0.2': + resolution: {integrity: sha512-+polYF4MF04aPpO5FTkHran9yUQDSXqy5GiSDKpsll5jy3l3+g9QLhpf39T+ePtefhXLOGrLl0QIjkQP6VnelA==} + engines: {node: '>=16.20.0'} + cpu: [arm64] + os: [netbsd] + + '@typescript/typescript-netbsd-x64@7.0.2': + resolution: {integrity: sha512-8YIT0EHM/3dq10ZOVF/A7pc/YSMtbcecct4rWtexrnSCHOPcpC2KTLXfTCR6vDpnSiY12heNb1GiN/wu+T/FyA==} + engines: {node: '>=16.20.0'} + cpu: [x64] + os: [netbsd] + + '@typescript/typescript-openbsd-arm64@7.0.2': + resolution: {integrity: sha512-APT8+ClYnuYm1u9+kgGXoMj2VzWzcymwh2gNSQVySHfkRDGOTVkoWLjCmOQSaO+PoqQ57B0flRp9SA+7GnnkzQ==} + engines: {node: '>=16.20.0'} + cpu: [arm64] + os: [openbsd] + + '@typescript/typescript-openbsd-x64@7.0.2': + resolution: {integrity: sha512-yX7s+Q0Dln0Dt9tEzZsAjXXR/+ytBM7AlglaqyeMPxQszJ1JhlJdZ6jLA+IzldHtflX81em7lDao1xXu+aRRkg==} + engines: {node: '>=16.20.0'} + cpu: [x64] + os: [openbsd] + + '@typescript/typescript-sunos-x64@7.0.2': + resolution: {integrity: sha512-dLJDGaLZ1D4HPQn62u1n8mBDkJREwMsAkCdkwd4Ieqw+x3TUyTsqY0YiBCtE6H6OzzgGk3iuZ3vFWRS+E8/d1g==} + engines: {node: '>=16.20.0'} + cpu: [x64] + os: [sunos] + + '@typescript/typescript-win32-arm64@7.0.2': + resolution: {integrity: sha512-Gyl1Vy6OsWesLzmq+EP0Fb7b4Nid5232AvcA2SFcdYreldpNtYFFofPjnt62y9hQy7VTaZp65ICJjuAQRaVcIQ==} + engines: {node: '>=16.20.0'} + cpu: [arm64] + os: [win32] + + '@typescript/typescript-win32-x64@7.0.2': + resolution: {integrity: sha512-0BQ3HkAHHlKLSp1qRvf3SUhGpGsDuhB/jgFw75guyqbxJqEaS0Cw/VFO8i2nHglJUzQCRtMMR/IBAKE3ETMC4g==} + engines: {node: '>=16.20.0'} + cpu: [x64] + os: [win32] + + '@wppconnect/wa-js@4.6.0': + resolution: {integrity: sha512-epf+ucZO9c25dCaGraslVNHB1mRjid7rfijZhKbalXed/gbDzi6u69wdHCgPbcZW9McVpKmVW+3AzffI39p2SQ==} + engines: {whatsapp-web: '>=2.3000.1038792969-alpha'} + + bun-types@1.4.2: + resolution: {integrity: sha512-bxV1FgK7yBIzjRe5zBozIM4Bem11ZJcCXSrjWRG3YWLt8yFDePu4cLjpebO8OvPeIE9trbyPF4fuj3Cia4Fj3w==} + + esbuild@0.28.2: + resolution: {integrity: sha512-HKVLS8dvII+xoKW9kmqxbRKrnWEXfJJr/FZhhJmiqIB0e053QNYFqOBouTMO/k5sID4MvCiUCvv8b9M4h32wIA==} + engines: {node: '>=18'} + hasBin: true + + typescript@7.0.2: + resolution: {integrity: sha512-8FYau96o3NKOhbjKi/qNvG/W5jhzxkbdm5sj9AbZ/5T5sWqn3hJgLfGx27sRKZWTvyzCP8dLRBTf5tBTSRVUNA==} + engines: {node: '>=16.20.0'} + hasBin: true + + undici-types@8.9.0: + resolution: {integrity: sha512-KTDyRTYX8sWmKXAikPHHSyc63CRPETMctyjKFupcC6OBLXT3xsN0e9aF7m+mIXutFWpUXuedtowG7iLOzp0kQg==} + + ws@8.21.3: + resolution: {integrity: sha512-201TZ/kPWxoPr/OKWjquZR1SWKXcvxdH+e1xrx89b3YbmzLMFCLfnaG1HFIgWzJOEWZ7MvpK++odZufgYR50Rw==} + engines: {node: '>=10.0.0'} + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: '>=5.0.2' + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + +snapshots: + + '@esbuild/aix-ppc64@0.28.2': + optional: true + + '@esbuild/android-arm64@0.28.2': + optional: true + + '@esbuild/android-arm@0.28.2': + optional: true + + '@esbuild/android-x64@0.28.2': + optional: true + + '@esbuild/darwin-arm64@0.28.2': + optional: true + + '@esbuild/darwin-x64@0.28.2': + optional: true + + '@esbuild/freebsd-arm64@0.28.2': + optional: true + + '@esbuild/freebsd-x64@0.28.2': + optional: true + + '@esbuild/linux-arm64@0.28.2': + optional: true + + '@esbuild/linux-arm@0.28.2': + optional: true + + '@esbuild/linux-ia32@0.28.2': + optional: true + + '@esbuild/linux-loong64@0.28.2': + optional: true + + '@esbuild/linux-mips64el@0.28.2': + optional: true + + '@esbuild/linux-ppc64@0.28.2': + optional: true + + '@esbuild/linux-riscv64@0.28.2': + optional: true + + '@esbuild/linux-s390x@0.28.2': + optional: true + + '@esbuild/linux-x64@0.28.2': + optional: true + + '@esbuild/netbsd-arm64@0.28.2': + optional: true + + '@esbuild/netbsd-x64@0.28.2': + optional: true + + '@esbuild/openbsd-arm64@0.28.2': + optional: true + + '@esbuild/openbsd-x64@0.28.2': + optional: true + + '@esbuild/openharmony-arm64@0.28.2': + optional: true + + '@esbuild/sunos-x64@0.28.2': + optional: true + + '@esbuild/win32-arm64@0.28.2': + optional: true + + '@esbuild/win32-ia32@0.28.2': + optional: true + + '@esbuild/win32-x64@0.28.2': + optional: true + + '@types/bun@1.4.2': + dependencies: + bun-types: 1.4.2 + + '@types/node@26.5.1': + dependencies: + undici-types: 8.9.0 + + '@types/ws@8.18.1': + dependencies: + '@types/node': 26.5.1 + + '@typescript/typescript-aix-ppc64@7.0.2': + optional: true + + '@typescript/typescript-darwin-arm64@7.0.2': + optional: true + + '@typescript/typescript-darwin-x64@7.0.2': + optional: true + + '@typescript/typescript-freebsd-arm64@7.0.2': + optional: true + + '@typescript/typescript-freebsd-x64@7.0.2': + optional: true + + '@typescript/typescript-linux-arm64@7.0.2': + optional: true + + '@typescript/typescript-linux-arm@7.0.2': + optional: true + + '@typescript/typescript-linux-loong64@7.0.2': + optional: true + + '@typescript/typescript-linux-mips64el@7.0.2': + optional: true + + '@typescript/typescript-linux-ppc64@7.0.2': + optional: true + + '@typescript/typescript-linux-riscv64@7.0.2': + optional: true + + '@typescript/typescript-linux-s390x@7.0.2': + optional: true + + '@typescript/typescript-linux-x64@7.0.2': + optional: true + + '@typescript/typescript-netbsd-arm64@7.0.2': + optional: true + + '@typescript/typescript-netbsd-x64@7.0.2': + optional: true + + '@typescript/typescript-openbsd-arm64@7.0.2': + optional: true + + '@typescript/typescript-openbsd-x64@7.0.2': + optional: true + + '@typescript/typescript-sunos-x64@7.0.2': + optional: true + + '@typescript/typescript-win32-arm64@7.0.2': + optional: true + + '@typescript/typescript-win32-x64@7.0.2': + optional: true + + '@wppconnect/wa-js@4.6.0': {} + + bun-types@1.4.2: + dependencies: + '@types/node': 26.5.1 + + esbuild@0.28.2: + optionalDependencies: + '@esbuild/aix-ppc64': 0.28.2 + '@esbuild/android-arm': 0.28.2 + '@esbuild/android-arm64': 0.28.2 + '@esbuild/android-x64': 0.28.2 + '@esbuild/darwin-arm64': 0.28.2 + '@esbuild/darwin-x64': 0.28.2 + '@esbuild/freebsd-arm64': 0.28.2 + '@esbuild/freebsd-x64': 0.28.2 + '@esbuild/linux-arm': 0.28.2 + '@esbuild/linux-arm64': 0.28.2 + '@esbuild/linux-ia32': 0.28.2 + '@esbuild/linux-loong64': 0.28.2 + '@esbuild/linux-mips64el': 0.28.2 + '@esbuild/linux-ppc64': 0.28.2 + '@esbuild/linux-riscv64': 0.28.2 + '@esbuild/linux-s390x': 0.28.2 + '@esbuild/linux-x64': 0.28.2 + '@esbuild/netbsd-arm64': 0.28.2 + '@esbuild/netbsd-x64': 0.28.2 + '@esbuild/openbsd-arm64': 0.28.2 + '@esbuild/openbsd-x64': 0.28.2 + '@esbuild/openharmony-arm64': 0.28.2 + '@esbuild/sunos-x64': 0.28.2 + '@esbuild/win32-arm64': 0.28.2 + '@esbuild/win32-ia32': 0.28.2 + '@esbuild/win32-x64': 0.28.2 + + typescript@7.0.2: + optionalDependencies: + '@typescript/typescript-aix-ppc64': 7.0.2 + '@typescript/typescript-darwin-arm64': 7.0.2 + '@typescript/typescript-darwin-x64': 7.0.2 + '@typescript/typescript-freebsd-arm64': 7.0.2 + '@typescript/typescript-freebsd-x64': 7.0.2 + '@typescript/typescript-linux-arm': 7.0.2 + '@typescript/typescript-linux-arm64': 7.0.2 + '@typescript/typescript-linux-loong64': 7.0.2 + '@typescript/typescript-linux-mips64el': 7.0.2 + '@typescript/typescript-linux-ppc64': 7.0.2 + '@typescript/typescript-linux-riscv64': 7.0.2 + '@typescript/typescript-linux-s390x': 7.0.2 + '@typescript/typescript-linux-x64': 7.0.2 + '@typescript/typescript-netbsd-arm64': 7.0.2 + '@typescript/typescript-netbsd-x64': 7.0.2 + '@typescript/typescript-openbsd-arm64': 7.0.2 + '@typescript/typescript-openbsd-x64': 7.0.2 + '@typescript/typescript-sunos-x64': 7.0.2 + '@typescript/typescript-win32-arm64': 7.0.2 + '@typescript/typescript-win32-x64': 7.0.2 + + undici-types@8.9.0: {} + + ws@8.21.3: {} diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml new file mode 100644 index 0000000..efc037a --- /dev/null +++ b/pnpm-workspace.yaml @@ -0,0 +1,2 @@ +onlyBuiltDependencies: + - esbuild diff --git a/src/client/test-client.ts b/src/client/test-client.ts new file mode 100644 index 0000000..f07ef09 --- /dev/null +++ b/src/client/test-client.ts @@ -0,0 +1,42 @@ +import { WhatsAppClient } from "./whatsapp"; + +const wa = new WhatsAppClient("ws://127.0.0.1:8787"); +await wa.connect(); + +const chats = await wa.listChats({ + limit: 10, + groupsOnly: true, + // usersOnly: true, +}); + +console.log( + chats.map((i) => ({ + id: typeof i.id === "object" ? i.id?._serialized : i.id, + lid: i.accountLid, + name: i.formattedTitle, + })), +); + +const messages = await wa.getMessages({ + chatId: "120363401241111568@g.us", + count: 5, +}); + +console.log( + messages.map((i) => ({ + id: i.id.id, + from: i.from, + text: i.body, + })), +); + +// const result = await wa.searchMessages({ +// query: "pasien", +// chatId: "628123456789@c.us", +// limit: 20, +// }); + +// await wa.sendText({ +// chatId: "628123456789@c.us", +// text: "Hello from Node!", +// }); diff --git a/src/client/whatsapp.ts b/src/client/whatsapp.ts new file mode 100644 index 0000000..eba7501 --- /dev/null +++ b/src/client/whatsapp.ts @@ -0,0 +1,138 @@ +import WebSocket from "ws"; + +import type { + GetMessagesParams, + ListChatsParams, + SearchMessagesParams, + SendTextParams, +} from "../shared/protocol"; +import type { + ChatDTO, + ContactDTO, + GroupMetadataDTO, + MessageDTO, +} from "../shared/dto"; + +export class WhatsAppClient { + private url: string = ""; + private isConnected: boolean = false; + private socket: WebSocket | null = null; + + private pending = new Map< + string, + { + resolve: (value: any) => void; + reject: (error: Error) => void; + timer: NodeJS.Timeout; + } + >(); + + constructor(url = "ws://127.0.0.1:8787") { + this.url = url; + } + + async connect() { + return new Promise((resolve, reject) => { + if (this.socket) return; + + this.socket = new WebSocket(this.url); + + this.socket.on("open", () => { + console.log("Connected to bridge"); + this.isConnected = true; + resolve(); + }); + + this.socket.on("message", (data) => { + const message = JSON.parse(data.toString()); + + 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.on("close", () => { + this.socket = null; + this.isConnected = 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(); + + 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", + id, + method, + params, + }), + ); + }); + } + + status() { + return this.request<{ + authenticated: boolean; + ready: boolean; + }>("status"); + } + + 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); + } + + searchMessages(params: SearchMessagesParams) { + return this.request("searchMessages", params); + } + + sendText(params: SendTextParams) { + return this.request("sendText", params); + } +} diff --git a/src/server/main.ts b/src/server/main.ts new file mode 100644 index 0000000..ce875d2 --- /dev/null +++ b/src/server/main.ts @@ -0,0 +1,206 @@ +import { WebSocketServer, WebSocket } from "ws"; + +import type { BridgeRequest, RpcResponse } from "../shared/protocol"; + +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); + } +} + +wss.on("connection", (ws) => { + console.log("[WS] client connected"); + + /* + * First connection from Tampermonkey is the + * WhatsApp bridge. + */ + if (!whatsapp) { + 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"); + }); +}); + +console.log(`WhatsApp bridge listening on ws://127.0.0.1:${PORT}`); diff --git a/src/shared/dto.ts b/src/shared/dto.ts new file mode 100644 index 0000000..21e27d5 --- /dev/null +++ b/src/shared/dto.ts @@ -0,0 +1,199 @@ +export type JsonValue = + | string + | number + | boolean + | null + | JsonValue[] + | { + [key: string]: JsonValue; + }; + +export interface SerializedId { + _serialized?: string; + user?: string; + server?: string; + domain?: string; + device?: string; + agent?: string; + [key: string]: JsonValue | undefined; +} + +export interface ContactDTO { + id: SerializedId | string | null; + + accountLid?: string | null; + + name?: string | null; + pushname?: string | null; + shortName?: string | null; + formattedName?: string | null; + + number?: string | null; + + isMe?: boolean; + isUser?: boolean; + isBusiness?: boolean; + isEnterprise?: boolean; + isGroup?: boolean; + + isMyContact?: boolean; + isBlocked?: boolean; + + profilePicThumbObj?: unknown; +} + +export interface ChatDTO { + id: SerializedId | string | null; + + accountLid?: string | null; + + name?: string | null; + formattedTitle?: string | null; + + isGroup?: boolean; + isUser?: boolean; + isNewsletter?: boolean; + isCommunity?: boolean; + + isReadOnly?: boolean; + isAnnounceGrpRestrict?: boolean; + + archived?: boolean; + pinned?: boolean; + muted?: boolean; + + unreadCount?: number; + unreadMentionsCount?: number; + + timestamp?: number | null; + lastMessage?: MessageDTO | null; + contact?: ContactDTO | null; + groupMetadata?: GroupMetadataDTO | null; +} + +export interface GroupMetadataDTO { + id: SerializedId | string | null; + + subject?: string | null; + subjectOwner?: SerializedId | string | null; + + subjectTime?: number | null; + creation?: number | null; + + owner?: SerializedId | string | null; + + participants?: GroupParticipantDTO[]; + + size?: number; + + restrict?: boolean; + announce?: boolean; + + isCommunity?: boolean; + isCommunityAnnounce?: boolean; +} + +export interface GroupParticipantDTO { + id: SerializedId | string | null; + + isAdmin?: boolean; + isSuperAdmin?: boolean; + isSuperParticipant?: boolean; + + isBusiness?: boolean; +} + +export interface MessageDTO { + id: MessageIdDTO; + + chatId?: string | null; + + from?: SerializedId | string | null; + to?: SerializedId | string | null; + + author?: SerializedId | string | null; + + fromMe?: boolean; + + body?: string | null; + caption?: string | null; + + type?: string | null; + subtype?: string | null; + + timestamp?: number | null; + + ack?: number | null; + + hasMedia?: boolean; + mediaKey?: string | null; + + isForwarded?: boolean; + forwardingScore?: number; + + isStarred?: boolean; + + broadcast?: boolean; + + mentionedIds?: (SerializedId | string)[]; + + quotedMsgId?: MessageIdDTO | null; + + quotedMsg?: MessageDTO | null; + + links?: MessageLinkDTO[]; + + location?: LocationDTO | null; + + vCard?: string | null; + vCards?: string[]; + + filename?: string | null; + mimetype?: string | null; + size?: number | null; + + duration?: number | null; +} + +export interface MessageIdDTO { + id?: string | null; + _serialized?: string | null; + + fromMe?: boolean; + remote?: SerializedId | string | null; + participant?: SerializedId | string | null; +} + +export interface MessageLinkDTO { + link?: string | null; + isSuspicious?: boolean; +} + +export interface LocationDTO { + latitude?: number | null; + longitude?: number | null; + + description?: string | null; + address?: string | null; + + url?: string | null; + + name?: string | null; +} + +export interface SentMessageDTO { + id: MessageIdDTO | null; + + ack?: number | null; + + timestamp?: number | null; + + chatId?: string | null; + + from?: SerializedId | string | null; + to?: SerializedId | string | null; + + body?: string | null; + + type?: string | null; +} diff --git a/src/shared/protocol.ts b/src/shared/protocol.ts new file mode 100644 index 0000000..fe6a34f --- /dev/null +++ b/src/shared/protocol.ts @@ -0,0 +1,71 @@ +export type RequestId = string; + +export interface RpcRequest { + type: "request"; + id: RequestId; + method: M; + params: P; +} + +export interface RpcResponse { + type: "response"; + id: RequestId; + ok: boolean; + result?: T; + error?: { + code: string; + message: string; + }; +} + +export interface RpcEvent { + type: "event"; + event: string; + data: T; +} + +export type ServerMessage = RpcResponse | RpcEvent; + +export interface ListChatsParams { + limit?: number; + groupsOnly?: boolean; + usersOnly?: boolean; +} + +export interface GetMessagesParams { + chatId: string; + count?: number; + direction?: "before" | "after"; + id?: string; +} + +export interface SearchMessagesParams { + query: string; + chatId?: string; + limit?: number; +} + +export interface SendTextParams { + chatId: string; + text: string; + + options?: { + delay?: number; + quotedMsg?: string; + mentionedList?: string[]; + waitForAck?: boolean; + }; +} + +export interface GetContactsParams { + limit?: number; +} + +export type BridgeRequest = + | RpcRequest<"status"> + | RpcRequest<"listChats", ListChatsParams> + | RpcRequest<"listContacts", GetContactsParams> + | RpcRequest<"listGroups"> + | RpcRequest<"getMessages", GetMessagesParams> + | RpcRequest<"searchMessages", SearchMessagesParams> + | RpcRequest<"sendText", SendTextParams>; diff --git a/src/userscript/index.ts b/src/userscript/index.ts new file mode 100644 index 0000000..9f93c81 --- /dev/null +++ b/src/userscript/index.ts @@ -0,0 +1,257 @@ +import type { BridgeRequest } from "../shared/protocol"; +import { + serialize, + serializeChat, + serializeContact, + serializeGroupMetadata, + serializeMessage, +} from "./serializer"; + +const WS_URL = "ws://127.0.0.1:8787"; + +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(); diff --git a/src/userscript/serializer.ts b/src/userscript/serializer.ts new file mode 100644 index 0000000..3ca8330 --- /dev/null +++ b/src/userscript/serializer.ts @@ -0,0 +1,298 @@ +import type { + ChatDTO, + ContactDTO, + GroupMetadataDTO, + GroupParticipantDTO, + LocationDTO, + MessageDTO, + MessageIdDTO, + SerializedId, +} from "../shared/dto"; + +/** + * WA models contain a lot of internal properties and sometimes + * circular references. Never JSON.stringify them directly. + */ +export function serialize(value: any): any { + if (value == null) { + return value; + } + + if (Array.isArray(value)) { + return value.map(serialize); + } + + if (typeof value !== "object") { + return value; + } + + const result: Record = {}; + + for (const key of Object.keys(value)) { + const v = value[key]; + + if ( + typeof v === "string" || + typeof v === "number" || + typeof v === "boolean" || + v === null + ) { + result[key] = v; + } + } + + return result; +} + +function safeProperty(object: unknown, key: string): T | undefined { + try { + if (object && typeof object === "object") { + return (object as any)[key]; + } + } catch { + return undefined; + } + + return undefined; +} + +export function safeGet( + object: unknown, + key: string, +): T | undefined { + if (object == null || typeof object !== "object") { + return undefined; + } + + try { + return (object as any)[key]; + } catch { + return undefined; + } +} + +export function pick( + source: any, + keys: readonly string[], +): Partial { + const result: Record = {}; + + for (const key of keys) { + const value = safeGet(source, key); + + if (value !== undefined) { + result[key] = serialize(value); + } + } + + return result as Partial; +} + +export function serializeId(id: any): SerializedId | string | null { + if (id == null) { + return null; + } + + if (typeof id === "string") { + return id; + } + + if (typeof id !== "object") { + return String(id); + } + + const result: SerializedId = {}; + + for (const key of [ + "_serialized", + "user", + "server", + "domain", + "device", + "agent", + "fromMe", + "remote", + "participant", + ]) { + const value = safeGet(id, key); + + if (value !== undefined) { + result[key] = serialize(value) as any; + } + } + + // Some WA objects expose useful fields that aren't + // enumerable. + if (!result._serialized && typeof safeGet(id, "toString") === "function") { + try { + const serialized = id.toString(); + + if (serialized && serialized !== "[object Object]") { + result._serialized = serialized; + } + } catch {} + } + + return Object.keys(result).length ? result : String(id); +} + +export function serializeChat(chat: any): ChatDTO { + return { + id: serializeId(chat.id), + accountLid: chat.accountLid, + name: chat.name, + formattedTitle: chat.formattedTitle, + + isGroup: chat.isGroup, + isUser: chat.isUser, + isReadOnly: chat.isReadOnly, + archived: chat.archived, + unreadCount: chat.unreadCount, + + timestamp: chat.t, + lastMessage: chat.lastMessage ? serialize(chat.lastMessage) : null, + }; +} + +export function serializeContact(contact: any): ContactDTO { + return { + id: serializeId(contact.id), + + accountLid: contact.accountLid ?? null, + name: contact.name ?? null, + pushname: contact.pushname ?? null, + shortName: contact.shortName ?? null, + formattedName: contact.formattedName ?? null, + number: contact.number ?? null, + + isMe: contact.isMe ?? false, + isUser: contact.isUser ?? false, + isBusiness: contact.isBusiness ?? false, + isEnterprise: contact.isEnterprise ?? false, + isGroup: contact.isGroup ?? false, + isMyContact: contact.isMyContact ?? false, + isBlocked: contact.isBlocked ?? false, + + profilePicThumbObj: contact.profilePicThumbObj + ? serialize(contact.profilePicThumbObj) + : undefined, + }; +} + +export function serializeGroupParticipant( + participant: any, +): GroupParticipantDTO { + return { + id: serializeId(participant.id), + + isAdmin: participant.isAdmin ?? false, + isSuperAdmin: participant.isSuperAdmin ?? false, + isSuperParticipant: participant.isSuperParticipant ?? false, + isBusiness: participant.isBusiness ?? false, + }; +} + +export function serializeGroupMetadata(metadata: any): GroupMetadataDTO { + return { + id: serializeId(metadata.id), + + subject: metadata.subject ?? null, + subjectOwner: serializeId(metadata.subjectOwner), + subjectTime: metadata.subjectTime ?? null, + creation: metadata.creation ?? null, + owner: serializeId(metadata.owner), + + participants: Array.isArray(metadata.participants) + ? (metadata.participants + .map(serializeGroupParticipant) + .filter(Boolean) as GroupParticipantDTO[]) + : [], + + size: metadata.size ?? metadata.participants?.length ?? 0, + restrict: metadata.restrict ?? false, + announce: metadata.announce ?? false, + isCommunity: metadata.isCommunity ?? false, + isCommunityAnnounce: metadata.isCommunityAnnounce ?? false, + }; +} +export function serializeMessageId(id: any): MessageIdDTO | null { + if (!id) { + return null; + } + + return { + id: id.id ?? null, + _serialized: id._serialized ?? null, + fromMe: id.fromMe ?? false, + remote: serializeId(id.remote), + participant: serializeId(id.participant), + }; +} +export function serializeLocation(location: any): LocationDTO | null { + if (!location) { + return null; + } + + return { + latitude: location.latitude ?? null, + longitude: location.longitude ?? null, + description: location.description ?? null, + address: location.address ?? null, + url: location.url ?? null, + name: location.name ?? null, + }; +} +export function serializeMessageLink(link: any): any { + return { + link: link?.link ?? null, + isSuspicious: link?.isSuspicious ?? false, + }; +} +export function serializeMessage(message: any): MessageDTO { + const quotedMsgId = safeProperty(message, "quotedMsgId"); + const hasQuoted = !!quotedMsgId; + let quotedMsg = null; + + if (hasQuoted) { + const rawQuoted = safeProperty(message, "quotedMsg"); + if (rawQuoted) { + quotedMsg = serializeMessage(rawQuoted); + } + } + + return { + id: serializeMessageId(message.id)!, + chatId: message.chatId ?? message.id?.remote?._serialized ?? null, + from: serializeId(message.from), + to: serializeId(message.to), + author: serializeId(message.author), + fromMe: message.fromMe ?? false, + body: message.body ?? null, + caption: message.caption ?? null, + type: message.type ?? null, + subtype: message.subtype ?? null, + timestamp: message.timestamp ?? null, + ack: message.ack ?? null, + hasMedia: message.hasMedia ?? false, + mediaKey: message.mediaKey ?? null, + isForwarded: message.isForwarded ?? false, + forwardingScore: message.forwardingScore ?? 0, + isStarred: message.isStarred ?? false, + broadcast: message.broadcast ?? false, + mentionedIds: Array.isArray(message.mentionedIds) + ? message.mentionedIds.map(serializeId) + : [], + quotedMsgId: serializeMessageId(quotedMsgId), + quotedMsg, + links: Array.isArray(message.links) + ? message.links.map(serializeMessageLink) + : [], + location: serializeLocation(message.location), + vCard: message.vCard ?? null, + vCards: Array.isArray(message.vCards) ? message.vCards : [], + filename: message.filename ?? null, + mimetype: message.mimetype ?? null, + size: message.size ?? null, + duration: message.duration ?? null, + }; +} diff --git a/src/userscript/types.d.ts b/src/userscript/types.d.ts new file mode 100644 index 0000000..028ec30 --- /dev/null +++ b/src/userscript/types.d.ts @@ -0,0 +1,62 @@ +declare const WPP: { + isReady: boolean; + + loader: { + onReady(callback: () => void): void; + }; + + conn: { + isAuthenticated(): boolean; + }; + + chat: { + list(options?: { + count?: number; + onlyUsers?: boolean; + onlyGroups?: boolean; + onlyCommunities?: boolean; + onlyNewsletter?: boolean; + onlyArchived?: boolean; + }): Promise; + + get(chatId: string): any | undefined; + + getMessages( + chatId: string, + options?: { + count?: number; + direction?: "before" | "after"; + id?: string; + includeCallMessages?: boolean; + media?: "image" | "document" | "url" | "all"; + onlyUnread?: boolean; + }, + ): Promise; + + sendTextMessage( + chatId: string, + text: string, + options?: { + delay?: number; + detectMentioned?: boolean; + markIsRead?: boolean; + mentionedList?: string[]; + messageId?: string; + quotedMsg?: string; + quotedMsgPayload?: string; + waitForAck?: boolean; + }, + ): Promise; + + on(event: string, callback: (...args: any[]) => void): void; + }; + + contact: { + getAllContacts(): Promise; + get(contactId: string): any | undefined; + }; + + group: { + getAllGroups(): Promise; + }; +}; diff --git a/src/userscript/utils.ts b/src/userscript/utils.ts new file mode 100644 index 0000000..e69de29 diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..b2e7497 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,30 @@ +{ + "compilerOptions": { + // Environment setup & latest features + "lib": ["ESNext"], + "target": "ESNext", + "module": "Preserve", + "moduleDetection": "force", + "jsx": "react-jsx", + "allowJs": true, + "types": ["bun"], + + // Bundler mode + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "verbatimModuleSyntax": true, + "noEmit": true, + + // Best practices + "strict": true, + "skipLibCheck": true, + "noFallthroughCasesInSwitch": true, + "noUncheckedIndexedAccess": true, + "noImplicitOverride": true, + + // Some stricter flags (disabled by default) + "noUnusedLocals": false, + "noUnusedParameters": false, + "noPropertyAccessFromIndexSignature": false + } +}