diff --git a/.env.example b/.env.example index 7e79beed..fccd2da8 100644 --- a/.env.example +++ b/.env.example @@ -73,6 +73,11 @@ PUPPETEER_ARGS=--no-sandbox,--disable-setuid-sandbox,--disable-dev-shm-usage,--d # 2.3000.1023204257 is confirmed to reach "ready" (incl. ARM64 / Raspberry Pi 5) — uncomment it: # WWEBJS_WEB_VERSION=2.3000.1023204257 +# Baileys engine (used when ENGINE_TYPE=baileys). WebSocket client, no Chromium. +# NOTE: the Baileys engine dependency is ESM-only; OpenWA therefore requires Node >= 20.19 (require-of-ESM support). +BAILEYS_AUTH_DIR=./data/baileys +# NOTE: proxy (PROXY_URL/PROXY_TYPE) is not yet supported by the baileys engine; it is ignored. + # Humanise single sends: show a "typing…" indicator and pause briefly (length-scaled, jittered) # before each text send so messages don't look instantaneous (anti-ban). ON by default — set to # false to disable. SIMULATE_TYPING_MAX_MS caps the pause (default 5000). Does not affect bulk diff --git a/CHANGELOG.md b/CHANGELOG.md index 0f8972c9..16411ac1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -27,9 +27,11 @@ plugins instead of in core (#265). documented as out of scope for now). (#294) - **`auto-reply` reference extension plugin**, first-party and **registered disabled by default** — enable it via `POST /plugins/auto-reply/enable` to exercise the capability layer end-to-end. (#294) +- **Baileys engine (minimal slice)** — `ENGINE_TYPE=baileys` now selects a second, browser-free WhatsApp engine built on `@whiskeysockets/baileys` (WebSocket/Noise protocol, no Chromium). This first slice supports linking (QR + pairing code), sending and receiving **text**, recipient resolution, and typing presence; all other operations return HTTP 501 until later slices add a message store. Config: `BAILEYS_AUTH_DIR` (default `./data/baileys`). Proxy is not yet supported on this engine. (#299) ### Changed +- ⚠️ **Node.js ≥ 20.19 now required.** The Baileys engine dependency (`@whiskeysockets/baileys`) is ESM-only and is loaded at startup, so OpenWA now requires Node ≥ 20.19 (for `require()` of ESM). Operators on older Node must upgrade. (#299) - Engine config is now **opaque per-engine**: `EngineFactory` passes only engine-neutral fields (`sessionId`/`proxyUrl`/`proxyType`) to an engine plugin and supplies engine-specific config (Puppeteer for whatsapp-web.js) as a blob via the plugin context, so a non-browser engine can be added without the diff --git a/package-lock.json b/package-lock.json index 3cb8c96e..997da1b2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -24,6 +24,7 @@ "@nestjs/swagger": "^11.4.4", "@nestjs/throttler": "^6.5.0", "@nestjs/typeorm": "^11.0.0", + "@whiskeysockets/baileys": "6.7.23", "archiver": "^8.0.0", "bullmq": "^5.78.1", "class-transformer": "^0.5.1", @@ -1309,6 +1310,86 @@ "@bull-board/api": "8.0.0" } }, + "node_modules/@cacheable/memory": { + "version": "2.0.9", + "resolved": "https://registry.npmjs.org/@cacheable/memory/-/memory-2.0.9.tgz", + "integrity": "sha512-HdMx6DoGywB30vacDbBsITbIX4pgFqj1zsrV58jZBUw3klzkNoXhj7qOqAgledhxG7YZI5rBSJg7Zp8/VG0DuA==", + "license": "MIT", + "dependencies": { + "@cacheable/utils": "^2.4.1", + "@keyv/bigmap": "^1.3.1", + "hookified": "^1.15.1", + "keyv": "^5.6.0" + } + }, + "node_modules/@cacheable/memory/node_modules/@keyv/bigmap": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@keyv/bigmap/-/bigmap-1.3.1.tgz", + "integrity": "sha512-WbzE9sdmQtKy8vrNPa9BRnwZh5UF4s1KTmSK0KUVLo3eff5BlQNNWDnFOouNpKfPKDnms9xynJjsMYjMaT/aFQ==", + "license": "MIT", + "dependencies": { + "hashery": "^1.4.0", + "hookified": "^1.15.0" + }, + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "keyv": "^5.6.0" + } + }, + "node_modules/@cacheable/memory/node_modules/keyv": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-5.6.0.tgz", + "integrity": "sha512-CYDD3SOtsHtyXeEORYRx2qBtpDJFjRTGXUtmNEMGyzYOKj1TE3tycdlho7kA1Ufx9OYWZzg52QFBGALTirzDSw==", + "license": "MIT", + "peer": true, + "dependencies": { + "@keyv/serialize": "^1.1.1" + } + }, + "node_modules/@cacheable/node-cache": { + "version": "1.7.6", + "resolved": "https://registry.npmjs.org/@cacheable/node-cache/-/node-cache-1.7.6.tgz", + "integrity": "sha512-6Omk2SgNnjtxB5f/E6bTIWIt5xhdpx39fGNRQgU9lojvRxU68v+qY+SXXLsp3ZGukqoPjsK21wZ6XABFr/Ge3A==", + "license": "MIT", + "dependencies": { + "cacheable": "^2.3.1", + "hookified": "^1.14.0", + "keyv": "^5.5.5" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@cacheable/node-cache/node_modules/keyv": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-5.6.0.tgz", + "integrity": "sha512-CYDD3SOtsHtyXeEORYRx2qBtpDJFjRTGXUtmNEMGyzYOKj1TE3tycdlho7kA1Ufx9OYWZzg52QFBGALTirzDSw==", + "license": "MIT", + "dependencies": { + "@keyv/serialize": "^1.1.1" + } + }, + "node_modules/@cacheable/utils": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/@cacheable/utils/-/utils-2.4.1.tgz", + "integrity": "sha512-eiFgzCbIneyMlLOmNG4g9xzF7Hv3Mga4LjxjcSC/ues6VYq2+gUbQI8JqNuw/ZM8tJIeIaBGpswAsqV2V7ApgA==", + "license": "MIT", + "dependencies": { + "hashery": "^1.5.1", + "keyv": "^5.6.0" + } + }, + "node_modules/@cacheable/utils/node_modules/keyv": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-5.6.0.tgz", + "integrity": "sha512-CYDD3SOtsHtyXeEORYRx2qBtpDJFjRTGXUtmNEMGyzYOKj1TE3tycdlho7kA1Ufx9OYWZzg52QFBGALTirzDSw==", + "license": "MIT", + "dependencies": { + "@keyv/serialize": "^1.1.1" + } + }, "node_modules/@colors/colors": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.5.0.tgz", @@ -1357,10 +1438,9 @@ } }, "node_modules/@emnapi/runtime": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.10.0.tgz", - "integrity": "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==", - "dev": true, + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.1.tgz", + "integrity": "sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw==", "license": "MIT", "optional": true, "dependencies": { @@ -1640,6 +1720,21 @@ "node": ">=6" } }, + "node_modules/@hapi/boom": { + "version": "9.1.4", + "resolved": "https://registry.npmjs.org/@hapi/boom/-/boom-9.1.4.tgz", + "integrity": "sha512-Ls1oH8jaN1vNsqcaHVYJrKmgMcKsC1wcp8bujvXrHaAqD2iDYq3HoOwsxwo09Cuda5R5nC0o0IxlrlTuvPuzSw==", + "license": "BSD-3-Clause", + "dependencies": { + "@hapi/hoek": "9.x.x" + } + }, + "node_modules/@hapi/hoek": { + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/@hapi/hoek/-/hoek-9.3.0.tgz", + "integrity": "sha512-/c6rf4UJlmHlC9b5BaNvzAcFv7HZ2QHaV0D4/HNlBdvFnvQq8RI4kYdhyPCl7Xj+oWvTWQ8ujhqS53LIgAe6KQ==", + "license": "BSD-3-Clause" + }, "node_modules/@humanfs/core": { "version": "0.19.2", "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.2.tgz", @@ -1675,35 +1770,535 @@ "dev": true, "license": "Apache-2.0", "engines": { - "node": ">=18.18.0" + "node": ">=18.18.0" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/retry": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", + "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@img/colour": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.1.0.tgz", + "integrity": "sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@img/sharp-darwin-arm64": { + "version": "0.35.1", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.35.1.tgz", + "integrity": "sha512-T15JRWOubQ3f5+GxnWeIvo47u5qV0M9HBgJhT+f2gE1e9e6OhR6K73Re52Hm80qWcu1DNb3GweKmpr/MnuP2Ow==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-arm64": "1.3.0" + } + }, + "node_modules/@img/sharp-darwin-x64": { + "version": "0.35.1", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.35.1.tgz", + "integrity": "sha512-t1CPD0cr7XCHjwUj6tQ5MC0pCi866I+gUW6zbUX4aFPnKd1DFBtk0M+gWcjX8VeEzgfCNiSiNTVFZ6b7kvdbnQ==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-x64": "1.3.0" + } + }, + "node_modules/@img/sharp-freebsd-wasm32": { + "version": "0.35.1", + "resolved": "https://registry.npmjs.org/@img/sharp-freebsd-wasm32/-/sharp-freebsd-wasm32-0.35.1.tgz", + "integrity": "sha512-MBSQXqNPThW9EcZ905H6N4sEdX5EwZEYzGx5EBq9ncDCGJALMiY1xPFJxNdzuB1iBjLOpIfxajM6YxdvwmQSLA==", + "license": "Apache-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "dependencies": { + "@img/sharp-wasm32": "0.35.1" + }, + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-darwin-arm64": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.3.0.tgz", + "integrity": "sha512-EKbmBKtyTH+GPFDRw2TgK2oV6hyxxlJVIar4hoTYSNmIwipgMFdxPQqR392GmfdsPGWga0mCFN1cCKjRb9cljw==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-darwin-x64": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.3.0.tgz", + "integrity": "sha512-Pl2OmOvrJ42adUllESxBsG54PfXLo1OYg9i3c5/5Ln/qJ0gZuTM9YMhQJPIbXqwidLRc/c2zuHt4RsrymmNv7A==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.3.0.tgz", + "integrity": "sha512-A8UpHoUDW4DwnXoV6+q3C1s7QLRAHtPDEjWuNZjwHMyoCNZnm0GeNN8ls9f/bsEYTRQRW96C/n34XJQHJ2fT7A==", + "cpu": [ + "arm" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm64": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.3.0.tgz", + "integrity": "sha512-C0SqjoFKnszqa44EQ7xoaT48nnO0lOyXEULfXMWi8krrjOPGYkeK30Okzla6ATbBYsyZ0ySinK0FVkpv3DwzfQ==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-ppc64": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.3.0.tgz", + "integrity": "sha512-WOpkVxAjFd369iaIzEgNRreFD+gWdUMIGD5zplhNKNeqS6mm5dac3q2AFyCBmzYoAdouzZvRBgxy4z8QHZb4/A==", + "cpu": [ + "ppc64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-riscv64": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.3.0.tgz", + "integrity": "sha512-DRWw0mOHusrCCuw2rqP87oLg6PGlkomVDFqw2hIwsSfwWpu4k3XLcBPaKKl6ct/GtL/cwNkgwjV/tc0Mqht3VA==", + "cpu": [ + "riscv64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-s390x": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.3.0.tgz", + "integrity": "sha512-9APy+nFWhHS+kzLgWZfLcyrUd7YqnAQVa4BPOo4xkoHpdoktOAPG4cEr9+Jpl0TtqfVmcMJimNL5qNTyyOHZNA==", + "cpu": [ + "s390x" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-x64": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.3.0.tgz", + "integrity": "sha512-y9RNUYDe2A1UAdhLyfeOodGRszQdaEoe4nfOpp/sNVPl2CWIcUyFaDoCh4vPLPxu19803j2naLqZup2WxDXCLA==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-arm64": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.3.0.tgz", + "integrity": "sha512-cC1wkC0Mlucd0KSiGrLkJnB/ZqPvZCntc/Lk7ZnYO5ZSbF2euNek4Xvxafojq+wN1q/W0eprdpUIjUr/EV2PBg==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-x64": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.3.0.tgz", + "integrity": "sha512-LiYMhUZicB1QG//+RvmYZpXJO8fYRENfp+MZUCnG9aw+AKvGAy9gPaCnuwsPcBFs8EV66M0NNxj9VHcNklE8zw==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-linux-arm": { + "version": "0.35.1", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.35.1.tgz", + "integrity": "sha512-jygmR02PpCYypt7xB7nst1vqjZp/BpRA/Kf9nK7qRponJ/KrLPaZWEG4G15z1d2FZ6XqI+T0350ha3RSnKx24A==", + "cpu": [ + "arm" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm": "1.3.0" + } + }, + "node_modules/@img/sharp-linux-arm64": { + "version": "0.35.1", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.35.1.tgz", + "integrity": "sha512-ErCRyGU7LeoaFBZ0xW8hhLlXzhAg80sc4vxePB86qvtEvW1jEhhmbiNBP4oEzZfPMnu6HwHXfzD2W2kBU+RnCw==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm64": "1.3.0" + } + }, + "node_modules/@img/sharp-linux-ppc64": { + "version": "0.35.1", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.35.1.tgz", + "integrity": "sha512-LUWZ2+r2UoLCd8j0RLCwQ4gL6w47+Y7igxtVnPIDXOOEjV86LpBkAHq5VpJeg+GHbw0KN/JWlPJOdZjyZnFqFQ==", + "cpu": [ + "ppc64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-ppc64": "1.3.0" + } + }, + "node_modules/@img/sharp-linux-riscv64": { + "version": "0.35.1", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.35.1.tgz", + "integrity": "sha512-i7x6J3mwF4JgT0sM4V4WlAWdJ0bucPtA9rzO1bTji1n5qgBq/W5nn87RvOQPleuuxahNoLdTngByD8/vDDLArw==", + "cpu": [ + "riscv64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-riscv64": "1.3.0" + } + }, + "node_modules/@img/sharp-linux-s390x": { + "version": "0.35.1", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.35.1.tgz", + "integrity": "sha512-0zSaTUjTF0kIWTSYxD4EG/nvCU4jez53+3RdURtoY3HvbXtIQ98W90JnrGz/oLRFuEnfIy9+7xeq883euc0ZWw==", + "cpu": [ + "s390x" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-s390x": "1.3.0" + } + }, + "node_modules/@img/sharp-linux-x64": { + "version": "0.35.1", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.35.1.tgz", + "integrity": "sha512-NbJD4mWdeyrNQKluO/tR/wBDOelcowSVGNBWxI0e3ZtlXc6F/UOVKDj1MLD4zl3oHTuvKW3s+MA9N54YTldAYw==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-x64": "1.3.0" + } + }, + "node_modules/@img/sharp-linuxmusl-arm64": { + "version": "0.35.1", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.35.1.tgz", + "integrity": "sha512-VoW2sQCWI+0YIKQEmWJ8vzaQjTg9wIyfkFpvEfAS2h43X6iHu7GTk1hhOgB4IpSzCHe8UwQZIcx7b81VTaOrJA==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-arm64": "1.3.0" + } + }, + "node_modules/@img/sharp-linuxmusl-x64": { + "version": "0.35.1", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.35.1.tgz", + "integrity": "sha512-LjBoSd/c5JU0/K5MwzDMlgsSRP2bPn98JQGFFQAOLQ0bU/1z4ekxUdSKY9BmlwSh/cA+OrvpgsWqfZyYfVHBRw==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-x64": "1.3.0" + } + }, + "node_modules/@img/sharp-wasm32": { + "version": "0.35.1", + "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.35.1.tgz", + "integrity": "sha512-PCQUoQdZyE8tp3HpbevuihfUmgSP4qWI0FGEPWoeXqaS+cUrFfemabHQiebUmUmlUhCuNnQMxGrQ+CPqK4hnxg==", + "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", + "optional": true, + "dependencies": { + "@emnapi/runtime": "^1.11.0" + }, + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-webcontainers-wasm32": { + "version": "0.35.1", + "resolved": "https://registry.npmjs.org/@img/sharp-webcontainers-wasm32/-/sharp-webcontainers-wasm32-0.35.1.tgz", + "integrity": "sha512-xU2ml2bU2OPxYVvW2A6ae4M1g5QKyhKG06P4FAt+YEaFQQO0919Qx+XxIZEUuWTMoDViLpMws2/dQwoe/VcA6A==", + "cpu": [ + "wasm32" + ], + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "@img/sharp-wasm32": "0.35.1" + }, + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-arm64": { + "version": "0.35.1", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.35.1.tgz", + "integrity": "sha512-IkmHwuFhYpd3bTsN5SAahjwhiAcyXPooBt8vEUgxY3T0IP70sSJ0nU1xiPzZY8AH/OB1XpV3j8aZSVSOSfTbdA==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" } }, - "node_modules/@humanwhocodes/module-importer": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", - "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", - "dev": true, - "license": "Apache-2.0", + "node_modules/@img/sharp-win32-ia32": { + "version": "0.35.1", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.35.1.tgz", + "integrity": "sha512-wQahqCi9MD8Yxzg4gVM4fNrZxh+r6vD55PyIg+WJPaM5ZRUyF35iQpwJCuma3r6viU9/8Pxlc+XHV+woVa6nCQ==", + "cpu": [ + "ia32" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], "engines": { - "node": ">=12.22" + "node": "^20.9.0" }, "funding": { - "type": "github", - "url": "https://github.com/sponsors/nzakas" + "url": "https://opencollective.com/libvips" } }, - "node_modules/@humanwhocodes/retry": { - "version": "0.4.3", - "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", - "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", - "dev": true, - "license": "Apache-2.0", + "node_modules/@img/sharp-win32-x64": { + "version": "0.35.1", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.35.1.tgz", + "integrity": "sha512-WzBtkYtZHATLPe8XRharxZXxQ9cdLrQWHiwxt+BJ5rBsisQrKeeV86ErxPSVhcG6xCEuNhs0SqLpWr7XDa2k6w==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], "engines": { - "node": ">=18.18" + "node": ">=20.9.0" }, "funding": { - "type": "github", - "url": "https://github.com/sponsors/nzakas" + "url": "https://opencollective.com/libvips" } }, "node_modules/@inquirer/ansi": { @@ -2769,6 +3364,12 @@ "url": "https://opencollective.com/js-sdsl" } }, + "node_modules/@keyv/serialize": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@keyv/serialize/-/serialize-1.1.1.tgz", + "integrity": "sha512-dXn3FZhPv0US+7dtJsIi2R+c7qWYiReoEh5zUntWCf4oSpMNib8FDhSoed6m3QyZdx5hK7iLFkYk3rNxwt8vTA==", + "license": "MIT" + }, "node_modules/@lukeed/csprng": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@lukeed/csprng/-/csprng-1.1.0.tgz", @@ -3458,6 +4059,12 @@ "@noble/hashes": "^1.1.5" } }, + "node_modules/@pinojs/redact": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@pinojs/redact/-/redact-0.4.0.tgz", + "integrity": "sha512-k2ENnmBugE/rzQfEcdWHcCY+/FM3VLzH9cYEsbdsoqrvzAKRhUZeRNhAZvB8OitQJ1TBed3yqWtdjzS6wJKBwg==", + "license": "MIT" + }, "node_modules/@pkgjs/parseargs": { "version": "0.11.0", "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", @@ -4947,6 +5554,44 @@ "@xtuc/long": "4.2.2" } }, + "node_modules/@whiskeysockets/baileys": { + "version": "6.7.23", + "resolved": "https://registry.npmjs.org/@whiskeysockets/baileys/-/baileys-6.7.23.tgz", + "integrity": "sha512-UDbysXJpFKHC2S27qy4oABQc2uZekhUcCNi+Nvzev7wZkOhC72wqezzM2cfYB3PHJdy3Jadc2VkVv8eQeywIUw==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "@cacheable/node-cache": "^1.4.0", + "@hapi/boom": "^9.1.3", + "async-mutex": "^0.5.0", + "axios": "^1.6.0", + "libsignal": "git+https://github.com/whiskeysockets/libsignal-node.git", + "music-metadata": "^11.7.0", + "pino": "^9.6", + "protobufjs": "^7.2.4", + "ws": "^8.13.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "audio-decode": "^2.1.3", + "jimp": "^1.6.0", + "link-preview-js": "^3.0.0", + "sharp": "*" + }, + "peerDependenciesMeta": { + "audio-decode": { + "optional": true + }, + "jimp": { + "optional": true + }, + "link-preview-js": { + "optional": true + } + } + }, "node_modules/@xtuc/ieee754": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz", @@ -5048,7 +5693,6 @@ "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", "license": "MIT", - "optional": true, "dependencies": { "debug": "4" }, @@ -5503,13 +6147,30 @@ "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", "license": "MIT" }, + "node_modules/async-mutex": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/async-mutex/-/async-mutex-0.5.0.tgz", + "integrity": "sha512-1A94B18jkJ3DYq284ohPxoXbfTA5HsQ7/Mf4DEhcyLx3Bz27Rh59iScbB6EPiP+B+joue6YCxcMXSbFC1tZKwA==", + "license": "MIT", + "dependencies": { + "tslib": "^2.4.0" + } + }, "node_modules/asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", - "dev": true, "license": "MIT" }, + "node_modules/atomic-sleep": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/atomic-sleep/-/atomic-sleep-1.0.0.tgz", + "integrity": "sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==", + "license": "MIT", + "engines": { + "node": ">=8.0.0" + } + }, "node_modules/available-typed-arrays": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", @@ -5525,6 +6186,27 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/axios": { + "version": "1.18.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.18.0.tgz", + "integrity": "sha512-E32NzpYKp++W7XRe52rHiXV2ehxmh3wbdgO7MHeFM+vqxLBYHzt0ElkiImtOBxtOmyp0yoC8C6uESVV84Y2/hw==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.16.0", + "form-data": "^4.0.5", + "https-proxy-agent": "^5.0.1", + "proxy-from-env": "^2.1.0" + } + }, + "node_modules/axios/node_modules/proxy-from-env": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-2.1.0.tgz", + "integrity": "sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, "node_modules/b4a": { "version": "1.8.0", "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.8.0.tgz", @@ -6199,6 +6881,28 @@ "license": "ISC", "optional": true }, + "node_modules/cacheable": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/cacheable/-/cacheable-2.3.5.tgz", + "integrity": "sha512-EQfaKe09tl615iNvq/TBRWTFf1AKJNXYQSsMx0Z3EI0nA+pVsVPS8wJhnRlkbdacKPh1d0qVIhwTc2zsQNFEEg==", + "license": "MIT", + "dependencies": { + "@cacheable/memory": "^2.0.8", + "@cacheable/utils": "^2.4.1", + "hookified": "^1.15.0", + "keyv": "^5.6.0", + "qified": "^0.10.1" + } + }, + "node_modules/cacheable/node_modules/keyv": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-5.6.0.tgz", + "integrity": "sha512-CYDD3SOtsHtyXeEORYRx2qBtpDJFjRTGXUtmNEMGyzYOKj1TE3tycdlho7kA1Ufx9OYWZzg52QFBGALTirzDSw==", + "license": "MIT", + "dependencies": { + "@keyv/serialize": "^1.1.1" + } + }, "node_modules/call-bind": { "version": "1.0.9", "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.9.tgz", @@ -6599,7 +7303,6 @@ "version": "1.0.8", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", - "dev": true, "license": "MIT", "dependencies": { "delayed-stream": "~1.0.0" @@ -7068,6 +7771,12 @@ "node": ">= 8" } }, + "node_modules/curve25519-js": { + "version": "0.0.4", + "resolved": "https://registry.npmjs.org/curve25519-js/-/curve25519-js-0.0.4.tgz", + "integrity": "sha512-axn2UMEnkhyDUPWOwVKBMVIzSQy2ejH2xRGy1wq81dqRwApXfIzfbE3hIX0ZRFBIihf/KDqK158DLwESu4AK1w==", + "license": "MIT" + }, "node_modules/data-uri-to-buffer": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-6.0.2.tgz", @@ -7212,7 +7921,6 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", - "dev": true, "license": "MIT", "engines": { "node": ">=0.4.0" @@ -7689,7 +8397,6 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", - "dev": true, "license": "MIT", "dependencies": { "es-errors": "^1.3.0", @@ -8498,6 +9205,26 @@ "which": "bin/which" } }, + "node_modules/follow-redirects": { + "version": "1.16.0", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.16.0.tgz", + "integrity": "sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, "node_modules/for-each": { "version": "0.3.5", "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz", @@ -8561,7 +9288,6 @@ "version": "4.0.5", "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", - "dev": true, "license": "MIT", "dependencies": { "asynckit": "^0.4.0", @@ -8578,7 +9304,6 @@ "version": "1.52.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.6" @@ -8588,7 +9313,6 @@ "version": "2.1.35", "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", - "dev": true, "license": "MIT", "dependencies": { "mime-db": "1.52.0" @@ -9059,6 +9783,18 @@ "license": "ISC", "optional": true }, + "node_modules/hashery": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/hashery/-/hashery-1.5.1.tgz", + "integrity": "sha512-iZyKG96/JwPz1N55vj2Ie2vXbhu440zfUfJvSwEqEbeLluk7NnapfGqa7LH0mOsnDxTF85Mx8/dyR6HfqcbmbQ==", + "license": "MIT", + "dependencies": { + "hookified": "^1.15.0" + }, + "engines": { + "node": ">=20" + } + }, "node_modules/hasown": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.3.tgz", @@ -9083,6 +9819,12 @@ "url": "https://github.com/sponsors/EvanHahn" } }, + "node_modules/hookified": { + "version": "1.15.1", + "resolved": "https://registry.npmjs.org/hookified/-/hookified-1.15.1.tgz", + "integrity": "sha512-MvG/clsADq1GPM2KGo2nyfaWVyn9naPiXrqIe4jYjXNZQt238kWyOGrsyc/DmRAQ+Re6yeo6yX/yoNCG5KAEVg==", + "license": "MIT" + }, "node_modules/html-escaper": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", @@ -9137,7 +9879,6 @@ "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", "license": "MIT", - "optional": true, "dependencies": { "agent-base": "6", "debug": "4" @@ -10493,6 +11234,15 @@ "integrity": "sha512-oKQFPTibqQwZZkChCDVMFVJXMZdyJNqDWZWYNn8BgyAaK/6yFJEowxCY0RVFirRyWP63hMRuKlkSEd9qlvbWXg==", "license": "MIT" }, + "node_modules/libsignal": { + "version": "6.0.0", + "resolved": "git+ssh://git@github.com/whiskeysockets/libsignal-node.git#bcea72df9ec34d9d9140ab30619cf479c7c144c7", + "license": "GPL-3.0", + "dependencies": { + "curve25519-js": "^0.0.4", + "protobufjs": "^7.5.5" + } + }, "node_modules/lines-and-columns": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", @@ -11237,6 +11987,63 @@ "node": ">= 0.6" } }, + "node_modules/music-metadata": { + "version": "11.13.0", + "resolved": "https://registry.npmjs.org/music-metadata/-/music-metadata-11.13.0.tgz", + "integrity": "sha512-uXRaov9dfjSpQufXIU7sMxVZnh+FilCQv2mXn+K5EJ/decP3dTWrgvPYa5r6MtRbieNSCE708Da4J0u1UGfQIw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + }, + { + "type": "buymeacoffee", + "url": "https://buymeacoffee.com/borewit" + } + ], + "license": "MIT", + "dependencies": { + "@borewit/text-codec": "^0.2.2", + "@tokenizer/token": "^0.3.0", + "content-type": "^2.0.0", + "debug": "^4.4.3", + "file-type": "^21.3.4", + "media-typer": "^2.0.0", + "strtok3": "^10.3.5", + "token-types": "^6.1.2", + "uint8array-extras": "^1.5.0", + "win-guid": "^0.2.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/music-metadata/node_modules/content-type": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-2.0.0.tgz", + "integrity": "sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/music-metadata/node_modules/media-typer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-2.0.0.tgz", + "integrity": "sha512-kOy3OxT2HH39N70UnKgu4NWDZjLOz8W/mfyvniHjRH/DrL3f2pOfvWQ4p60offbbtDAnXWp0v9LfMIqMec269Q==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "node_modules/mute-stream": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-2.0.0.tgz", @@ -11529,6 +12336,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/on-exit-leak-free": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/on-exit-leak-free/-/on-exit-leak-free-2.1.2.tgz", + "integrity": "sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA==", + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, "node_modules/on-finished": { "version": "2.4.1", "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", @@ -11996,6 +12812,43 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/pino": { + "version": "9.14.0", + "resolved": "https://registry.npmjs.org/pino/-/pino-9.14.0.tgz", + "integrity": "sha512-8OEwKp5juEvb/MjpIc4hjqfgCNysrS94RIOMXYvpYCdm/jglrKEiAYmiumbmGhCvs+IcInsphYDFwqrjr7398w==", + "license": "MIT", + "dependencies": { + "@pinojs/redact": "^0.4.0", + "atomic-sleep": "^1.0.0", + "on-exit-leak-free": "^2.1.0", + "pino-abstract-transport": "^2.0.0", + "pino-std-serializers": "^7.0.0", + "process-warning": "^5.0.0", + "quick-format-unescaped": "^4.0.3", + "real-require": "^0.2.0", + "safe-stable-stringify": "^2.3.1", + "sonic-boom": "^4.0.1", + "thread-stream": "^3.0.0" + }, + "bin": { + "pino": "bin.js" + } + }, + "node_modules/pino-abstract-transport": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/pino-abstract-transport/-/pino-abstract-transport-2.0.0.tgz", + "integrity": "sha512-F63x5tizV6WCh4R6RHyi2Ml+M70DNRXt/+HANowMflpgGFMAym/VKm6G7ZOQRjqN7XbGxK1Lg9t6ZrtzOaivMw==", + "license": "MIT", + "dependencies": { + "split2": "^4.0.0" + } + }, + "node_modules/pino-std-serializers": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/pino-std-serializers/-/pino-std-serializers-7.1.0.tgz", + "integrity": "sha512-BndPH67/JxGExRgiX1dX0w1FvZck5Wa4aal9198SrRhZjH3GxKQUKIBnYJTdj2HDN3UQAS06HlfcSbQj2OHmaw==", + "license": "MIT" + }, "node_modules/pirates": { "version": "4.0.7", "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz", @@ -12253,6 +13106,22 @@ "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", "license": "MIT" }, + "node_modules/process-warning": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/process-warning/-/process-warning-5.0.0.tgz", + "integrity": "sha512-a39t9ApHNx2L4+HBnQKqxxHNs1r7KF+Intd8Q/g1bUh6q0WIp9voPXJ/x0j+ZL45KF1pJd9+q2jLIRMfvEshkA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT" + }, "node_modules/progress": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", @@ -12504,6 +13373,24 @@ ], "license": "MIT" }, + "node_modules/qified": { + "version": "0.10.1", + "resolved": "https://registry.npmjs.org/qified/-/qified-0.10.1.tgz", + "integrity": "sha512-+Owyggi9IxT1ePKGafcI87ubSmxol6smwJ+RAHDQlx9+9cPwFWDiKFFCPuWhr9ignlGpZ9vDQLw67N4dcTVFEA==", + "license": "MIT", + "dependencies": { + "hookified": "^2.1.1" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/qified/node_modules/hookified": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/hookified/-/hookified-2.2.0.tgz", + "integrity": "sha512-p/LgFzRN5FeoD3DLS6bkUapeye6E4SI6yJs6KetENd18S+FBthqYq2amJUWpt5z0EQwwHemidjY5OqJGEKm5uA==", + "license": "MIT" + }, "node_modules/qrcode": { "version": "1.5.4", "resolved": "https://registry.npmjs.org/qrcode/-/qrcode-1.5.4.tgz", @@ -12649,6 +13536,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/quick-format-unescaped": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/quick-format-unescaped/-/quick-format-unescaped-4.0.4.tgz", + "integrity": "sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==", + "license": "MIT" + }, "node_modules/range-parser": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", @@ -12818,6 +13711,15 @@ "url": "https://paulmillr.com/funding/" } }, + "node_modules/real-require": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/real-require/-/real-require-0.2.0.tgz", + "integrity": "sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==", + "license": "MIT", + "engines": { + "node": ">= 12.13.0" + } + }, "node_modules/redis-errors": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/redis-errors/-/redis-errors-1.2.0.tgz", @@ -13028,6 +13930,15 @@ ], "license": "MIT" }, + "node_modules/safe-stable-stringify": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.5.0.tgz", + "integrity": "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, "node_modules/safer-buffer": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", @@ -13159,6 +14070,62 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/sharp": { + "version": "0.35.1", + "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.35.1.tgz", + "integrity": "sha512-lW979AMi+ESidzMv/Lnv+F9bknzLyxLqFI05Sm433vOeRcltgxQmXpnfOOFIAlKtwXU/ksupm2srQoFCkR214g==", + "license": "Apache-2.0", + "dependencies": { + "@img/colour": "^1.1.0", + "detect-libc": "^2.1.2", + "semver": "^7.8.4" + }, + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-darwin-arm64": "0.35.1", + "@img/sharp-darwin-x64": "0.35.1", + "@img/sharp-freebsd-wasm32": "0.35.1", + "@img/sharp-libvips-darwin-arm64": "1.3.0", + "@img/sharp-libvips-darwin-x64": "1.3.0", + "@img/sharp-libvips-linux-arm": "1.3.0", + "@img/sharp-libvips-linux-arm64": "1.3.0", + "@img/sharp-libvips-linux-ppc64": "1.3.0", + "@img/sharp-libvips-linux-riscv64": "1.3.0", + "@img/sharp-libvips-linux-s390x": "1.3.0", + "@img/sharp-libvips-linux-x64": "1.3.0", + "@img/sharp-libvips-linuxmusl-arm64": "1.3.0", + "@img/sharp-libvips-linuxmusl-x64": "1.3.0", + "@img/sharp-linux-arm": "0.35.1", + "@img/sharp-linux-arm64": "0.35.1", + "@img/sharp-linux-ppc64": "0.35.1", + "@img/sharp-linux-riscv64": "0.35.1", + "@img/sharp-linux-s390x": "0.35.1", + "@img/sharp-linux-x64": "0.35.1", + "@img/sharp-linuxmusl-arm64": "0.35.1", + "@img/sharp-linuxmusl-x64": "0.35.1", + "@img/sharp-webcontainers-wasm32": "0.35.1", + "@img/sharp-win32-arm64": "0.35.1", + "@img/sharp-win32-ia32": "0.35.1", + "@img/sharp-win32-x64": "0.35.1" + } + }, + "node_modules/sharp/node_modules/semver": { + "version": "7.8.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.4.tgz", + "integrity": "sha512-rUCObTnP32Q08R2uuIrt7r9PlEonuTmtuXYcW6s5kjdlj3xbnwe+21yXptAUYcMAABLkYYTtnmzb3w3EDZfueA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", @@ -13455,6 +14422,15 @@ "node": ">= 10" } }, + "node_modules/sonic-boom": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/sonic-boom/-/sonic-boom-4.2.1.tgz", + "integrity": "sha512-w6AxtubXa2wTXAUsZMMWERrsIRAdrK0Sc+FUytWvYAhBJLyuI4llrMIC1DtlNSdI99EI86KZum2MMq3EAZlF9Q==", + "license": "MIT", + "dependencies": { + "atomic-sleep": "^1.0.0" + } + }, "node_modules/source-map": { "version": "0.7.4", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.4.tgz", @@ -14210,6 +15186,15 @@ "b4a": "^1.6.4" } }, + "node_modules/thread-stream": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/thread-stream/-/thread-stream-3.2.0.tgz", + "integrity": "sha512-zLBvqpwr4Esa0kRjcrzGU6zL25lePWaCLMx0RQFrmteozIfeNdaMLpG5U7PeHzvlFkAWaRKA9/KVW4F60iB+qw==", + "license": "MIT", + "dependencies": { + "real-require": "^0.2.0" + } + }, "node_modules/tinyglobby": { "version": "0.2.17", "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", @@ -15537,6 +16522,12 @@ "node": ">=8" } }, + "node_modules/win-guid": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/win-guid/-/win-guid-0.2.1.tgz", + "integrity": "sha512-gEIQU4mkgl2OPeoNrWflcJFJ3Ae2BPd4eCsHHA/XikslkIVms/nHhvnvzIZV7VLmBvtFlDOzLt9rrZT+n6D67A==", + "license": "MIT" + }, "node_modules/word-wrap": { "version": "1.2.5", "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", diff --git a/package.json b/package.json index 7e9570c5..9ed46369 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,9 @@ { "name": "openwa", "version": "0.2.10", + "engines": { + "node": ">=20.19.0" + }, "description": "Open Source WhatsApp API Gateway - Free, Self-Hosted HTTP API for WhatsApp", "author": "Yudhi Armyndharis & OpenWA Contributors", "private": true, @@ -50,6 +53,7 @@ "@nestjs/swagger": "^11.4.4", "@nestjs/throttler": "^6.5.0", "@nestjs/typeorm": "^11.0.0", + "@whiskeysockets/baileys": "6.7.23", "archiver": "^8.0.0", "bullmq": "^5.78.1", "class-transformer": "^0.5.1", @@ -110,6 +114,10 @@ "transform": { "^.+\\.(t|j)s$": "ts-jest" }, + "moduleNameMapper": { + "^@whiskeysockets/baileys$": "/../test/__mocks__/@whiskeysockets/baileys.ts", + "^@whiskeysockets/baileys/(.*)$": "/../test/__mocks__/@whiskeysockets/baileys.ts" + }, "collectCoverageFrom": [ "**/*.(t|j)s" ], diff --git a/src/common/errors/engine-not-supported.error.spec.ts b/src/common/errors/engine-not-supported.error.spec.ts new file mode 100644 index 00000000..e75031f9 --- /dev/null +++ b/src/common/errors/engine-not-supported.error.spec.ts @@ -0,0 +1,11 @@ +import { EngineNotSupportedError } from './engine-not-supported.error'; + +describe('EngineNotSupportedError', () => { + it('carries HTTP 501 so NestJS returns Not Implemented without a custom filter', () => { + expect(new EngineNotSupportedError('getGroups').getStatus()).toBe(501); + }); + + it('names the unsupported operation in the message', () => { + expect(new EngineNotSupportedError('getGroups').message).toContain('getGroups'); + }); +}); diff --git a/src/common/errors/engine-not-supported.error.ts b/src/common/errors/engine-not-supported.error.ts new file mode 100644 index 00000000..e2a90468 --- /dev/null +++ b/src/common/errors/engine-not-supported.error.ts @@ -0,0 +1,16 @@ +import { NotImplementedException } from '@nestjs/common'; + +/** + * Thrown by an engine adapter when a method is part of the {@link IWhatsAppEngine} + * contract but the active engine cannot implement it (e.g. the Baileys engine has no + * self-maintained store, so history/contacts/groups are unavailable in its minimal slice). + * + * Extends NestJS `NotImplementedException` so it maps to **HTTP 501** through NestJS's + * built-in exception handler — no custom global filter required. Mirrors how + * {@link EngineNotReadyError} maps to 409. + */ +export class EngineNotSupportedError extends NotImplementedException { + constructor(method: string) { + super(`Operation not supported by the active engine: ${method}`); + } +} diff --git a/src/config/configuration.ts b/src/config/configuration.ts index f08c9b35..a6c852d1 100644 --- a/src/config/configuration.ts +++ b/src/config/configuration.ts @@ -64,6 +64,11 @@ export default () => ({ executablePath: process.env.PUPPETEER_EXECUTABLE_PATH || undefined, }, sessionDataPath: process.env.SESSION_DATA_PATH || './data/sessions', + // Baileys engine (used when ENGINE_TYPE=baileys). Multi-file auth state base dir; each session + // gets its own subdirectory. Read by the Baileys plugin from the opaque engine config blob. + baileys: { + authDir: process.env.BAILEYS_AUTH_DIR || './data/baileys', + }, }, // Webhook configuration diff --git a/src/engine/adapters/baileys-message-mapper.spec.ts b/src/engine/adapters/baileys-message-mapper.spec.ts new file mode 100644 index 00000000..9eabe112 --- /dev/null +++ b/src/engine/adapters/baileys-message-mapper.spec.ts @@ -0,0 +1,110 @@ +import { + BaileysIncomingFields, + buildIncomingMessageFromBaileys, + mapBaileysMessageType, + mapBaileysStatus, +} from './baileys-message-mapper'; + +describe('mapBaileysMessageType (baileys content-type -> neutral MessageType)', () => { + it.each([ + ['conversation', false, 'text'], + ['extendedTextMessage', false, 'text'], + ['imageMessage', false, 'image'], + ['videoMessage', false, 'video'], + ['audioMessage', false, 'audio'], + ['audioMessage', true, 'voice'], + ['documentMessage', false, 'document'], + ['stickerMessage', false, 'sticker'], + ['locationMessage', false, 'location'], + ['contactMessage', false, 'contact'], + [undefined, false, 'unknown'], + ['pollCreationMessage', false, 'unknown'], + ])('maps %s (ptt=%s) -> %s', (raw, ptt, expected) => { + // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion + expect(mapBaileysMessageType(raw as string | undefined, ptt as boolean)).toBe(expected); + }); +}); + +describe('mapBaileysStatus (proto WAMessageStatus -> neutral DeliveryStatus)', () => { + it.each([ + [0, 'failed'], + [1, 'pending'], + [2, 'sent'], + [3, 'delivered'], + [4, 'read'], + [5, 'read'], // PLAYED collapses to read, mirroring the wwjs adapter + ])('maps status %s -> %s', (status, expected) => { + expect(mapBaileysStatus(status)).toBe(expected); + }); + + it('returns null for an unknown/absent status so the adapter skips the ack', () => { + expect(mapBaileysStatus(undefined)).toBeNull(); + expect(mapBaileysStatus(99)).toBeNull(); + }); +}); + +describe('buildIncomingMessageFromBaileys', () => { + const base: BaileysIncomingFields = { + id: 'MSG1', + remoteJid: '628111@s.whatsapp.net', + fromMe: false, + body: 'hi', + contentType: 'conversation', + timestamp: 1700000000, + selfJid: '628999@s.whatsapp.net', + }; + + it('maps a 1:1 inbound message to the neutral shape (chatId, type, non-group)', () => { + const r = buildIncomingMessageFromBaileys(base); + expect(r.id).toBe('MSG1'); + expect(r.chatId).toBe('628111@s.whatsapp.net'); + expect(r.from).toBe('628111@s.whatsapp.net'); + expect(r.to).toBe('628999@s.whatsapp.net'); + expect(r.type).toBe('text'); + expect(r.isGroup).toBe(false); + expect(r.fromMe).toBe(false); + }); + + it('inverts from/to for an outgoing (fromMe) message', () => { + const r = buildIncomingMessageFromBaileys({ ...base, fromMe: true }); + expect(r.from).toBe('628999@s.whatsapp.net'); // self + expect(r.to).toBe('628111@s.whatsapp.net'); // chat + }); + + it('sets author to the participant for a group message and flags isGroup', () => { + const r = buildIncomingMessageFromBaileys({ + ...base, + remoteJid: '123-456@g.us', + participant: '628222@s.whatsapp.net', + }); + expect(r.isGroup).toBe(true); + expect(r.author).toBe('628222@s.whatsapp.net'); + expect(r.chatId).toBe('123-456@g.us'); + expect(r.from).toBe('123-456@g.us'); // group inbound: from is the group JID (mirrors wwjs) + expect(r.to).toBe('628999@s.whatsapp.net'); // recipient is self + }); + + it('flags an @lid 1:1 sender', () => { + const r = buildIncomingMessageFromBaileys({ ...base, remoteJid: '111@lid' }); + expect(r.isLidSender).toBe(true); + }); + + it('flags an @lid group participant via participant, not the group JID', () => { + const r = buildIncomingMessageFromBaileys({ + ...base, + remoteJid: '123-456@g.us', + participant: '222@lid', + }); + expect(r.isLidSender).toBe(true); + }); + + it('flags a status broadcast', () => { + const r = buildIncomingMessageFromBaileys({ ...base, remoteJid: 'status@broadcast' }); + expect(r.isStatusBroadcast).toBe(true); + }); + + it('carries the push name onto contact when present', () => { + const r = buildIncomingMessageFromBaileys({ ...base, pushName: 'Alice' }); + expect(r.contact).toEqual({ pushName: 'Alice' }); + }); +}); diff --git a/src/engine/adapters/baileys-message-mapper.ts b/src/engine/adapters/baileys-message-mapper.ts new file mode 100644 index 00000000..3fab3473 --- /dev/null +++ b/src/engine/adapters/baileys-message-mapper.ts @@ -0,0 +1,121 @@ +import { DeliveryStatus, IncomingMessage, MessageType } from '../interfaces/whatsapp-engine.interface'; + +/** + * Map a Baileys message content-type token (from `getContentType`) to the engine-neutral + * {@link MessageType}. `audioMessage` splits on the `ptt` flag into `voice` vs `audio`, + * mirroring the wwjs `ptt -> voice` mapping. Anything unmapped becomes `unknown`. + */ +export function mapBaileysMessageType(contentType: string | undefined, isPtt = false): MessageType { + switch (contentType) { + case 'conversation': + case 'extendedTextMessage': + return 'text'; + case 'imageMessage': + return 'image'; + case 'videoMessage': + return 'video'; + case 'audioMessage': + return isPtt ? 'voice' : 'audio'; + case 'documentMessage': + case 'documentWithCaptionMessage': + return 'document'; + case 'stickerMessage': + return 'sticker'; + case 'locationMessage': + case 'liveLocationMessage': + return 'location'; + case 'contactMessage': + case 'contactsArrayMessage': + return 'contact'; + default: + return 'unknown'; + } +} + +/** + * Map a Baileys delivery status (`proto.WebMessageInfo.Status`, numeric) to the engine-neutral + * {@link DeliveryStatus}. Returns `null` for an absent/unknown status so the adapter skips emitting + * an ack. PLAYED collapses to `read`, matching the wwjs adapter. + */ +export function mapBaileysStatus(status: number | null | undefined): DeliveryStatus | null { + switch (status) { + case 0: + return 'failed'; // ERROR + case 1: + return 'pending'; // PENDING + case 2: + return 'sent'; // SERVER_ACK + case 3: + return 'delivered'; // DELIVERY_ACK + case 4: + return 'read'; // READ + case 5: + return 'read'; // PLAYED + default: + return null; + } +} + +/** + * The subset of a Baileys `WAMessage` the adapter reads (after proto extraction) to build the + * base of an {@link IncomingMessage}. Declared explicitly so the neutral-shape logic is + * unit-testable without constructing a full proto message — mirrors wwjs `RawMessageFields`. + */ +export interface BaileysIncomingFields { + id: string; + /** The chat JID (`key.remoteJid`): a contact, a `@g.us` group, or `status@broadcast`. */ + remoteJid: string; + fromMe: boolean; + /** Group sender (`key.participant`); `remoteJid` is the group JID for group messages. */ + participant?: string; + body: string; + /** Result of `getContentType(msg.message)`. */ + contentType: string | undefined; + /** `audioMessage.ptt === true` — distinguishes a voice note from an audio file. */ + isPtt?: boolean; + timestamp: number; + pushName?: string; + /** The account's own normalized JID, for from/to on outgoing messages. */ + selfJid?: string; +} + +/** + * Build a neutral {@link IncomingMessage} from extracted Baileys fields. The chat is always + * `remoteJid` (Baileys reports the conversation directly); `fromMe` only flips from/to. The group + * sender lives in `participant` (exposed as `author`), matching the wwjs convention where `from` + * is the group JID. + */ +export function buildIncomingMessageFromBaileys(fields: BaileysIncomingFields): IncomingMessage { + const chatId = fields.remoteJid; + const isGroup = chatId.endsWith('@g.us'); + const self = fields.selfJid ?? ''; + + const incoming: IncomingMessage = { + id: fields.id, + from: fields.fromMe ? self : chatId, + to: fields.fromMe ? chatId : self, + chatId, + body: fields.body, + type: mapBaileysMessageType(fields.contentType, fields.isPtt), + timestamp: fields.timestamp, + fromMe: fields.fromMe, + isGroup, + isStatusBroadcast: chatId === 'status@broadcast', + }; + + if (isGroup && fields.participant) { + incoming.author = fields.participant; + } + + // The real sender for an @lid check is the participant in a group, else the chat JID itself. + const senderJid = fields.participant ?? chatId; + if (senderJid.endsWith('@lid')) { + incoming.isLidSender = true; + } + + if (fields.pushName) { + incoming.contact = { pushName: fields.pushName }; + } + + return incoming; +} diff --git a/src/engine/adapters/baileys.adapter.spec.ts b/src/engine/adapters/baileys.adapter.spec.ts new file mode 100644 index 00000000..c087d1f4 --- /dev/null +++ b/src/engine/adapters/baileys.adapter.spec.ts @@ -0,0 +1,272 @@ +import { EventEmitter } from 'events'; + +// A fake Baileys socket: an event emitter wearing the methods the adapter calls. +class FakeSock extends EventEmitter { + public ev = { + on: (event: string, handler: (arg: unknown) => void) => { + this.emitter.on(event, handler); + }, + }; + public emitter = new EventEmitter(); + public user: { id: string; name?: string } | undefined; + public requestPairingCode = jest.fn().mockResolvedValue('ABCD-EFGH'); + public end = jest.fn(); + public logout = jest.fn().mockResolvedValue(undefined); + public sendMessage = jest.fn(); + public onWhatsApp = jest.fn(); + public sendPresenceUpdate = jest.fn().mockResolvedValue(undefined); + fire(event: string, arg: unknown): void { + this.emitter.emit(event, arg); + } + resetEmitter(): void { + this.emitter.removeAllListeners(); + } +} + +const fakeSock = new FakeSock(); +const saveCreds = jest.fn().mockResolvedValue(undefined); + +jest.mock('@whiskeysockets/baileys', () => ({ + __esModule: true, + default: jest.fn(() => fakeSock), + useMultiFileAuthState: jest.fn().mockResolvedValue({ state: { creds: {}, keys: {} }, saveCreds }), + fetchLatestBaileysVersion: jest.fn().mockResolvedValue({ version: [2, 3000, 0] }), + getContentType: jest.fn(() => 'conversation'), + DisconnectReason: { loggedOut: 401, restartRequired: 515 }, +})); + +import { BaileysAdapter } from './baileys.adapter'; +import { EngineStatus, EngineEventCallbacks } from '../interfaces/whatsapp-engine.interface'; +import { EngineNotReadyError } from '../../common/errors/engine-not-ready.error'; +import { EngineNotSupportedError } from '../../common/errors/engine-not-supported.error'; + +const newAdapter = (): BaileysAdapter => new BaileysAdapter({ sessionId: 'sess-1', authDir: './data/baileys' }); + +const noopCallbacks = (over: Partial = {}): EngineEventCallbacks => over; + +describe('BaileysAdapter lifecycle & status', () => { + beforeEach(() => { + fakeSock.user = undefined; + fakeSock.resetEmitter(); // drop listeners from previous test's initialize() + jest.clearAllMocks(); + }); + + it('starts DISCONNECTED', () => { + expect(newAdapter().getStatus()).toBe(EngineStatus.DISCONNECTED); + }); + + it('emits onQRCode and moves to QR_READY on a connection.update with a qr', async () => { + const onQRCode = jest.fn(); + const adapter = newAdapter(); + await adapter.initialize(noopCallbacks({ onQRCode })); + fakeSock.fire('connection.update', { qr: 'QR-STRING' }); + expect(onQRCode).toHaveBeenCalledWith('QR-STRING'); + expect(adapter.getStatus()).toBe(EngineStatus.QR_READY); + expect(adapter.getQRCode()).toBe('QR-STRING'); + }); + + it('captures phone/pushName and fires onReady on connection open', async () => { + const onReady = jest.fn(); + const adapter = newAdapter(); + await adapter.initialize(noopCallbacks({ onReady })); + fakeSock.user = { id: '628999:12@s.whatsapp.net', name: 'Me' }; + fakeSock.fire('connection.update', { connection: 'open' }); + expect(adapter.getStatus()).toBe(EngineStatus.READY); + expect(adapter.getPhoneNumber()).toBe('628999'); + expect(adapter.getPushName()).toBe('Me'); + expect(onReady).toHaveBeenCalledWith('628999', 'Me'); + }); + + it('on a logged-out close: DISCONNECTED, onDisconnected, and NO reconnect', async () => { + const onDisconnected = jest.fn(); + const adapter = newAdapter(); + await adapter.initialize(noopCallbacks({ onDisconnected })); + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access + const makeWASocket = jest.requireMock('@whiskeysockets/baileys').default as jest.Mock; + makeWASocket.mockClear(); + fakeSock.fire('connection.update', { + connection: 'close', + lastDisconnect: { error: { output: { statusCode: 401 } } }, + }); + expect(adapter.getStatus()).toBe(EngineStatus.DISCONNECTED); + expect(onDisconnected).toHaveBeenCalled(); + expect(makeWASocket).not.toHaveBeenCalled(); // no reconnect + }); + + it('on a recoverable close: reconnects (re-creates the socket) and does NOT fire onDisconnected', async () => { + const onDisconnected = jest.fn(); + const adapter = newAdapter(); + await adapter.initialize(noopCallbacks({ onDisconnected })); + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access + const makeWASocket = jest.requireMock('@whiskeysockets/baileys').default as jest.Mock; + makeWASocket.mockClear(); + fakeSock.fire('connection.update', { + connection: 'close', + lastDisconnect: { error: { output: { statusCode: 515 } } }, + }); + await new Promise(r => setImmediate(r)); // let the async connect() run + expect(makeWASocket).toHaveBeenCalledTimes(1); + expect(onDisconnected).not.toHaveBeenCalled(); + }); + + it('disconnect() ends the socket and does not reconnect', async () => { + const adapter = newAdapter(); + await adapter.initialize(noopCallbacks({})); + await adapter.disconnect(); + expect(fakeSock.end).toHaveBeenCalled(); + expect(adapter.getStatus()).toBe(EngineStatus.DISCONNECTED); + }); + + it('requestPairingCode throws EngineNotReadyError before initialize()', async () => { + const adapter = newAdapter(); + await expect(adapter.requestPairingCode('628999')).rejects.toBeInstanceOf(EngineNotReadyError); + }); + + it('requestPairingCode delegates to the socket', async () => { + const adapter = newAdapter(); + await adapter.initialize(noopCallbacks({})); + await expect(adapter.requestPairingCode('628999')).resolves.toBe('ABCD-EFGH'); + expect(fakeSock.requestPairingCode).toHaveBeenCalledWith('628999'); + }); + + it('persists creds: subscribes saveCreds to creds.update', async () => { + const adapter = newAdapter(); + await adapter.initialize(noopCallbacks({})); + fakeSock.fire('creds.update', {}); + expect(saveCreds).toHaveBeenCalled(); + }); +}); + +describe('BaileysAdapter capability gating', () => { + it('throws EngineNotSupportedError for store-backed methods (e.g. getGroups, getChats)', async () => { + const adapter = newAdapter(); + await expect(adapter.getGroups()).rejects.toBeInstanceOf(EngineNotSupportedError); + await expect(adapter.getChats()).rejects.toBeInstanceOf(EngineNotSupportedError); + await expect(adapter.sendImageMessage('x', { mimetype: 'image/png', data: 'AAA' })).rejects.toBeInstanceOf( + EngineNotSupportedError, + ); + }); +}); + +describe('BaileysAdapter messaging', () => { + beforeEach(() => { + fakeSock.user = { id: '628999:1@s.whatsapp.net', name: 'Me' }; + jest.clearAllMocks(); + }); + + const readyAdapter = async (over: Partial = {}): Promise => { + const adapter = newAdapter(); + await adapter.initialize(over); + fakeSock.fire('connection.update', { connection: 'open' }); + return adapter; + }; + + it('sendTextMessage calls sock.sendMessage(jid, { text }) and returns the message id', async () => { + fakeSock.sendMessage.mockResolvedValue({ key: { id: 'OUT1' }, messageTimestamp: 1700000001 }); + const adapter = await readyAdapter(); + const res = await adapter.sendTextMessage('628111@s.whatsapp.net', 'hello'); + expect(fakeSock.sendMessage).toHaveBeenCalledWith('628111@s.whatsapp.net', { text: 'hello' }); + expect(res).toEqual({ id: 'OUT1', timestamp: 1700000001 }); + }); + + it('getNumberId resolves via onWhatsApp and returns the jid when it exists', async () => { + fakeSock.onWhatsApp.mockResolvedValue([{ jid: '628111@s.whatsapp.net', exists: true }]); + const adapter = await readyAdapter(); + await expect(adapter.getNumberId('628111')).resolves.toBe('628111@s.whatsapp.net'); + await expect(adapter.checkNumberExists('628111')).resolves.toBe(true); + }); + + it('getNumberId returns null when the number is not on WhatsApp', async () => { + fakeSock.onWhatsApp.mockResolvedValue([{ jid: '628111@s.whatsapp.net', exists: false }]); + const adapter = await readyAdapter(); + await expect(adapter.getNumberId('628111')).resolves.toBeNull(); + await expect(adapter.checkNumberExists('628111')).resolves.toBe(false); + }); + + it('sendChatState maps typing -> composing presence', async () => { + const adapter = await readyAdapter(); + await adapter.sendChatState('628111@s.whatsapp.net', 'typing'); + expect(fakeSock.sendPresenceUpdate).toHaveBeenCalledWith('composing', '628111@s.whatsapp.net'); + }); + + it('messaging methods throw EngineNotReadyError before the connection is open', async () => { + const adapter = newAdapter(); + await adapter.initialize({}); + await expect(adapter.sendTextMessage('x', 'y')).rejects.toBeInstanceOf(EngineNotReadyError); + await expect(adapter.checkNumberExists('628111')).rejects.toBeInstanceOf(EngineNotReadyError); + await expect(adapter.getNumberId('628111')).rejects.toBeInstanceOf(EngineNotReadyError); + await expect(adapter.sendChatState('628111@s.whatsapp.net', 'typing')).rejects.toBeInstanceOf(EngineNotReadyError); + }); +}); + +describe('BaileysAdapter inbound fan-out', () => { + // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion + const baileys = jest.requireMock('@whiskeysockets/baileys') as { getContentType: jest.Mock }; + + beforeEach(() => { + fakeSock.user = { id: '628999:1@s.whatsapp.net', name: 'Me' }; + jest.clearAllMocks(); + baileys.getContentType.mockReturnValue('conversation'); + }); + + it('routes an inbound (not fromMe) message to onMessage with a neutral shape', async () => { + const onMessage = jest.fn(); + const adapter = newAdapter(); + await adapter.initialize({ onMessage }); + fakeSock.fire('messages.upsert', { + type: 'notify', + messages: [ + { + key: { remoteJid: '628111@s.whatsapp.net', fromMe: false, id: 'IN1' }, + message: { conversation: 'hi there' }, + messageTimestamp: 1700000002, + pushName: 'Alice', + }, + ], + }); + expect(onMessage).toHaveBeenCalledTimes(1); + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access + const msg = onMessage.mock.calls[0][0] as { id: string; body: string; type: string; fromMe: boolean }; + expect(msg).toMatchObject({ id: 'IN1', body: 'hi there', type: 'text', fromMe: false }); + }); + + it('routes a fromMe message to onMessageCreate (outgoing), not onMessage', async () => { + const onMessage = jest.fn(); + const onMessageCreate = jest.fn(); + const adapter = newAdapter(); + await adapter.initialize({ onMessage, onMessageCreate }); + fakeSock.fire('messages.upsert', { + type: 'notify', + messages: [ + { + key: { remoteJid: '628111@s.whatsapp.net', fromMe: true, id: 'OUT2' }, + message: { conversation: 'sent from phone' }, + messageTimestamp: 1700000003, + }, + ], + }); + expect(onMessageCreate).toHaveBeenCalledTimes(1); + expect(onMessage).not.toHaveBeenCalled(); + }); + + it('ignores append (history) upserts', async () => { + const onMessage = jest.fn(); + const adapter = newAdapter(); + await adapter.initialize({ onMessage }); + fakeSock.fire('messages.upsert', { + type: 'append', + messages: [ + { key: { remoteJid: '628111@s.whatsapp.net', fromMe: false, id: 'OLD' }, message: { conversation: 'old' } }, + ], + }); + expect(onMessage).not.toHaveBeenCalled(); + }); + + it('emits onMessageAck from messages.update with a neutral status', async () => { + const onMessageAck = jest.fn(); + const adapter = newAdapter(); + await adapter.initialize({ onMessageAck }); + fakeSock.fire('messages.update', [{ key: { id: 'OUT1' }, update: { status: 3 } }]); + expect(onMessageAck).toHaveBeenCalledWith('OUT1', 'delivered'); + }); +}); diff --git a/src/engine/adapters/baileys.adapter.ts b/src/engine/adapters/baileys.adapter.ts new file mode 100644 index 00000000..0082eeec --- /dev/null +++ b/src/engine/adapters/baileys.adapter.ts @@ -0,0 +1,509 @@ +import * as path from 'path'; +import makeWASocket, { + DisconnectReason, + fetchLatestBaileysVersion, + getContentType, + useMultiFileAuthState, +} from '@whiskeysockets/baileys'; +import type { WAMessage, WASocket } from '@whiskeysockets/baileys'; +import { buildIncomingMessageFromBaileys, mapBaileysStatus } from './baileys-message-mapper'; +import type { ILogger } from '@whiskeysockets/baileys/lib/Utils/logger.js'; +import { + ChatState, + Channel, + ChannelMessage, + Catalog, + Contact, + ContactCard, + EngineEventCallbacks, + EngineStatus, + Group, + GroupInfo, + IncomingMessage, + IWhatsAppEngine, + Label, + LocationInput, + MediaInput, + MessageReaction, + MessageResult, + PaginatedProducts, + Product, + ProductQueryOptions, + Status, + StatusResult, + ChatSummary, + TextStatusOptions, +} from '../interfaces/whatsapp-engine.interface'; +import { EngineNotReadyError } from '../../common/errors/engine-not-ready.error'; +import { EngineNotSupportedError } from '../../common/errors/engine-not-supported.error'; +import { createLogger } from '../../common/services/logger.service'; +import { BaileysAdapterConfig, BaileysLogger } from '../types/baileys.types'; + +/** Linked-device identity shown in WhatsApp (Settings → Linked Devices). */ +const BAILEYS_BROWSER: [string, string, string] = ['OpenWA', 'Chrome', '120.0.0']; + +/** Fully silent logger so Baileys does not spam stdout; diagnostics flow via connection.update. */ +function createSilentLogger(): BaileysLogger { + const noop = (): void => {}; + const logger: BaileysLogger = { + level: 'silent', + child: () => logger, + trace: noop, + debug: noop, + info: noop, + warn: noop, + error: noop, + }; + return logger; +} + +export class BaileysAdapter implements IWhatsAppEngine { + private readonly logger = createLogger('BaileysAdapter'); + private readonly authPath: string; + private sock: WASocket | null = null; + private status: EngineStatus = EngineStatus.DISCONNECTED; + private qrCode: string | null = null; + private phoneNumber: string | null = null; + private pushName: string | null = null; + private callbacks: EngineEventCallbacks = {}; + private intentionalClose = false; + + constructor(private readonly config: BaileysAdapterConfig) { + // Isolate each session's auth state under its own subdirectory of the shared auth dir. + this.authPath = path.join(config.authDir, config.sessionId); + if (config.proxyUrl) { + // Proxy support is gated for this slice — Baileys proxying needs an http/socks agent (a new dep). + this.logger.warn('Proxy configured but not supported by the baileys engine in this slice; ignoring it', { + action: 'baileys_proxy_unsupported', + sessionId: config.sessionId, + }); + } + } + + // ----- Lifecycle ----- + + async initialize(callbacks: EngineEventCallbacks): Promise { + this.callbacks = callbacks; + this.intentionalClose = false; + await this.connect(); + } + + private async connect(): Promise { + this.setStatus(EngineStatus.INITIALIZING); + const { state, saveCreds } = await useMultiFileAuthState(this.authPath); + const { version } = await fetchLatestBaileysVersion(); + + const sock = makeWASocket({ + auth: state, + version, + browser: BAILEYS_BROWSER, + printQRInTerminal: false, + // BaileysLogger matches ILogger exactly; cast needed because the module resolves + // the type through a deep import path that TypeScript does not auto-unify here. + // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion + logger: createSilentLogger() as unknown as ILogger, + }); + this.sock = sock; + + // eslint-disable-next-line @typescript-eslint/no-misused-promises + sock.ev.on('creds.update', saveCreds); + sock.ev.on('connection.update', update => this.handleConnectionUpdate(update)); + sock.ev.on('messages.upsert', event => this.handleMessagesUpsert(event)); + sock.ev.on('messages.update', updates => this.handleMessagesUpdate(updates)); + } + + private handleConnectionUpdate(update: { + connection?: string; + qr?: string; + lastDisconnect?: { error?: unknown }; + }): void { + const { connection, qr, lastDisconnect } = update; + + if (qr) { + this.qrCode = qr; + this.setStatus(EngineStatus.QR_READY); + this.callbacks.onQRCode?.(qr); + } + + if (connection === 'connecting') { + this.setStatus(EngineStatus.INITIALIZING); + } + + if (connection === 'open') { + this.qrCode = null; + this.phoneNumber = this.extractPhone(this.sock?.user?.id); + this.pushName = this.sock?.user?.name ?? null; + this.setStatus(EngineStatus.READY); + this.callbacks.onReady?.(this.phoneNumber ?? '', this.pushName ?? ''); + } + + if (connection === 'close') { + const statusCode = (lastDisconnect?.error as { output?: { statusCode?: number } } | undefined)?.output + ?.statusCode; + + if (this.intentionalClose) { + this.setStatus(EngineStatus.DISCONNECTED); + return; + } + + if (statusCode === DisconnectReason.loggedOut) { + // Credentials invalidated — terminal. Re-linking requires a fresh QR/pairing. + this.setStatus(EngineStatus.DISCONNECTED); + this.callbacks.onDisconnected?.('logged out'); + return; + } + + // Recoverable (e.g. restartRequired right after pairing, transient drop) — reconnect. + // Do NOT fire onDisconnected here; this is a transient drop, not a terminal disconnect. + // connect() calls setStatus(INITIALIZING) which fires onStateChanged — that is the correct signal. + this.logger.log('Baileys connection dropped; reconnecting', { statusCode }); + this.connect().catch(err => { + this.setStatus(EngineStatus.FAILED); + this.callbacks.onError?.(err instanceof Error ? err.message : String(err)); + }); + } + } + + disconnect(): Promise { + this.intentionalClose = true; + this.sock?.end(undefined); + this.sock = null; + this.setStatus(EngineStatus.DISCONNECTED); + return Promise.resolve(); + } + + async logout(): Promise { + this.intentionalClose = true; + try { + await this.sock?.logout(); + } catch (err) { + this.logger.warn('Baileys logout failed; ending socket', { + error: err instanceof Error ? err.message : String(err), + }); + this.sock?.end(undefined); + } + this.sock = null; + this.setStatus(EngineStatus.DISCONNECTED); + // ponytail: leaves the multi-file auth dir on disk; a fresh link overwrites it. Add fs cleanup if + // stale creds ever block re-linking. + } + + destroy(): Promise { + this.intentionalClose = true; + this.sock?.end(undefined); + this.sock = null; + this.setStatus(EngineStatus.DISCONNECTED); + return Promise.resolve(); + } + + // ----- Status ----- + + getStatus(): EngineStatus { + return this.status; + } + + getQRCode(): string | null { + return this.qrCode; + } + + async requestPairingCode(phoneNumber: string): Promise { + if (!this.sock) { + throw new EngineNotReadyError('Cannot request a pairing code before the engine is initialized.'); + } + return this.sock.requestPairingCode(phoneNumber); + } + + getPhoneNumber(): string | null { + return this.phoneNumber; + } + + getPushName(): string | null { + return this.pushName; + } + + // ----- Messaging ----- + + async sendTextMessage(chatId: string, text: string): Promise { + this.ensureReady(); + const sent = await this.sock!.sendMessage(chatId, { text }); + return { + id: sent?.key?.id ?? '', + timestamp: this.toUnixSeconds(sent?.messageTimestamp), + }; + } + + async checkNumberExists(number: string): Promise { + return (await this.getNumberId(number)) !== null; + } + + async getNumberId(number: string): Promise { + this.ensureReady(); + const results = await this.sock!.onWhatsApp(number); + const hit = results?.[0]; + return hit?.exists ? hit.jid : null; + } + + async sendChatState(chatId: string, state: ChatState): Promise { + this.ensureReady(); + const presence = state === 'typing' ? 'composing' : state === 'recording' ? 'recording' : 'paused'; + await this.sock!.sendPresenceUpdate(presence, chatId); + } + + // ----- Gated: not supported by this minimal slice (no store) ----- + /* eslint-disable @typescript-eslint/no-unused-vars */ + + sendImageMessage(_chatId: string, _media: MediaInput): Promise { + return this.unsupported('sendImageMessage'); + } + sendVideoMessage(_chatId: string, _media: MediaInput): Promise { + return this.unsupported('sendVideoMessage'); + } + sendAudioMessage(_chatId: string, _media: MediaInput): Promise { + return this.unsupported('sendAudioMessage'); + } + sendDocumentMessage(_chatId: string, _media: MediaInput): Promise { + return this.unsupported('sendDocumentMessage'); + } + sendLocationMessage(_chatId: string, _location: LocationInput): Promise { + return this.unsupported('sendLocationMessage'); + } + sendContactMessage(_chatId: string, _contact: ContactCard): Promise { + return this.unsupported('sendContactMessage'); + } + sendStickerMessage(_chatId: string, _media: MediaInput): Promise { + return this.unsupported('sendStickerMessage'); + } + replyToMessage(_chatId: string, _quotedMsgId: string, _text: string): Promise { + return this.unsupported('replyToMessage'); + } + forwardMessage(_fromChatId: string, _toChatId: string, _messageId: string): Promise { + return this.unsupported('forwardMessage'); + } + reactToMessage(_chatId: string, _messageId: string, _emoji: string): Promise { + return this.unsupported('reactToMessage'); + } + getMessageReactions(_chatId: string, _messageId: string): Promise { + return this.unsupported('getMessageReactions'); + } + getContacts(): Promise { + return this.unsupported('getContacts'); + } + getContactById(_contactId: string): Promise { + return this.unsupported('getContactById'); + } + resolveContactPhone(_contactId: string): Promise { + return this.unsupported('resolveContactPhone'); + } + getGroups(): Promise { + return this.unsupported('getGroups'); + } + getGroupInfo(_groupId: string): Promise { + return this.unsupported('getGroupInfo'); + } + createGroup(_name: string, _participants: string[]): Promise { + return this.unsupported('createGroup'); + } + addParticipants(_groupId: string, _participants: string[]): Promise { + return this.unsupported('addParticipants'); + } + removeParticipants(_groupId: string, _participants: string[]): Promise { + return this.unsupported('removeParticipants'); + } + promoteParticipants(_groupId: string, _participants: string[]): Promise { + return this.unsupported('promoteParticipants'); + } + demoteParticipants(_groupId: string, _participants: string[]): Promise { + return this.unsupported('demoteParticipants'); + } + leaveGroup(_groupId: string): Promise { + return this.unsupported('leaveGroup'); + } + setGroupSubject(_groupId: string, _subject: string): Promise { + return this.unsupported('setGroupSubject'); + } + setGroupDescription(_groupId: string, _description: string): Promise { + return this.unsupported('setGroupDescription'); + } + getGroupInviteCode(_groupId: string): Promise { + return this.unsupported('getGroupInviteCode'); + } + revokeGroupInviteCode(_groupId: string): Promise { + return this.unsupported('revokeGroupInviteCode'); + } + deleteMessage(_chatId: string, _messageId: string, _forEveryone?: boolean): Promise { + return this.unsupported('deleteMessage'); + } + getChatHistory(_chatId: string, _limit?: number, _includeMedia?: boolean): Promise { + return this.unsupported('getChatHistory'); + } + getProfilePicture(_contactId: string): Promise { + return this.unsupported('getProfilePicture'); + } + blockContact(_contactId: string): Promise { + return this.unsupported('blockContact'); + } + unblockContact(_contactId: string): Promise { + return this.unsupported('unblockContact'); + } + getLabels(): Promise { + return this.unsupported('getLabels'); + } + getLabelById(_labelId: string): Promise