Compare commits

..
159 Commits
Author SHA1 Message Date
devlikepro 443f09d5a2 [core] 2025.2.3
Release / WEBJS - chrome - amd64 - chrome (push) Waiting to run
Release / WEBJS - chromium - amd64 - latest (push) Waiting to run
Release / WEBJS - chromium - linux/arm64 - arm (push) Waiting to run
Release / GOWS - none - amd64 - gows (push) Waiting to run
Release / GOWS - none - linux/arm64 - gows-arm (push) Waiting to run
Release / NOWEB - none - amd64 - noweb (push) Waiting to run
Release / NOWEB - none - linux/arm64 - noweb-arm (push) Waiting to run
2025-02-12 17:18:00 +07:00
devlikepro 3500952bb7 [core] Up GOWS 2025-02-12 17:18:00 +07:00
devlikepro e98182c4b5 [core] Add /profile API
fix #529
2025-02-12 17:18:00 +07:00
devlikepro 2337827744 [core] GOWS hasMedia fix 2025-02-12 17:18:00 +07:00
devlikepro 556dd66af3 [core] NOWEB hasMedia fix 2025-02-12 17:18:00 +07:00
devlikepro 8bdad3f4f9 [core] WEBJS hasMedia fix 2025-02-12 17:17:59 +07:00
devlikepro 6346d4143d [core] 2025.2.2
Release / WEBJS - chrome - amd64 - chrome (push) Waiting to run
Release / WEBJS - chromium - amd64 - latest (push) Waiting to run
Release / WEBJS - chromium - linux/arm64 - arm (push) Waiting to run
Release / GOWS - none - amd64 - gows (push) Waiting to run
Release / GOWS - none - linux/arm64 - gows-arm (push) Waiting to run
Release / NOWEB - none - amd64 - noweb (push) Waiting to run
Release / NOWEB - none - linux/arm64 - noweb-arm (push) Waiting to run
2025-02-10 08:57:14 +07:00
devlikepro fd8dc88760 [core] rm auth/authorize-code 2025-02-10 08:57:14 +07:00
devlikepro b2810f390e [core] max connections 3000 2025-02-10 08:57:14 +07:00
devlikepro 23d3cd0f4b [core] Up GOWS - build proto 2025-02-10 08:57:13 +07:00
devlikepro 4a3c76773d [core] Up GOWS 2025-02-10 08:57:12 +07:00
devlikepro d23e7604f0 [core] NOWEB Channels Search 2025-02-10 08:57:12 +07:00
devlikepro 0b3f6c6eff [core] GOWS - Search Channels 2025-02-10 08:57:12 +07:00
devlikepro f2869c892b [core] WEBJS - ensure suffix 2025-02-10 08:57:12 +07:00
devlikepro 3755a9529b [core] GOWS - Channels Search, Messages Preview 2025-02-10 08:57:11 +07:00
devlikepro 890f0593ee [core] NOWEB - Channels Search, Messages Preview 2025-02-10 08:57:11 +07:00
devlikepro e6d666b5b0 [core] WEBJS Channels Search, Messages Preview 2025-02-10 08:57:10 +07:00
devlikepro f440acf145 [core] Channels Search, Messages Preview - API 2025-02-10 08:57:10 +07:00
devlikepro 1ad0e0c6dd [core] Add proto files
fixes #784
2025-02-07 08:41:37 +07:00
devlikepro bb8691ddc0 [core] Use waha docker image
Release / WEBJS - chromium - amd64 - latest (push) Waiting to run
Release / WEBJS - chromium - linux/arm64 - arm (push) Waiting to run
Release / GOWS - none - amd64 - gows (push) Waiting to run
Release / GOWS - none - linux/arm64 - gows-arm (push) Waiting to run
Release / NOWEB - none - amd64 - noweb (push) Waiting to run
Release / NOWEB - none - linux/arm64 - noweb-arm (push) Waiting to run
Release / WEBJS - chrome - amd64 - chrome (push) Waiting to run
2025-02-02 13:25:24 +07:00
devlikepro cf3041d07b [core] swagger 2025-02-02 13:23:24 +07:00
devlikepro 313b3d5d6b [core] up NOWEB 2025-02-02 13:23:24 +07:00
devlikepro 122e7336be [core] NOWEB - use channels-6712 branch 2025-02-02 13:23:24 +07:00
devlikepro e954453f6b [core] up GOWS 2025-02-02 13:23:24 +07:00
devlikepro 55ef3d09f3 [core] 2025.2.1 2025-01-30 14:35:08 +07:00
devlikepro 88b9cc5ac9 [core] Up dashboard 2025-01-30 14:35:08 +07:00
devlikepro f07cb7470d [core] GOWS - contacts, chats 2025-01-30 14:33:50 +07:00
devlikepro bd874a0232 [core] Up NOWEB 2025-01-30 14:33:50 +07:00
devlikepro 7cc4cccb9c [core] Fetch gows bin and proto from gows releases 2025-01-30 14:33:50 +07:00
devlikepro e85a3bacb7 [core] GOWS - Add ack to incoming messages 2025-01-30 14:33:50 +07:00
devlikepro 530dfa758f [core] Handle keep alive restored 2025-01-30 14:33:50 +07:00
devlikepro 8bc3489060 [core] 2025.1.6
Release / WEBJS - chrome - amd64 - chrome (push) Waiting to run
Release / WEBJS - chromium - amd64 - latest (push) Waiting to run
Release / WEBJS - chromium - linux/arm64 - arm (push) Waiting to run
Release / GOWS - none - amd64 - gows (push) Waiting to run
Release / GOWS - none - linux/arm64 - gows-arm (push) Waiting to run
Release / NOWEB - none - amd64 - noweb (push) Waiting to run
Release / NOWEB - none - linux/arm64 - noweb-arm (push) Waiting to run
2025-01-22 09:20:20 +07:00
devlikepro 6914cb171a [core] WEBJS send image to channel 2025-01-22 09:20:12 +07:00
devlikepro 176c2bbd4c [core] 2025.1.5
Release / WEBJS - chrome - amd64 - chrome (push) Waiting to run
Release / WEBJS - chromium - amd64 - latest (push) Waiting to run
Release / WEBJS - chromium - linux/arm64 - arm (push) Waiting to run
Release / GOWS - none - amd64 - gows (push) Waiting to run
Release / GOWS - none - linux/arm64 - gows-arm (push) Waiting to run
Release / NOWEB - none - amd64 - noweb (push) Waiting to run
Release / NOWEB - none - linux/arm64 - noweb-arm (push) Waiting to run
2025-01-21 11:57:40 +07:00
devlikepro 7771954455 [core] build proto on package install 2025-01-21 11:57:28 +07:00
devlikepro a66743bb20 [core] rm old scripts 2025-01-21 11:57:27 +07:00
devlikepro 851c7b75f9 [core] GOWS Channels 2025-01-21 11:57:26 +07:00
devlikepro ed0b406027 [core] GOWS Send Status 2025-01-21 11:57:26 +07:00
devlikepro 71985972a2 [core] GOWS check exists 2025-01-21 11:57:26 +07:00
devlikepro 2dbeef9290 [core] font 0 by default in status 2025-01-21 11:57:25 +07:00
devlikepro a79453270d [core] WEBJS handle status 2025-01-21 11:57:25 +07:00
devlikepro ae71808c61 [core] ws heartbeat do not return after closing one client 2025-01-21 11:57:25 +07:00
devlikepro 5c195ba37b [core] ws 60 seconds heartbeat 2025-01-21 11:57:24 +07:00
devlikepro 607ee17a86 [core] perf tests 2025-01-21 11:57:23 +07:00
devlikepro a15aacc5b3 [core] perf tests 2025-01-21 11:57:23 +07:00
devlikepro 78fb4b1a65 [core] Init store
Release / WEBJS - chrome - amd64 - chrome (push) Waiting to run
Release / WEBJS - chromium - amd64 - latest (push) Waiting to run
Release / WEBJS - chromium - linux/arm64 - arm (push) Waiting to run
Release / GOWS - none - amd64 - gows (push) Waiting to run
Release / GOWS - none - linux/arm64 - gows-arm (push) Waiting to run
Release / NOWEB - none - amd64 - noweb (push) Waiting to run
Release / NOWEB - none - linux/arm64 - noweb-arm (push) Waiting to run
2025-01-19 15:18:07 +07:00
devlikepro 8e566cba72 [core] 2025.1.4 2025-01-19 14:44:06 +07:00
devlikepro 36184288de [core] NOWEB PostgreSQL 2025-01-19 14:42:59 +07:00
devlikepro 6fc5bf38a7 [core] NOWEB - handle not found profile picture 2025-01-19 14:42:59 +07:00
devlikepro e146ecde70 [core] Add local store to WAHA Core
fix #741
2025-01-19 14:42:58 +07:00
devlikepro 72b796019a [core] PostgreSQL Media 2025-01-19 14:42:58 +07:00
devlikepro 0f66d15ba8 [core] PSQL Media 2025-01-19 14:42:57 +07:00
devlikepro 4695e933b8 [core] Up GOWS
[core] GOWS up
2025-01-19 14:42:57 +07:00
devlikepro 5e1da6cd6f [core] PostgreSQL docker-compose.yaml 2025-01-19 14:42:56 +07:00
devlikepro 0c3bbc0980 [core] PostgreSQL 2025-01-19 14:42:56 +07:00
devlikepro d2bb5bea63 [core] Add session config schema + migrations 2025-01-19 14:42:55 +07:00
devlikepro c76e191d4d [core] Extract general SqlKVRepository 2025-01-19 14:42:54 +07:00
devlikepro 171594d0ae [core] NOWEB Update contact if not found 2025-01-19 14:42:53 +07:00
devlikepro 4e5f499b48 [core] Rename db max name limit 2025-01-19 14:42:53 +07:00
devlikepro bb75ad8b8e [core] Add pg 2025-01-19 14:42:52 +07:00
devlikepro 482cc7028b [core] Move sqlite3 2025-01-19 14:42:51 +07:00
devlikepro 735bdb3854 [core] Websocket - close server and then terminate clients 2025-01-19 14:42:51 +07:00
devlikepro 1ee1bd78af [core] Stop events after we stop the session 2025-01-19 14:42:51 +07:00
devlikepro 00463c729d [core] SIGTERM to GOWS 2025-01-19 14:42:51 +07:00
devlikepro dac636b3bb [core] detached GOWS subprocess 2025-01-19 14:42:51 +07:00
devlikepro ad87121506 [core] make copy-dashboard (dist) 2025-01-19 14:42:51 +07:00
devlikepro 55d802f005 [core] Disable some flag for WEBJS stability 2025-01-19 14:42:51 +07:00
devlikepro f17164a3d9 [core] 2025.1.3
Release / WEBJS - chrome - amd64 - chrome (push) Waiting to run
Release / WEBJS - chromium - amd64 - latest (push) Waiting to run
Release / WEBJS - chromium - linux/arm64 - arm (push) Waiting to run
Release / GOWS - none - amd64 - gows (push) Waiting to run
Release / GOWS - none - linux/arm64 - gows-arm (push) Waiting to run
Release / NOWEB - none - amd64 - noweb (push) Waiting to run
Release / NOWEB - none - linux/arm64 - noweb-arm (push) Waiting to run
2025-01-10 11:01:11 +07:00
devlikepro 0b9f9b2335 [core] Add GOWS engine 2025-01-10 11:00:56 +07:00
devlikepro 71d3e24241 [core] nestjs forceCloseConnections 2025-01-10 11:00:56 +07:00
devlikepro ccd4512e8f [core] Get QR PNG on demand 2025-01-10 11:00:55 +07:00
devlikepro fe6b77d67d [core] Use UV_THREADPOOL_SIZE as cpu*1 2025-01-10 11:00:55 +07:00
devlikepro 58d12f4e70 [core] NOWEB - sleep 1s to get latest presence 2025-01-10 11:00:55 +07:00
devlikepro 17303039b8 [core] NOWEB subscribe presence 2025-01-10 11:00:55 +07:00
devlikepro f70d5203a3 [core] Websocket logs verbose 2025-01-10 11:00:55 +07:00
devlikepro efd6b1db65 [core] WEBJS Remove in-process-gpu
Release / WEBJS - chrome - amd64 - chrome (push) Waiting to run
Release / WEBJS - chromium - amd64 - latest (push) Waiting to run
Release / WEBJS - chromium - linux/arm64 - arm (push) Waiting to run
Release / NOWEB - none - amd64 - noweb (push) Waiting to run
Release / NOWEB - none - linux/arm64 - noweb-arm (push) Waiting to run
fix #725
2025-01-09 16:23:27 +07:00
devlikepro dcca0fa921 [core] 2025.1.2 2025-01-09 16:03:04 +07:00
devlikepro e5bc447807 [core] Load name from contact
fix #732
2025-01-09 14:31:22 +07:00
devlikepro cd6b4b5ab1 [core] Use atomic writes
fix #731
2025-01-09 14:21:38 +07:00
devlikepro fb79ae6138 [core] 2025.1.1
Release / NOWEB - none - amd64 - noweb (push) Waiting to run
Release / WEBJS - chrome - amd64 - chrome (push) Waiting to run
Release / WEBJS - chromium - amd64 - latest (push) Waiting to run
Release / WEBJS - chromium - linux/arm64 - arm (push) Waiting to run
Release / NOWEB - none - linux/arm64 - noweb-arm (push) Waiting to run
2025-01-08 09:46:15 +07:00
devlikepro 17801b0b13 [core] Set log level to "trace" if specified 2025-01-08 09:45:52 +07:00
devlikepro 9010388de0 [core] Add websocket send error handler 2025-01-08 09:45:52 +07:00
devlikepro 2da901f994 [core] Add lock timeout 10s to session operations 2025-01-08 09:45:52 +07:00
devlikepro 7832cc7a5d [core] NOWEB - save creds if any 2025-01-08 09:41:20 +07:00
devlikepro 13ab2e8d0e [core] downloadMedia=false in list messages 2024-12-29 14:52:53 +07:00
devlikepro 868d23a278 [core] Mark /api/messages as deprecated 2024-12-29 14:48:47 +07:00
devlikepro eaf2bca44e [core] 2024.12.6
Release / WEBJS - chrome - amd64 - chrome (push) Waiting to run
Release / WEBJS - chromium - amd64 - latest (push) Waiting to run
Release / WEBJS - chromium - linux/arm64 - arm (push) Waiting to run
Release / NOWEB - none - amd64 - noweb (push) Waiting to run
Release / NOWEB - none - linux/arm64 - noweb-arm (push) Waiting to run
2024-12-24 13:35:25 +07:00
devlikepro af8f537f58 [core] Up dashboard 2024-12-24 13:34:21 +07:00
devlikepro bc7ff472be [core] Chats overview API 2024-12-24 13:33:41 +07:00
devlikepro 9b9a2b5203 [core] NOWEB - listen update profile image 2024-12-24 13:33:40 +07:00
devlikepro 347259394b [core] Cache null picture to avoid additional requests 2024-12-24 13:33:40 +07:00
devlikepro ead61b5e37 [core] Add GET /api/:session/chats/:chatId/picture 2024-12-24 13:33:40 +07:00
devlikepro c9f7814f5d [core] Refresh profile picture 2024-12-24 13:33:39 +07:00
devlikepro 3ee3355e81 [core] Add in-memory cache for profile picture 2024-12-24 13:33:39 +07:00
devlikepro ce8f8b0ce9 [core] Add dedicated fetch contact profile picture
fix #381
2024-12-24 13:33:39 +07:00
devlikepro cae35cf3b9 [core] Up WEBJS
Fix #576 - add retries and send regular message anyway

fix #698
2024-12-24 13:33:39 +07:00
devlikepro 900e3c23bc [core] 2024.12.5
Release / WEBJS - chrome - amd64 - chrome (push) Waiting to run
Release / WEBJS - chromium - amd64 - latest (push) Waiting to run
Release / WEBJS - chromium - linux/arm64 - arm (push) Waiting to run
Release / NOWEB - none - amd64 - noweb (push) Waiting to run
Release / NOWEB - none - linux/arm64 - noweb-arm (push) Waiting to run
2024-12-16 11:56:50 +07:00
devlikepro aaeffeb983 [core] Update libsignal 2024-12-16 11:56:39 +07:00
devlikepro 15600324be [core] Update NOWEB 2024-12-16 11:56:39 +07:00
devlikepro da4913edb5 [core] Use libsignal fork
Add logging
2024-12-16 11:56:39 +07:00
devlikepro e8cc1e4906 [core] 2024.12.4
Release / WEBJS - chrome - amd64 - chrome (push) Waiting to run
Release / WEBJS - chromium - amd64 - latest (push) Waiting to run
Release / WEBJS - chromium - linux/arm64 - arm (push) Waiting to run
Release / NOWEB - none - amd64 - noweb (push) Waiting to run
Release / NOWEB - none - linux/arm64 - noweb-arm (push) Waiting to run
2024-12-14 19:15:09 +07:00
devlikepro 801fc3f9fd [core] Join group via code
fix #688
2024-12-14 19:14:56 +07:00
devlikepro 703b0b4cde [core] Fix groups sort by
Release / WEBJS - chrome - amd64 - chrome (push) Waiting to run
Release / WEBJS - chromium - amd64 - latest (push) Waiting to run
Release / WEBJS - chromium - linux/arm64 - arm (push) Waiting to run
Release / NOWEB - none - amd64 - noweb (push) Waiting to run
Release / NOWEB - none - linux/arm64 - noweb-arm (push) Waiting to run
2024-12-09 11:30:39 +07:00
devlikepro d55895389a [core] 2024.12.3 2024-12-09 10:37:35 +07:00
devlikepro 56009e482b [core] Add dedicated POST /api/{session}/groups/refresh endpoint 2024-12-09 10:19:38 +07:00
devlikepro 767d1bb3e3 [core] Fix 'rate-overlimit' error for groups
fix #462
2024-12-08 18:23:39 +07:00
devlikepro 05132ac0f5 [core] Update NOWEB 1018710061 2024-12-08 18:23:39 +07:00
devlikepro fb372f8c28 [core] Set WHATSAPP_DEFAULT_ENGINE at build time
fix #414
2024-12-08 13:52:43 +07:00
devlikepro a2dc863b7a [core] 12.2
Release / amd64 - chrome - chrome (push) Waiting to run
Release / amd64 - chromium - latest (push) Waiting to run
Release / linux/arm64 - chromium - arm (push) Waiting to run
Release / amd64 - none - noweb (push) Waiting to run
Release / linux/arm64 - none - noweb-arm (push) Waiting to run
2024-12-07 00:15:49 +07:00
devlikepro 15671c6781 [core] NOWEB - potential fix for #671 2024-12-07 00:15:34 +07:00
devlikepro 6296115adb [core] Cache keys between restart 2024-12-07 00:15:34 +07:00
devlikepro 0a099a91d0 [core] 2024.12.1
Release / amd64 - chrome - chrome (push) Waiting to run
Release / amd64 - chromium - latest (push) Waiting to run
Release / linux/arm64 - chromium - arm (push) Waiting to run
Release / amd64 - none - noweb (push) Waiting to run
Release / linux/arm64 - none - noweb-arm (push) Waiting to run
2024-12-06 10:09:04 +07:00
devlikepro 5f060fe022 [core] Fix channel messages
fix #683
2024-12-06 10:09:03 +07:00
devlikepro a4bbe1cf7b [core] Fix GET channel by id
fix #682
2024-12-06 10:09:03 +07:00
devlikepro dce74ae7d5 [core] 2024.11.11
Release / amd64 - chrome - chrome (push) Waiting to run
Release / amd64 - chromium - latest (push) Waiting to run
Release / linux/arm64 - chromium - arm (push) Waiting to run
Release / amd64 - none - noweb (push) Waiting to run
Release / linux/arm64 - none - noweb-arm (push) Waiting to run
2024-12-01 16:23:50 +07:00
devlikepro 871c509493 [core] Up WEBJS
fix #664
2024-12-01 16:23:39 +07:00
devlikepro 9333470e3b [core] Fix 403 on media
fix #667
2024-12-01 15:56:40 +07:00
devlikepro 0c108f5232 [core] 2024.11.10
Release / amd64 - chrome - chrome (push) Waiting to run
Release / amd64 - chromium - latest (push) Waiting to run
Release / linux/arm64 - chromium - arm (push) Waiting to run
Release / amd64 - none - noweb (push) Waiting to run
Release / linux/arm64 - none - noweb-arm (push) Waiting to run
2024-12-01 15:37:21 +07:00
devlikepro 39d298c867 [core] WEBJS fix groups
fix #674 #670
2024-12-01 15:35:00 +07:00
devlikepro 376e804a28 [core] Up NOWEB 2024-12-01 15:16:47 +07:00
devlikepro 79e5c0589d [core] Up NOWEB
Release / amd64 - chrome - chrome (push) Waiting to run
Release / amd64 - chromium - latest (push) Waiting to run
Release / linux/arm64 - chromium - arm (push) Waiting to run
Release / amd64 - none - noweb (push) Waiting to run
Release / linux/arm64 - none - noweb-arm (push) Waiting to run
2024-11-29 14:51:16 +07:00
devlikepro f6679b7368 [core] 2024.11.9 2024-11-29 09:38:06 +07:00
devlikepro a3c0b86b67 [core] Add explicit placeholder cache 2024-11-29 09:37:49 +07:00
devlikepro c8f48dd62b [core] 11.8
Release / amd64 - chrome - chrome (push) Waiting to run
Release / amd64 - chromium - latest (push) Waiting to run
Release / linux/arm64 - chromium - arm (push) Waiting to run
Release / amd64 - none - noweb (push) Waiting to run
Release / linux/arm64 - none - noweb-arm (push) Waiting to run
2024-11-25 12:57:23 +07:00
devlikepro aed157697a [core] WEBJS - Fix QR code
fix #659
2024-11-25 12:56:51 +07:00
devlikepro 913b8606d9 [core] 2024.11.7
Release / amd64 - chrome - chrome (push) Waiting to run
Release / amd64 - chromium - latest (push) Waiting to run
Release / linux/arm64 - chromium - arm (push) Waiting to run
Release / amd64 - none - noweb (push) Waiting to run
Release / linux/arm64 - none - noweb-arm (push) Waiting to run
2024-11-24 14:06:33 +07:00
devlikepro 7cadbad36e [core] Add WebJSEngineConfigService.ts
fix #654 fix #653
2024-11-24 14:06:28 +07:00
devlikepro abb5bdffcc [core] 2024.11.6
Release / amd64 - chrome - chrome (push) Waiting to run
Release / amd64 - chromium - latest (push) Waiting to run
Release / linux/arm64 - chromium - arm (push) Waiting to run
Release / amd64 - none - noweb (push) Waiting to run
Release / linux/arm64 - none - noweb-arm (push) Waiting to run
2024-11-20 16:38:59 +07:00
devlikepro 6a44c15dd3 [core] fix undefined.get
fix #645
2024-11-20 16:38:43 +07:00
devlikepro e669dfe533 [core] Update dashboard
Release / amd64 - chrome - chrome (push) Waiting to run
Release / amd64 - chromium - latest (push) Waiting to run
Release / linux/arm64 - chromium - arm (push) Waiting to run
Release / amd64 - none - noweb (push) Waiting to run
Release / linux/arm64 - none - noweb-arm (push) Waiting to run
2024-11-19 13:17:40 +07:00
devlikepro be888f195d [core] NOWEB - fix timestamp 2024-11-19 13:17:39 +07:00
devlikepro d87cdef311 [core] Add participant for group 2024-11-19 13:17:39 +07:00
devlikepro ecf07cb520 [core] Pin Unpin message
fix #613
2024-11-19 13:17:39 +07:00
devlikepro 10829d53b9 [core] Check quoted message better 2024-11-19 13:17:38 +07:00
devlikepro 71093f2f31 [core] Rm unused code 2024-11-19 13:17:38 +07:00
devlikepro e88192002a [core] Fix log ctx 2024-11-19 13:17:38 +07:00
devlikepro 3e14282603 [core] 2024.11.5 2024-11-19 13:17:37 +07:00
devlikepro 5f16f4ca77 [core] Update dashboard 2024-11-19 13:17:37 +07:00
devlikepro 7b4fa4331f [core] Websockets 2024-11-19 13:17:36 +07:00
devlikepro 3ad0a463b5 [core] Lazy message and message.any 2024-11-19 13:17:36 +07:00
devlikepro d28d640f77 [core] Use EventWildUmask 2024-11-19 13:17:35 +07:00
devlikepro 594c0a6122 [core] Emit events in engine 2024-11-19 13:17:35 +07:00
devlikepro 843a826437 [core] Merge plus webhooks to core 2024-11-19 13:17:35 +07:00
devlikepro c4660bc824 [core] 2024.11.4
Release / amd64 - chrome - chrome (push) Waiting to run
Release / amd64 - chromium - latest (push) Waiting to run
Release / amd64 - none - noweb (push) Waiting to run
Release / linux/arm64 - none - noweb-arm (push) Waiting to run
Release / linux/arm64 - chromium - arm (push) Waiting to run
2024-11-10 12:48:54 +07:00
devlikepro 6053d38e4d [core] Handle WEBJS 429 error
fix #633
2024-11-10 12:48:54 +07:00
devlikepro 3865254a1c [core] NOWEB - get contacts list for status from store if not provided 2024-11-10 12:48:54 +07:00
devlikepro 65b61c09cb [core] Fix 10K status problem
fix #390 fix #413
2024-11-10 12:48:54 +07:00
devlikepro cce1618117 [core] add mongo auth by default 2024-11-08 18:14:35 +07:00
devlikepro 8b4b49a34b [core] waha-plus image 2024-11-08 17:19:50 +07:00
devlikepro 048c5dc2ec [core] send video back
Release / amd64 - chrome - chrome (push) Waiting to run
Release / amd64 - chromium - latest (push) Waiting to run
Release / linux/arm64 - chromium - arm (push) Waiting to run
Release / amd64 - none - noweb (push) Waiting to run
Release / linux/arm64 - none - noweb-arm (push) Waiting to run
fix #629
2024-11-07 11:43:42 +07:00
devlikepro eb218da8fb [core] linkPreview: false|true
fix #596 #598
2024-11-06 15:46:55 +07:00
devlikepro 65942a4ed4 [core] 2024.11.3 2024-11-06 15:46:55 +07:00
devlikepro 1444155c09 [core] Use WAL for sqlite3
fix #628
2024-11-06 15:46:55 +07:00
devlikepro 84ac1d8cfc [core] 2024.11.2
Release / amd64 - chrome - chrome (push) Waiting to run
Release / amd64 - chromium - latest (push) Waiting to run
Release / linux/arm64 - chromium - arm (push) Waiting to run
Release / amd64 - none - noweb (push) Waiting to run
Release / linux/arm64 - none - noweb-arm (push) Waiting to run
2024-11-05 18:39:40 +07:00
devlikepro 440e1e879a [core] WEBJS - fix injection 2024-11-05 18:39:40 +07:00
devlikepro bcecf793d4 [core] WEBJS - only issue WORKING on READY event 2024-11-05 18:39:40 +07:00
devlikepro 16c0d827e1 [core] Add some fonts 2024-11-05 18:39:40 +07:00
devlikepro 94f16a9583 [core] WEBJS - --disable-software-rasterizer 2024-11-05 18:39:40 +07:00
devlikepro 4bdac3bbc2 [core] use sessions folder (no dot) so it's visible via ls 2024-11-05 18:39:40 +07:00
133 changed files with 25859 additions and 1627 deletions

No files matched your search

+2
View File
@@ -1,5 +1,7 @@
Dockerfile
src/core/engines/gows/proto
.*sessions
sessions
files
venv
.wwebjs_auth
+33 -18
View File
@@ -6,10 +6,12 @@
# If you know the right URL, you can use it here.
# Use any port in WHATSAPP_API_PORT and domain in WHATSAPP_API_HOSTNAME
# https://waha.devlike.pro/docs/how-to/config/#common
WAHA_BASE_URL=http://localhost:3000
#WHATSAPP_API_SCHEMA=http
#WHATSAPP_API_PORT=3000
#WHATSAPP_API_HOSTNAME=localhost
#WAHA_BASE_URL=http://localhost:3000
#
# Set your timezone to see the right time on screenshots
# Find your timezone name in the list
@@ -90,6 +92,17 @@ WHATSAPP_SWAGGER_PASSWORD=admin
#WHATSAPP_SWAGGER_TITLE=WAHA API
#
#
# PostgresSQL Configuration
# https://waha.devlike.pro/docs/how-to/storages/#sessions---postgresql
#WHATSAPP_SESSIONS_POSTGRESQL_URL=postgres://postgres:postgres@postgres:5432/postgres?sslmode=disable
#
# Media - PostgresSQL Storage
# https://waha.devlike.pro/docs/how-to/storages/#media---postgresql
#WAHA_MEDIA_STORAGE=POSTGRESQL
#WAHA_MEDIA_POSTGRESQL_URL=postgres://postgres:postgres@postgres:5432/postgres?sslmode=disable
#
#
# Global Proxy Settings
# https://waha.devlike.pro/docs/how-to/config/#global-proxy-configuration
@@ -101,22 +114,6 @@ WHATSAPP_SWAGGER_PASSWORD=admin
#WHATSAPP_PROXY_SERVER_PASSWORD=pass
#
#
# HTTPS Configuration
# https://waha.devlike.pro/docs/how-to/config/#https
#
# Consider using certbot for HTTPS
# https://waha.devlike.pro/blog/setting-up-https-for-waha/#lets-encrypt-certbot--waha
#
#WAHA_HTTPS_ENABLED=true
#WAHA_HTTPS_PATH_KEY=/etc/letsencrypt/live/waha.example.pro/privkey.pem
#WAHA_HTTPS_PATH_CERT=/etc/letsencrypt/live/waha.example.pro/cert.pem \
#WAHA_HTTPS_PATH_CA=/etc/letsencrypt/live/waha.example.pro/chain.pem
#WHATSAPP_API_SCHEMA=https
#WHATSAPP_API_PORT=3000
#WHATSAPP_API_HOSTNAME=waha.example.pro
#
#
# Media Configuration
#
@@ -169,8 +166,26 @@ WHATSAPP_FILES_FOLDER=/app/.media
# volumes:
# - './.sessions:/app/sessions'
#
# MongoDB Configuration
# https://waha.devlike.pro/docs/how-to/storages/#sessions---mongodb
#WHATSAPP_SESSIONS_MONGO_URL=mongodb://mongodb:27017
#WHATSAPP_SESSIONS_MONGO_URL=mongodb://mongouser:mongopassword@mongodb:27017
#
#
# HTTPS Configuration
# https://waha.devlike.pro/docs/how-to/config/#https
#
# Consider using certbot for HTTPS
# https://waha.devlike.pro/blog/setting-up-https-for-waha/#lets-encrypt-certbot--waha
#
#WAHA_HTTPS_ENABLED=true
#WAHA_HTTPS_PATH_KEY=/etc/letsencrypt/live/waha.example.pro/privkey.pem
#WAHA_HTTPS_PATH_CERT=/etc/letsencrypt/live/waha.example.pro/cert.pem \
#WAHA_HTTPS_PATH_CA=/etc/letsencrypt/live/waha.example.pro/chain.pem
#WHATSAPP_API_SCHEMA=https
#WHATSAPP_API_PORT=3000
#WHATSAPP_API_HOSTNAME=waha.example.pro
#
+2
View File
@@ -0,0 +1,2 @@
src/core/engines/gows/grpc/*
src/core/engines/gows/proto/*
+28 -3
View File
@@ -8,7 +8,9 @@ on:
jobs:
docker-build:
runs-on: ${{ matrix.runner }}
name: ${{ matrix.platform }} - ${{ matrix.browser }} - ${{ matrix.tag }}
name:
${{ matrix.engine }} - ${{ matrix.browser }} - ${{ matrix.platform }} -
${{ matrix.tag }}
strategy:
matrix:
include:
@@ -17,6 +19,7 @@ jobs:
tag: 'latest'
platform: 'amd64'
browser: 'chromium'
engine: 'WEBJS'
goss: 'goss-linux-amd64'
# Chromium - ARM
@@ -24,6 +27,7 @@ jobs:
tag: 'arm'
platform: 'linux/arm64'
browser: 'chromium'
engine: 'WEBJS'
goss: 'goss-linux-arm'
# Chrome - x86
@@ -31,6 +35,7 @@ jobs:
tag: 'chrome'
platform: 'amd64'
browser: 'chrome'
engine: 'WEBJS'
goss: 'goss-linux-amd64'
# Chrome - ARM (Chrome is not available for ARM)
@@ -40,18 +45,36 @@ jobs:
# browser: "chrome"
# goss: 'goss-linux-arm'
# No browser - x86
# NOWEB - x86
- runner: 'buildjet-4vcpu-ubuntu-2204'
tag: 'noweb'
platform: 'amd64'
browser: 'none'
engine: 'NOWEB'
goss: 'goss-linux-amd64'
# No browser - ARM
# NOWEB - ARM
- runner: 'buildjet-4vcpu-ubuntu-2204-arm'
tag: 'noweb-arm'
platform: 'linux/arm64'
browser: 'none'
engine: 'NOWEB'
goss: 'goss-linux-arm'
# GOWS - x86
- runner: 'buildjet-4vcpu-ubuntu-2204'
tag: 'gows'
platform: 'amd64'
browser: 'none'
engine: 'GOWS'
goss: 'goss-linux-amd64'
# No browser - ARM
- runner: 'buildjet-4vcpu-ubuntu-2204-arm'
tag: 'gows-arm'
platform: 'linux/arm64'
browser: 'none'
engine: 'GOWS'
goss: 'goss-linux-arm'
steps:
@@ -85,6 +108,7 @@ jobs:
platforms: ${{ matrix.platform }}
build-args: |
USE_BROWSER=${{ matrix.browser }}
WHATSAPP_DEFAULT_ENGINE=${{ matrix.engine }}
push: false
load: true
tags: ${{ steps.meta.outputs.tags }}
@@ -116,6 +140,7 @@ jobs:
platforms: ${{ matrix.platform }}
build-args: |
USE_BROWSER=${{ matrix.browser }}
WHATSAPP_DEFAULT_ENGINE=${{ matrix.engine }}
push: ${{ github.ref_type == 'tag' }}
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
+2
View File
@@ -1,8 +1,10 @@
src/core/engines/gows/proto
.env
*.heapsnapshot
.yarn
example.ts
.*sessions
sessions
.secrets
.minio
.media
+4
View File
@@ -2,6 +2,8 @@ exclude: |
(?x)^(
^src/dashboard/.*|
^src/core/engines/webjs/.*html|
^src/core/engines/gows/grpc/.*|
^src/core/engines/gows/proto/.*|
^src/plus/engines/webjs/.*html|
^.yarn/.*
)$
@@ -73,4 +75,6 @@ repos:
.pre-commit-config.yaml|
^src/core/engines/webjs/session.webjs.core.ts|
^src/core/engines/webjs/_lodash.js|
^tests/perf/.*|
^scripts/.*|
)$
+76 -9
View File
@@ -6,7 +6,7 @@ FROM node:${NODE_VERSION} AS build
ENV PUPPETEER_SKIP_DOWNLOAD=True
# npm packages
WORKDIR /src
WORKDIR /git
COPY package.json .
COPY yarn.lock .
ENV YARN_CHECKSUM_BEHAVIOR=update
@@ -15,8 +15,8 @@ RUN yarn set version 3.6.3
RUN yarn install
# App
WORKDIR /src
ADD . /src
WORKDIR /git
ADD . /git
RUN yarn install
RUN yarn build && find ./dist -name "*.d.ts" -delete
@@ -25,16 +25,51 @@ RUN yarn build && find ./dist -name "*.d.ts" -delete
#
FROM node:${NODE_VERSION} AS dashboard
# Download WAHA Dashboard
ENV WAHA_DASHBOARD_SHA e6a72c5de2effc97d83e997cb1f361851604bae8
# jq to parse json
RUN apt-get update && apt-get install -y jq && rm -rf /var/lib/apt/lists/*
COPY waha.config.json /tmp/waha.config.json
RUN \
wget https://github.com/devlikeapro/dashboard/archive/${WAHA_DASHBOARD_SHA}.zip \
WAHA_DASHBOARD_GITHUB_REPO=$(jq -r '.waha.dashboard.repo' /tmp/waha.config.json) && \
WAHA_DASHBOARD_SHA=$(jq -r '.waha.dashboard.ref' /tmp/waha.config.json) && \
wget https://github.com/${WAHA_DASHBOARD_GITHUB_REPO}/archive/${WAHA_DASHBOARD_SHA}.zip \
&& unzip ${WAHA_DASHBOARD_SHA}.zip -d /tmp/dashboard \
&& mkdir -p /dashboard \
&& mv /tmp/dashboard/dashboard-${WAHA_DASHBOARD_SHA}/* /dashboard/ \
&& rm -rf ${WAHA_DASHBOARD_SHA}.zip \
&& rm -rf /tmp/dashboard/dashboard-${WAHA_DASHBOARD_SHA}
#
# GOWS
#
FROM golang:1.23-bullseye AS gows
# jq to parse json
RUN apt-get update && apt-get install -y jq && rm -rf /var/lib/apt/lists/*
# install protoc
RUN apt-get update && \
apt-get install protobuf-compiler -y
# Image processing for thumbnails
RUN apt-get update \
&& apt-get install -y libvips-dev \
&& rm -rf /var/lib/apt/lists/*
COPY waha.config.json /tmp/waha.config.json
WORKDIR /go/gows
RUN \
GOWS_GITHUB_REPO=$(jq -r '.waha.gows.repo' /tmp/waha.config.json) && \
GOWS_SHA=$(jq -r '.waha.gows.ref' /tmp/waha.config.json) && \
ARCH=$(uname -m) && \
if [ "$ARCH" = "x86_64" ]; then ARCH="amd64"; \
elif [ "$ARCH" = "aarch64" ]; then ARCH="arm64"; \
else echo "Unsupported architecture: $ARCH" && exit 1; fi && \
mkdir -p /go/gows/bin && \
wget -O /go/gows/bin/gows https://github.com/${GOWS_GITHUB_REPO}/releases/download/v.${GOWS_SHA}/gows-${ARCH} && \
chmod +x /go/gows/bin/gows
#
# Final
#
@@ -44,12 +79,18 @@ ENV PUPPETEER_SKIP_DOWNLOAD=True
# https://github.com/devlikeapro/waha/issues/347
ENV NODE_OPTIONS="--max-old-space-size=16384"
ARG USE_BROWSER=chromium
ARG WHATSAPP_DEFAULT_ENGINE
RUN echo "USE_BROWSER=$USE_BROWSER"
# Install ffmpeg to generate previews for videos
RUN apt-get update && apt-get install -y ffmpeg --no-install-recommends && rm -rf /var/lib/apt/lists/*
# Image processing for thumbnails
RUN apt-get update \
&& apt-get install -y libvips \
&& rm -rf /var/lib/apt/lists/*
# Install zip and unzip - either for chromium or chrome
RUN if [ "$USE_BROWSER" = "chromium" ] || [ "$USE_BROWSER" = "chrome" ]; then \
apt-get update \
@@ -60,7 +101,20 @@ RUN if [ "$USE_BROWSER" = "chromium" ] || [ "$USE_BROWSER" = "chrome" ]; then \
# Install fonts if using either chromium or chrome
RUN if [ "$USE_BROWSER" = "chromium" ] || [ "$USE_BROWSER" = "chrome" ]; then \
apt-get update \
&& apt-get install -y fonts-ipafont-gothic fonts-wqy-zenhei fonts-thai-tlwg fonts-kacst fonts-freefont-ttf \
&& apt-get install -y \
fontconfig \
fonts-freefont-ttf \
fonts-gfs-neohellenic \
fonts-indic \
fonts-ipafont-gothic \
fonts-kacst \
fonts-liberation \
fonts-noto-cjk \
fonts-noto-color-emoji \
fonts-roboto \
fonts-thai-tlwg \
fonts-wqy-zenhei \
fonts-open-sans \
--no-install-recommends \
&& rm -rf /var/lib/apt/lists/*; \
fi
@@ -86,12 +140,25 @@ RUN if [ "$USE_BROWSER" = "chrome" ]; then \
&& rm -rf /var/lib/apt/lists/*; \
fi
# GOWS requirements
# libc6
RUN apt-get update \
&& apt-get install -y libc6 \
&& rm -rf /var/lib/apt/lists/*
# Set the ENV for docker image
ENV WHATSAPP_DEFAULT_ENGINE=$WHATSAPP_DEFAULT_ENGINE
# Attach sources, install packages
WORKDIR /app
COPY package.json ./
COPY --from=build /src/node_modules ./node_modules
COPY --from=build /src/dist ./dist
COPY --from=build /git/node_modules ./node_modules
COPY --from=build /git/dist ./dist
COPY --from=dashboard /dashboard ./dist/dashboard
COPY --from=gows /go/gows/bin/gows /app/gows
ENV WAHA_GOWS_PATH /app/gows
ENV WAHA_GOWS_SOCKET /tmp/gows.sock
COPY entrypoint.sh /entrypoint.sh
# Chokidar options to monitor file changes
+21 -2
View File
@@ -5,7 +5,10 @@ build-chrome:
docker build . -t devlikeapro/waha-plus:chrome --build-arg USE_BROWSER=chrome
build-noweb:
docker build . -t devlikeapro/waha:noweb --build-arg USE_BROWSER=none
docker build . -t devlikeapro/waha:noweb --build-arg USE_BROWSER=none --build-arg WHATSAPP_DEFAULT_ENGINE=NOWEB
build-plus-gows:
docker build . -t devlikeapro/waha-plus:gows --build-arg USE_BROWSER=none --build-arg WHATSAPP_DEFAULT_ENGINE=GOWS
build-all: build build-chrome build-noweb
@@ -32,10 +35,26 @@ for-swagger:
WHATSAPP_SWAGGER_CONFIG_ADVANCED=true . ${NVM_DIR}/nvm.sh && nvm exec yarn start
up-noweb:
. ${NVM_DIR}/nvm.sh && nvm exec yarn up @adiwajshing/baileys@github:devlikeapro/Baileys#fork-master-channels-678
. ${NVM_DIR}/nvm.sh && nvm exec yarn up @adiwajshing/baileys@github:devlikeapro/Baileys#fork-master-channels-6712
up-noweb-libsignal:
. ${NVM_DIR}/nvm.sh && nvm exec yarn up libsignal@github:devlikeapro/libsignal-node#fork-master
up-webjs:
. ${NVM_DIR}/nvm.sh && nvm exec yarn up whatsapp-web.js@github:devlikeapro/whatsapp-web.js#fork-main-channels
start-proxy:
docker run --rm -d --name squid-container -e TZ=UTC -p 3128:3128 ubuntu/squid:5.2-22.04_beta
proto-gows:
node scripts/gows-proto.js build --dir ../gows/proto
gows:
cd ../gows && \
export PATH=${HOME}/go/bin:${PATH} && \
make all
copy-dashboard:
cd ../waha-hub/ui && \
make copy-waha
+21 -59
View File
@@ -6,22 +6,16 @@
**WAHA** - **W**hats**A**pp **H**TTP **A**PI (REST API) that you can install on your own server and run in less than 5 minutes!
[![Docker Pulls](https://img.shields.io/docker/pulls/devlikeapro/whatsapp-http-api)](https://hub.docker.com/r/devlikeapro/waha)
[![Docker Pulls](https://img.shields.io/docker/pulls/devlikeapro/waha)](https://hub.docker.com/r/devlikeapro/waha)
- Documentation: [https://waha.devlike.pro/](https://waha.devlike.pro/)
- Dashboard Example: [https://waha.devlike.pro/dashboard](https://waha.devlike.pro/dashboard)
- Swagger Example: [https://waha.devlike.pro/swagger](https://waha.devlike.pro/swagger)
# Tables of Contents
<!-- toc -->
- [Features](#features)
* [Messages](#messages)
* [Sessions](#sessions)
* [Security](#security)
* [Updates](#updates)
* [Groups](#groups)
* [Contacts](#contacts)
- [Quick start](#quick-start)
* [Requirements](#requirements)
* [Send your first message](#send-your-first-message)
@@ -32,52 +26,11 @@
+ [5. Get the screenshot](#5-get-the-screenshot)
+ [6. Send a text message](#6-send-a-text-message)
* [What is next?](#what-is-next)
- [Development](#development)
* [Start the project](#start-the-project)
<!-- tocstop -->
# Features
## Messages
- Send messages
- Receive messages
- Message replies
- Send location
- Receive location
- React to messages
- Send media (images/documents/files)
- Send voice messages
- Receive media (images/audio/video/documents)
- Receive messages - webhook retries
## Sessions
- Multi Device
- Get the screenshot
- Session saving (don't have to scan QR on every restart)
- Single WhatsApp account running inside one container
- Multiple WhatsApp account running inside one container
## Security
- Swagger panel authentication
- API authentication
## Updates
- Bug fixes and updates come as soon as they are implemented.
## Groups
- Create a group
- Get invite for group
- Modify group info (subject, description)
- Add group participants
- Kick group participants
- Promote/demote group participants
## Contacts
- Mute/unmute chats
- Block/unblock contacts
- Get contact info
- Get profile pictures
- Get presence (online\offline\typing) status
# Quick start
## Requirements
@@ -87,13 +40,6 @@ instruction <a href="https://docs.docker.com/get-docker/" target="_blank" rel="n
When you are ready - come back and follows the below steps to send the first text message to WhatsApp via HTTP API!
{{< details "Why Docker?" >}}
Docker makes it easy to ship all-in-one solution with the runtime and dependencies. You don't have to worry about
language-specific libraries or chrome installation.
Also Docker makes installation and update processes so simple, just one command!
{{< /details >}}
## Send your first message
Let's go over steps that allow you to send your first text message via WhatsApp HTTP API!
@@ -114,7 +60,7 @@ docker pull devlikeapro/waha-plus
docker logout
```
Read more about how to get `PASSWORD` for [Plus Version →]({{< relref "plus-version" >}})
Read more about how to get `PASSWORD` for [**➕ WAHA Plus**](https://waha.devlike.pro/docs/how-to/waha-plus/)
### 2. Run WhatsApp HTTP API
@@ -195,3 +141,19 @@ curl -d "{\"chatId\": \"${PHONE}@c.us\", \"text\": \"Hello from WhatsApp HTTP AP
## What is next?
[Go and read the full documentation!](https://waha.devlike.pro/docs/overview/introduction/)
# Development
## Start the project
1. Clone the repository
2. Make sure you're using node>=22
3. Run the following commands:
```bash
# Install dependencies
yarn install
# Fetch and compile proto files
yarn gows:proto
# Run
yarn start
# open http://localhost:3000
```
+28 -8
View File
@@ -4,7 +4,7 @@ services:
restart: always
# https://waha.devlike.pro/docs/how-to/engines/#docker-images
# https://portal.devlike.pro/docker-image
image: devlikeapro/waha-plus:latest
image: devlikeapro/waha-plus
# WAHA Core
# image: devlikeapro/waha:latest
@@ -13,7 +13,7 @@ services:
volumes:
# Store sessions in the .sessions folder (comment it if you're using MongoDB)
- './.sessions:/app/.sessions'
- './sessions:/app/.sessions'
# Save media files
# https://waha.devlike.pro/docs/how-to/storages/#save-media-files-between-the-container-restarts
@@ -22,9 +22,27 @@ services:
env_file:
- .env
# NOTE: Only if you're using MongoDB
# https://waha.devlike.pro/docs/how-to/storages/#sessions---mongodb
# Comment this block if you're using MongoDB
# NOTE: Only if you're using PostgreSQL to save sessions
# https://waha.devlike.pro/docs/how-to/storages/#media---postgresql
# postgres:
# image: postgres:17
# restart: always
# environment:
# POSTGRES_USER: postgres
# POSTGRES_PASSWORD: postgres
# POSTGRES_DB: postgres
# ports:
# - "5432:5432"
# volumes:
# - pg_data:/var/lib/postgresql/data
# command:
# - postgres
# - "-c"
# - "max_connections=3000"
# NOTE: Only if you're using MongoDB
# https://waha.devlike.pro/docs/how-to/storages/#sessions---mongodb
# Uncomment this block if you're using MongoDB
# mongodb:
# image: mongo
# container_name: mongodb
@@ -32,11 +50,13 @@ services:
# - '127.0.0.1:27017:27017/tcp'
# volumes:
# - mongodb_data:/data/db
# restart: always
# environment:
# - MONGO_INITDB_ROOT_USERNAME=mongouser
# - MONGO_INITDB_ROOT_PASSWORD=mongopassword
# NOTE: Only if you're using S3 to save media files
# https://waha.devlike.pro/docs/how-to/storages/#media---s3
# Comment this block if you're using AWS S3
# Uncomment this block if you're using AWS S3
# minio:
# image: quay.io/minio/minio
# container_name: minio
@@ -53,6 +73,6 @@ services:
# command: server /data --console-address ":9001"
volumes:
# NOTE: Only if you're using MongoDB
mongodb_data: {}
minio_data: {}
pg_data: {}
+1 -1
View File
@@ -2,7 +2,7 @@
# Calculate UV_THREADPOOL_SIZE based on number of CPUs
cpus=$(node -e "const os = require('os'); console.log(os.cpus().length);")
uv_threadpool_size=$(($cpus * 2))
uv_threadpool_size=$(($cpus * 1))
# Set UV_THREADPOOL_SIZE as an environment variable
export UV_THREADPOOL_SIZE="${UV_THREADPOOL_SIZE:-$uv_threadpool_size}"
+20 -6
View File
@@ -20,10 +20,13 @@
"test:watch": "jest --watch",
"test:cov": "jest --coverage",
"test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand",
"test:e2e": "jest --config ./test/jest-e2e.json"
"test:e2e": "jest --config ./test/jest-e2e.json",
"gows:proto:fetch": "node scripts/gows-proto.js fetch",
"gows:proto:build": "node scripts/gows-proto.js build",
"gows:proto": "yarn gows:proto:fetch && yarn gows:proto:build"
},
"dependencies": {
"@adiwajshing/baileys": "github:devlikeapro/Baileys#fork-master-channels-678",
"@adiwajshing/baileys": "github:devlikeapro/Baileys#fork-master-channels-6712",
"@adiwajshing/keyed-db": "^0.2.4",
"@aws-sdk/client-s3": "^3.633.0",
"@aws-sdk/s3-request-presigner": "^3.633.0",
@@ -55,10 +58,12 @@
"express-basic-auth": "^1.2.1",
"file-type": "16.5.4",
"fs-extra": "^11.2.0",
"google-protobuf": "^3.21.4",
"https-proxy-agent": "^7.0.0",
"joi": "^17.13.3",
"knex": "^3.1.0",
"libphonenumber-js": "^1.10.36",
"libsignal": "github:devlikeapro/libsignal-node#fork-master",
"link-preview-js": "^3.0.4",
"lodash": "^4.17.21",
"mime-types": "^2.1.27",
@@ -66,6 +71,7 @@
"nestjs-pino": "^4.1.0",
"passport": "^0.6.0",
"passport-headerapikey": "^1.2.2",
"pg": "^8.13.1",
"pino-http": "^10.2.0",
"pino-pretty": "^11.2.1",
"pretty-bytes": "5.6.0",
@@ -75,10 +81,12 @@
"qrcode-terminal": "^0.12.0",
"reflect-metadata": "^0.1.13",
"rimraf": "^3.0.2",
"rxjs": "^7.1.0",
"rxjs": "^7.8.1",
"sharp": "^0.33.4",
"swagger-ui-express": "^4.1.4",
"whatsapp-web.js": "github:devlikeapro/whatsapp-web.js#fork-main-channels"
"ulid": "^2.3.0",
"whatsapp-web.js": "github:devlikeapro/whatsapp-web.js#fork-main-channels",
"write-file-atomic": "^6.0.0"
},
"optionalDependencies": {
"bufferutil": "^4.0.8"
@@ -87,9 +95,12 @@
"bufferutil": "^4.0.8",
"ws": "^8.18.0",
"puppeteer": "^23.6.0",
"whatwg-url": "13.0.0"
"whatwg-url": "13.0.0",
"libsignal": "github:devlikeapro/libsignal-node#fork-master"
},
"devDependencies": {
"@grpc/grpc-js": "^1.12.4",
"@grpc/proto-loader": "^0.7.13",
"@nestjs/cli": "^9.0.0",
"@nestjs/schematics": "^9.0.1",
"@nestjs/testing": "^9.0.9",
@@ -103,14 +114,17 @@
"eslint-config-prettier": "^6.10.0",
"eslint-plugin-import": "^2.20.1",
"eslint-plugin-simple-import-sort": "^10.0.0",
"grpc-tools": "^1.12.4",
"jest": "^29.7.0",
"prettier": "^1.19.1",
"protoc-gen-ts": "^0.8.7",
"supertest": "^4.0.2",
"ts-jest": "^29.1.3",
"ts-loader": "^6.2.1",
"ts-node": "^10.9.1",
"tsconfig-paths": "^4.1.0",
"typescript": "^4.8.4"
"typescript": "^4.8.4",
"yargs": "^17.7.2"
},
"jest": {
"moduleFileExtensions": [
+144
View File
@@ -0,0 +1,144 @@
// eslint-disable-next-line @typescript-eslint/no-var-requires
const fs = require('fs');
// eslint-disable-next-line @typescript-eslint/no-var-requires
const path = require('path');
// eslint-disable-next-line @typescript-eslint/no-var-requires
const axios = require('axios');
// eslint-disable-next-line @typescript-eslint/no-var-requires
const { execSync } = require('child_process');
// eslint-disable-next-line @typescript-eslint/no-var-requires
const yargs = require('yargs');
// Defaults
const CONFIG_FILE = 'waha.config.json';
// Load defaults from package.json
gows = (() => {
try {
const config = JSON.parse(fs.readFileSync(CONFIG_FILE, 'utf8'));
return config.waha.gows || {};
} catch (error) {
return {};
}
})();
const DEFAULT_REPO = gows.repo;
if (!DEFAULT_REPO) {
throw new Error(`Missing default repo in ${CONFIG_FILE}`);
}
const DEFAULT_REF = gows.ref;
if (!DEFAULT_REF) {
throw new Error('Missing default ref in ${CONFIG_FILE}');
}
const DEFAULT_DIR = './src/core/engines/gows/proto';
const PROTO_FILES = ['gows.proto'];
const PROTO_OUTPUT = './src/core/engines/gows/grpc';
// Helper function to clean directory
function cleanDirectory(directory, suffix) {
if (!fs.existsSync(directory)) {
fs.mkdirSync(directory, { recursive: true });
return;
}
const files = fs.readdirSync(directory);
for (const file of files) {
const filePath = path.join(directory, file);
if (file.endsWith(suffix)) {
fs.unlinkSync(filePath);
}
}
}
// Helper function to download files
async function downloadFiles(repo, ref, directory) {
for (const file of PROTO_FILES) {
const url = `https://github.com/${repo}/releases/download/v.${ref}/${file}`;
const filePath = path.join(directory, file);
try {
const response = await axios.get(url, { responseType: 'arraybuffer' });
fs.writeFileSync(filePath, response.data);
console.log(`Downloaded: ${file}`);
} catch (error) {
console.error(`Failed to download ${file}: ${error.message}`);
}
}
}
// Handler for fetch command
async function handleFetch(repo, ref, dir) {
console.log(`Fetching .proto files from ${repo}@${ref} to ${dir}...`);
cleanDirectory(dir, '.proto');
await downloadFiles(repo, ref, dir);
}
// Handler for build command
function handleBuild(dir) {
console.log('Building gRPC files...');
cleanDirectory(PROTO_OUTPUT);
const command = `grpc_tools_node_protoc \
--plugin=protoc-gen-ts=node_modules/.bin/protoc-gen-ts \
--plugin=protoc-gen-grpc=node_modules/.bin/grpc_tools_node_protoc_plugin \
--js_out=import_style=commonjs,binary:${PROTO_OUTPUT} \
--grpc_out=grpc_js:${PROTO_OUTPUT} \
--ts_out=grpc_js:${PROTO_OUTPUT} \
-I ${dir} ${dir}/gows.proto`;
try {
execSync(command, { stdio: 'inherit' });
console.log('gRPC files built successfully.');
} catch (error) {
console.error(`Failed to build gRPC files: ${error.message}`);
}
}
//
// Commands
//
// fetch
yargs.command(
'fetch',
'Fetch .proto files from GitHub',
(yargs) => {
yargs
.option('repo', {
describe: 'GitHub repository (owner/repo)',
type: 'string',
default: DEFAULT_REPO,
})
.option('ref', {
describe: 'Git reference (branch or commit SHA)',
type: 'string',
default: DEFAULT_REF,
})
.option('dir', {
describe: 'Directory to output .proto files',
type: 'string',
default: DEFAULT_DIR,
});
},
async (argv) => {
await handleFetch(argv.repo, argv.ref, argv.dir);
},
);
// build
yargs.command(
'build',
'Build gRPC files from .proto files',
(yargs) => {
yargs.option('dir', {
describe: 'Directory containing .proto files',
type: 'string',
default: DEFAULT_DIR,
});
},
(argv) => {
handleBuild(argv.dir);
},
);
// Parse arguments
yargs.parse();
-22
View File
@@ -1,22 +0,0 @@
#!/bin/bash
set -e
echo "Starting WAHA..."
docker run -d \
--restart=always \
--name waha \
-p 3000:3000/tcp \
-v ./.sessions:/app/.sessions \
-v ./.media:/app/.media \
--env WHATSAPP_API_KEY=321 \
--env WAHA_DASHBOARD_USERNAME=waha \
--env WAHA_DASHBOARD_PASSWORD=waha \
--env WAHA_LOG_FORMAT=JSON \
--env WAHA_LOG_LEVEL=info \
--env WHATSAPP_DEFAULT_ENGINE=WEBJS \
--env WAHA_PRINT_QR=False \
--env WHATSAPP_FILES_LIFETIME=0 \
--env WHATSAPP_FILES_FOLDER=/app/.media \
devlikeapro/waha-plus:latest
echo "WAHA started."
-21
View File
@@ -1,21 +0,0 @@
set -e
echo "Updating WAHA..."
echo "Logging in to Docker..."
docker login -u devlikeapro # -p {KEY} - you can hard code key here https://portal.devlike.pro/
echo "Pulling the latest image..."
docker pull devlikeapro/waha-plus
echo "Logging out of Docker..."
docker logout
# Just pull if you're using WAHA Core
# docker pull devlikeapro/waha
echo "Stopping the current container..."
docker stop waha || true
echo "Removing the current container..."
docker rm waha || true
echo "Running WAHA..."
sh ./waha-run.sh
-39
View File
@@ -6,7 +6,6 @@ import {
Query,
UseInterceptors,
} from '@nestjs/common';
import { UnprocessableEntityException } from '@nestjs/common/exceptions/unprocessable-entity.exception';
import { ApiOperation, ApiSecurity, ApiTags } from '@nestjs/swagger';
import { ApiFileAcceptHeader } from '@waha/nestjs/ApiFileAcceptHeader';
import {
@@ -19,8 +18,6 @@ import { SessionManager } from '../core/abc/manager.abc';
import { WhatsappSession } from '../core/abc/session.abc';
import { BufferResponseInterceptor } from '../nestjs/BufferResponseInterceptor';
import {
CaptchaBody,
OTPRequest,
QRCodeFormat,
QRCodeQuery,
QRCodeValue,
@@ -63,42 +60,6 @@ class AuthController {
) {
return session.requestCode(request.phoneNumber, request.method, request);
}
@Post('authorize-code')
@SessionApiParam
@ApiOperation({
summary: 'Send OTP authentication code.',
})
authorizeCode(
@SessionParam session: WhatsappSession,
@Body() request: OTPRequest,
) {
return session.authorizeCode(request.code);
}
@Get('captcha')
@SessionApiParam
@ApiFileAcceptHeader()
@UseInterceptors(new BufferResponseInterceptor())
@ApiOperation({
summary: 'Get captcha image.',
})
async getCaptcha(@SessionParam session: WhatsappSession): Promise<Buffer> {
const image = await session.getCaptcha();
return image.get();
}
@Post('captcha')
@SessionApiParam
@ApiOperation({
summary: 'Enter captcha code.',
})
saveCaptcha(
@SessionParam session: WhatsappSession,
@Body() body: CaptchaBody,
) {
return session.saveCaptcha(body.code);
}
}
export { AuthController };
+102 -5
View File
@@ -3,31 +3,129 @@ import {
Controller,
Delete,
Get,
HttpCode,
HttpStatus,
Param,
Post,
Query,
UsePipes,
} from '@nestjs/common';
import { ApiOperation, ApiSecurity, ApiTags } from '@nestjs/swagger';
import { ApiOperation, ApiParam, ApiSecurity, ApiTags } from '@nestjs/swagger';
import { ChannelsInfoServiceCore } from '@waha/core/services/ChannelsInfoServiceCore';
import {
SessionApiParam,
WorkingSessionParam,
} from '@waha/nestjs/params/SessionApiParam';
import { WAHAValidationPipe } from '@waha/nestjs/pipes/WAHAValidationPipe';
import {
Channel,
ChannelCategory,
ChannelCountry,
ChannelListResult,
ChannelMessage,
ChannelSearchByText,
ChannelSearchByView,
ChannelView,
CreateChannelRequest,
ListChannelsQuery,
NewsletterIdApiParam,
NewsletterIdOrInviteCodeApiParam,
PreviewChannelMessages,
} from '@waha/structures/channels.dto';
import { SessionManager } from '../core/abc/manager.abc';
import { isNewsletter, WhatsappSession } from '../core/abc/session.abc';
import {
isNewsletter,
parseChannelInviteLink,
WhatsappSession,
} from '../core/abc/session.abc';
@ApiSecurity('api_key')
@Controller('api/:session/channels')
@ApiTags('📢 Channels')
export class ChannelsController {
constructor(private manager: SessionManager) {}
constructor(
private manager: SessionManager,
private channelsInfoService: ChannelsInfoServiceCore,
) {}
@Post('/search/by-view')
@HttpCode(HttpStatus.OK)
@SessionApiParam
@UsePipes(new WAHAValidationPipe())
@ApiOperation({ summary: 'Search for channels (by view)' })
async searchByView(
@WorkingSessionParam session: WhatsappSession,
@Body() request: ChannelSearchByView,
): Promise<ChannelListResult> {
return session.searchChannelsByView(request);
}
@Post('/search/by-text')
@HttpCode(HttpStatus.OK)
@SessionApiParam
@UsePipes(new WAHAValidationPipe())
@ApiOperation({ summary: 'Search for channels (by text)' })
async searchByText(
@WorkingSessionParam session: WhatsappSession,
@Body() request: ChannelSearchByText,
): Promise<ChannelListResult> {
return session.searchChannelsByText(request);
}
@Get('/search/views')
@SessionApiParam
@ApiOperation({ summary: 'Get list of views for channel search' })
getSearchViews(): Promise<ChannelView[]> {
return this.channelsInfoService.getViews();
}
@Get('/search/countries')
@SessionApiParam
@ApiOperation({ summary: 'Get list of countries for channel search' })
getSearchCountries(): Promise<ChannelCountry[]> {
return this.channelsInfoService.getCountries();
}
@Get('/search/categories')
@SessionApiParam
@ApiOperation({ summary: 'Get list of categories for channel search' })
getSearchCategories(): Promise<ChannelCategory[]> {
return this.channelsInfoService.getCategories();
}
@Get(':id/messages/preview')
@SessionApiParam
@UsePipes(new WAHAValidationPipe())
@ApiParam({
name: 'id',
description: 'Channel id or invite code',
required: true,
type: 'string',
schema: {
default: '0029Va4K0PZ5a245NkngBA2M',
},
})
@ApiOperation({
summary: 'Preview channel messages',
description:
'You can use either ' +
'invite code (https://www.whatsapp.com/channel/123) or (123)' +
'OR' +
'Channel ID (123@newsletter).',
})
async previewChannelMessages(
@WorkingSessionParam session: WhatsappSession,
@Param('id') code: string,
@Query() query: PreviewChannelMessages,
): Promise<ChannelMessage[]> {
if (isNewsletter(code)) {
const channel = await session.channelsGetChannel(code);
code = parseChannelInviteLink(channel.invite);
}
const inviteCode = parseChannelInviteLink(code);
return session.previewChannelMessages(inviteCode, query);
}
@Get('')
@SessionApiParam
@@ -76,8 +174,7 @@ export class ChannelsController {
if (isNewsletter(id)) {
return session.channelsGetChannel(id);
} else {
// https://www.whatsapp.com/channel/123 => 123
const inviteCode = id.split('/').pop();
const inviteCode = parseChannelInviteLink(id);
return session.channelsGetChannelByInviteCode(inviteCode);
}
}
+59 -1
View File
@@ -21,12 +21,16 @@ import {
import { SessionManager } from '../core/abc/manager.abc';
import { WhatsappSession } from '../core/abc/session.abc';
import { parseBool } from '../helpers';
import {
ChatPictureQuery,
ChatPictureResponse,
ChatsPaginationParams,
ChatSummary,
GetChatMessageQuery,
GetChatMessagesFilter,
GetChatMessagesQuery,
OverviewPaginationParams,
PinMessageRequest,
} from '../structures/chats.dto';
import { EditMessageRequest } from '../structures/chatting.dto';
@@ -47,6 +51,20 @@ class ChatsController {
return session.getChats(pagination);
}
@Get('overview')
@SessionApiParam
@ApiOperation({
summary:
'Get chats overview. Includes all necessary things to build UI "your chats overview" page - chat id, name, picture, last message. Sorting by last message timestamp',
})
@UsePipes(new ValidationPipe({ transform: true, whitelist: true }))
getChatsOverview(
@WorkingSessionParam session: WhatsappSession,
@Query() pagination: OverviewPaginationParams,
): Promise<ChatSummary[]> {
return session.getChatsOverview(pagination);
}
@Delete(':chatId')
@SessionApiParam
@ApiOperation({ summary: 'Deletes the chat' })
@@ -58,6 +76,19 @@ class ChatsController {
return session.deleteChat(chatId);
}
@Get(':chatId/picture')
@SessionApiParam
@ApiOperation({ summary: 'Gets chat picture' })
@UsePipes(new ValidationPipe({ transform: true, whitelist: true }))
async getChatPicture(
@WorkingSessionParam session: WhatsappSession,
@Param('chatId') chatId: string,
@Query() query: ChatPictureQuery,
): Promise<ChatPictureResponse> {
const url = await session.getContactProfilePicture(chatId, query.refresh);
return { url: url };
}
@Get(':chatId/messages')
@SessionApiParam
@ApiOperation({ summary: 'Gets messages in the chat' })
@@ -89,6 +120,33 @@ class ChatsController {
return message;
}
@Post(':chatId/messages/:messageId/pin')
@SessionApiParam
@ApiOperation({ summary: 'Pins a message in the chat' })
@ChatIdApiParam
async pinMessage(
@WorkingSessionParam session: WhatsappSession,
@Param('chatId') chatId: string,
@Param('messageId') messageId: string,
@Body() body: PinMessageRequest,
) {
const result = await session.pinMessage(chatId, messageId, body.duration);
return { success: result };
}
@Post(':chatId/messages/:messageId/unpin')
@SessionApiParam
@ApiOperation({ summary: 'Unpins a message in the chat' })
@ChatIdApiParam
async unpinMessage(
@WorkingSessionParam session: WhatsappSession,
@Param('chatId') chatId: string,
@Param('messageId') messageId: string,
) {
const result = await session.unpinMessage(chatId, messageId);
return { success: result };
}
@Delete(':chatId/messages')
@SessionApiParam
@ApiOperation({ summary: 'Clears all messages from the chat' })
+17 -13
View File
@@ -171,15 +171,10 @@ export class ChattingController {
return whatsapp.sendLinkPreview(request);
}
@Get('/messages')
@ApiOperation({ summary: 'Get messages in a chat' })
@UsePipes(new ValidationPipe({ transform: true, whitelist: true }))
async getMessages(
@Query() query: GetMessageQuery,
@Query() filter: GetChatMessagesFilter,
) {
const whatsapp = await this.manager.getWorkingSession(query.session);
return whatsapp.getChatMessages(query.chatId, query, filter);
@Post('/sendContactVcard')
async sendContactVcard(@Body() request: MessageContactVcardRequest) {
const whatsapp = await this.manager.getWorkingSession(request.session);
return whatsapp.sendContactVCard(request);
}
@Get('/sendText')
@@ -192,10 +187,19 @@ export class ChattingController {
return whatsapp.sendText(msg);
}
@Post('/sendContactVcard')
async sendContactVcard(@Body() request: MessageContactVcardRequest) {
const whatsapp = await this.manager.getWorkingSession(request.session);
return whatsapp.sendContactVCard(request);
@Get('/messages')
@ApiOperation({
summary: 'Get messages in a chat',
description: 'DEPRECATED. Use "GET /api/chats/{id}/messages" instead',
deprecated: true,
})
@UsePipes(new ValidationPipe({ transform: true, whitelist: true }))
async getMessages(
@Query() query: GetMessageQuery,
@Query() filter: GetChatMessagesFilter,
) {
const whatsapp = await this.manager.getWorkingSession(query.session);
return whatsapp.getChatMessages(query.chatId, query, filter);
}
@Get('/checkNumberStatus')
+8 -2
View File
@@ -16,6 +16,7 @@ import {
WANumberExistResult,
} from '../structures/chatting.dto';
import {
ContactProfilePictureQuery,
ContactQuery,
ContactRequest,
ContactsPaginationParams,
@@ -75,9 +76,14 @@ export class ContactsController {
description:
'If privacy settings do not allow to get the picture, the method will return null.',
})
async getProfilePicture(@Query() query: ContactQuery) {
@UsePipes(new ValidationPipe({ transform: true, whitelist: true }))
async getProfilePicture(@Query() query: ContactProfilePictureQuery) {
const whatsapp = await this.manager.getWorkingSession(query.session);
return whatsapp.getContactProfilePicture(query);
const url = await whatsapp.getContactProfilePicture(
query.contactId,
query.refresh,
);
return { profilePictureURL: url };
}
@Post('/block')
+45 -2
View File
@@ -6,6 +6,9 @@ import {
Param,
Post,
Put,
Query,
UsePipes,
ValidationPipe,
} from '@nestjs/common';
import { ApiOperation, ApiSecurity, ApiTags } from '@nestjs/swagger';
import { GroupIdApiParam } from '@waha/nestjs/params/ChatIdApiParam';
@@ -19,6 +22,9 @@ import { WhatsappSession } from '../core/abc/session.abc';
import {
CreateGroupRequest,
DescriptionRequest,
GroupsPaginationParams,
JoinGroupRequest,
JoinGroupResponse,
ParticipantsRequest,
SettingsSecurityChangeInfo,
SubjectRequest,
@@ -40,11 +46,48 @@ export class GroupsController {
return session.createGroup(request);
}
@Get('join-info')
@SessionApiParam
@ApiOperation({ summary: 'Get info about the group before joining.' })
async joinInfoGroup(
@WorkingSessionParam session: WhatsappSession,
@Query() query: JoinGroupRequest,
): Promise<any> {
// https://chat.whatsapp.com/123 => 123
const code = query.code.split('/').pop();
return session.joinInfoGroup(code);
}
@Post('join')
@SessionApiParam
@ApiOperation({ summary: 'Join group via code' })
async joinGroup(
@WorkingSessionParam session: WhatsappSession,
@Body() request: JoinGroupRequest,
): Promise<JoinGroupResponse> {
// https://chat.whatsapp.com/123 => 123
const code = request.code.split('/').pop();
const id = await session.joinGroup(code);
return { id: id };
}
@Get('')
@SessionApiParam
@ApiOperation({ summary: 'Get all groups.' })
getGroups(@WorkingSessionParam session: WhatsappSession) {
return session.getGroups();
@UsePipes(new ValidationPipe({ transform: true, whitelist: true }))
getGroups(
@WorkingSessionParam session: WhatsappSession,
@Query() pagination: GroupsPaginationParams,
) {
return session.getGroups(pagination);
}
@Post('refresh')
@SessionApiParam
@ApiOperation({ summary: 'Refresh groups from the server.' })
@UsePipes(new ValidationPipe({ transform: true, whitelist: true }))
async refreshGroups(@WorkingSessionParam session: WhatsappSession) {
return { success: await session.refreshGroups() };
}
@Get(':id')
+94
View File
@@ -0,0 +1,94 @@
import {
Body,
Controller,
Delete,
Get,
Put,
UnprocessableEntityException,
UsePipes,
} from '@nestjs/common';
import { ApiOperation, ApiSecurity, ApiTags } from '@nestjs/swagger';
import { SessionManager } from '@waha/core/abc/manager.abc';
import { WhatsappSession } from '@waha/core/abc/session.abc';
import {
SessionApiParam,
WorkingSessionParam,
} from '@waha/nestjs/params/SessionApiParam';
import { WAHAValidationPipe } from '@waha/nestjs/pipes/WAHAValidationPipe';
import { Result } from '@waha/structures/base.dto';
import {
MyProfile,
ProfileNameRequest,
ProfilePictureRequest,
ProfileStatusRequest,
} from '@waha/structures/profile.dto';
@ApiSecurity('api_key')
@Controller('api/:session/profile')
@ApiTags('🆔 Profile')
export class ProfileController {
constructor(private manager: SessionManager) {}
@Get('')
@SessionApiParam
@ApiOperation({ summary: 'Get my profile' })
async getMyProfile(
@WorkingSessionParam session: WhatsappSession,
): Promise<MyProfile> {
const me = session.getSessionMeInfo();
if (!me) {
throw new UnprocessableEntityException('No profile found');
}
const picture = await session.getContactProfilePicture(me.id, false);
return {
id: me.id,
name: me.pushName,
picture: picture || null,
};
}
@Put('/name')
@SessionApiParam
@UsePipes(new WAHAValidationPipe())
@ApiOperation({ summary: 'Set my profile name' })
async setProfileName(
@WorkingSessionParam session: WhatsappSession,
@Body() request: ProfileNameRequest,
): Promise<Result> {
const success = await session.setProfileName(request.name);
return { success: success ?? true };
}
@Put('/status')
@SessionApiParam
@UsePipes(new WAHAValidationPipe())
@ApiOperation({ summary: 'Set profile status (About)' })
async setProfileStatus(
@WorkingSessionParam session: WhatsappSession,
@Body() request: ProfileStatusRequest,
): Promise<Result> {
const success = await session.setProfileStatus(request.status);
return { success: success ?? true };
}
@Put('/picture')
@SessionApiParam
@ApiOperation({ summary: 'Set profile picture' })
async setProfilePicture(
@WorkingSessionParam session: WhatsappSession,
@Body() request: ProfilePictureRequest,
): Promise<Result> {
const success = await session.updateProfilePicture(request.file);
return { success: success ?? true };
}
@Delete('/picture')
@SessionApiParam
@ApiOperation({ summary: 'Delete profile picture' })
async deleteProfilePicture(
@WorkingSessionParam session: WhatsappSession,
): Promise<Result> {
const success = await session.updateProfilePicture(null);
return { success: success || true };
}
}
+7
View File
@@ -134,6 +134,13 @@ export class WhatsappConfigService {
return this.configService.get('WHATSAPP_SESSIONS_MONGO_URL', undefined);
}
getSessionPostgresUrl(): string | undefined {
return this.configService.get(
'WHATSAPP_SESSIONS_POSTGRESQL_URL',
undefined,
);
}
get(name: string, defaultValue: any = undefined): any {
return this.configService.get(name, defaultValue);
}
+8 -5
View File
@@ -1,13 +1,16 @@
// eslint-disable-next-line @typescript-eslint/no-var-requires
const QRCode = require('qrcode');
export class QR {
private base64: string;
public raw?: string;
save(base64: string, raw?: string) {
this.base64 = base64.replace(/^data:image\/png;base64,/, '');
save(raw?: string) {
this.raw = raw;
}
get(): Buffer {
return Buffer.from(this.base64, 'base64');
async get(): Promise<Buffer> {
const url = await QRCode.toDataURL(this.raw);
const base64 = url.replace(/^data:image\/png;base64,/, '');
return Buffer.from(base64, 'base64');
}
}
+2 -4
View File
@@ -57,13 +57,11 @@ export class SwaggerConfiguratorCore {
.setVersion(VERSION.version)
.addTag('🖥️ Sessions', 'Control WhatsApp sessions (accounts)')
.addTag('🔑 Auth', 'Authentication')
.addTag('🆔 Profile', 'Your profile information')
.addTag('🖼️ Screenshot', 'Get screenshot of WhatsApp and show QR code')
.addTag('📤 Chatting', 'Chatting methods')
.addTag('📢 Channels', 'Channels (newsletters) methods')
.addTag(
'🟢 Status',
'Status (aka stories) methods. <b>NOWEB</b> engine only!',
)
.addTag('🟢 Status', 'Status (aka stories) methods')
.addTag('💬 Chats', `Chats methods`)
.addTag(
'👤 Contacts',
+15
View File
@@ -0,0 +1,15 @@
export interface EngineBootstrap {
bootstrap(): Promise<void>;
shutdown(): Promise<void>;
}
export class NoopEngineBootstrap implements EngineBootstrap {
async bootstrap(): Promise<void> {
return;
}
async shutdown(): Promise<void> {
return;
}
}
+67 -25
View File
@@ -1,15 +1,22 @@
import {
BeforeApplicationShutdown,
OnApplicationBootstrap,
UnprocessableEntityException,
} from '@nestjs/common';
import { WhatsappConfigService } from '@waha/config.service';
import {
EngineBootstrap,
NoopEngineBootstrap,
} from '@waha/core/abc/EngineBootstrap';
import { GowsEngineConfigService } from '@waha/core/config/GowsEngineConfigService';
import { GowsBootstrap } from '@waha/core/engines/gows/GowsBootstrap';
import { ISessionMeRepository } from '@waha/core/storage/ISessionMeRepository';
import { ISessionWorkerRepository } from '@waha/core/storage/ISessionWorkerRepository';
import { WAHAWebhook } from '@waha/structures/webhooks.dto';
import { waitUntil } from '@waha/utils/promiseTimeout';
import { VERSION } from '@waha/version';
import { EventEmitter } from 'events';
import { PinoLogger } from 'nestjs-pino';
import { merge, Observable, of } from 'rxjs';
import {
WAHAEngine,
@@ -25,28 +32,33 @@ import {
import { ISessionAuthRepository } from '../storage/ISessionAuthRepository';
import { ISessionConfigRepository } from '../storage/ISessionConfigRepository';
import { WhatsappSession } from './session.abc';
import { WebhookConductor } from './webhooks.abc';
// eslint-disable-next-line @typescript-eslint/no-var-requires
const AsyncLock = require('async-lock');
export abstract class SessionManager implements BeforeApplicationShutdown {
export abstract class SessionManager
implements BeforeApplicationShutdown, OnApplicationBootstrap
{
public store: any;
public sessionAuthRepository: ISessionAuthRepository;
public sessionConfigRepository: ISessionConfigRepository;
protected sessionMeRepository: ISessionMeRepository;
protected sessionWorkerRepository: ISessionWorkerRepository;
public events: EventEmitter;
private lock: any;
WAIT_STATUS_INTERVAL = 500;
WAIT_STATUS_TIMEOUT = 5_000;
LOCK_TIMEOUT = 10_000;
protected constructor(
protected config: WhatsappConfigService,
protected log: PinoLogger,
protected config: WhatsappConfigService,
protected gowsConfigService: GowsEngineConfigService,
) {
this.lock = new AsyncLock({ maxPending: Infinity });
this.lock = new AsyncLock({
maxPending: Infinity,
timeout: this.LOCK_TIMEOUT,
});
this.log.setContext(SessionManager.name);
}
@@ -72,7 +84,18 @@ export abstract class SessionManager implements BeforeApplicationShutdown {
protected abstract get EngineClass(): typeof WhatsappSession;
protected abstract get WebhookConductorClass(): typeof WebhookConductor;
public getSessionEvent(session: string, event: WAHAEvents): Observable<any> {
return of();
}
public getSessionEvents(
session: string,
events: WAHAEvents[],
): Observable<any> {
return merge(
...events.map((event) => this.getSessionEvent(session, event)),
);
}
//
// API Methods
@@ -114,22 +137,6 @@ export abstract class SessionManager implements BeforeApplicationShutdown {
await this.sessionWorkerRepository?.unassign(name, this.workerId);
}
protected handleSessionEvent(event: WAHAEvents, session: WhatsappSession) {
return (payload: any) => {
const me = session.getSessionMeInfo();
const data: WAHAWebhook = {
event: event,
session: session.name,
metadata: session.sessionConfig?.metadata,
me: me,
payload: payload,
engine: session.engine,
environment: VERSION,
};
this.events.emit(event, data);
};
}
async getWorkingSession(sessionName: string): Promise<WhatsappSession> {
return this.waitUntilStatus(sessionName, [WAHASessionStatus.WORKING]);
}
@@ -160,7 +167,42 @@ export abstract class SessionManager implements BeforeApplicationShutdown {
return session;
}
async beforeApplicationShutdown(signal?: string) {
this.events.removeAllListeners();
beforeApplicationShutdown(signal?: string) {
return;
}
onApplicationBootstrap(): any {
return;
}
protected getEngineBootstrap(engine: WAHAEngine): EngineBootstrap {
const logger = this.log.logger.child({ engine: engine.toLowerCase() });
if (engine === WAHAEngine.GOWS) {
const config = this.gowsConfigService.getBootstrapConfig();
return new GowsBootstrap(logger, config);
}
return new NoopEngineBootstrap();
}
}
export function populateSessionInfo(
event: WAHAEvents,
session: WhatsappSession,
) {
return (payload: any): WAHAWebhook => {
const id = payload._eventId;
const data = { ...payload };
delete data._eventId;
const me = session.getSessionMeInfo();
return {
id: id,
event: event,
session: session.name,
metadata: session.sessionConfig?.metadata,
me: me,
payload: data,
engine: session.engine,
environment: VERSION,
};
};
}
+246 -46
View File
@@ -1,22 +1,46 @@
import {
Channel,
ChannelListResult,
ChannelMessage,
ChannelSearchByText,
ChannelSearchByView,
CreateChannelRequest,
ListChannelsQuery,
PreviewChannelMessages,
} from '@waha/structures/channels.dto';
import {
ChatSummary,
GetChatMessageQuery,
GetChatMessagesFilter,
GetChatMessagesQuery,
} from '@waha/structures/chats.dto';
import { SendButtonsRequest } from '@waha/structures/chatting.buttons.dto';
import { BinaryFile, RemoteFile } from '@waha/structures/files.dto';
import { Label, LabelDTO, LabelID } from '@waha/structures/labels.dto';
import { PaginationParams } from '@waha/structures/pagination.dto';
import { WAMessage } from '@waha/structures/responses.dto';
import { DefaultMap } from '@waha/utils/DefaultMap';
import { generatePrefixedId } from '@waha/utils/ids';
import { LoggerBuilder } from '@waha/utils/logging';
import { EventEmitter } from 'events';
import { complete } from '@waha/utils/reactive/complete';
import { SwitchObservable } from '@waha/utils/reactive/SwitchObservable';
import * as fs from 'fs';
import * as lodash from 'lodash';
import { PinoLogger } from 'nestjs-pino';
import * as NodeCache from 'node-cache';
import { Logger } from 'pino';
import {
BehaviorSubject,
catchError,
delay,
filter,
of,
retry,
share,
Subject,
switchMap,
} from 'rxjs';
import { distinctUntilChanged, map } from 'rxjs/operators';
import { MessageId } from 'whatsapp-web.js';
import {
@@ -36,6 +60,7 @@ import {
MessageTextRequest,
MessageVideoRequest,
MessageVoiceRequest,
SendSeenRequest,
} from '../../structures/chatting.dto';
import { ContactQuery, ContactRequest } from '../../structures/contacts.dto';
import {
@@ -46,6 +71,7 @@ import {
} from '../../structures/enums.dto';
import {
CreateGroupRequest,
GroupsPaginationParams,
ParticipantsRequest,
SettingsSecurityChangeInfo,
} from '../../structures/groups.dto';
@@ -88,10 +114,6 @@ export function ensureSuffix(phone) {
return phone + suffix;
}
export enum WAHAInternalEvent {
ENGINE_START = 'engine.start',
}
export interface SessionParams {
name: string;
printQR: boolean;
@@ -104,7 +126,6 @@ export interface SessionParams {
}
export abstract class WhatsappSession {
public events: EventEmitter;
public engine: WAHAEngine;
public name: string;
@@ -119,6 +140,11 @@ export abstract class WhatsappSession {
private _status: WAHASessionStatus;
private shouldPrintQR: boolean;
protected events2: DefaultMap<WAHAEvents, SwitchObservable<any>>;
private status$: Subject<WAHASessionStatus>;
protected profilePictures: NodeCache = new NodeCache({
stdTTL: 24 * 60 * 60, // 1 day
});
public constructor({
name,
@@ -130,11 +156,64 @@ export abstract class WhatsappSession {
sessionConfig,
engineConfig,
}: SessionParams) {
this.events = new EventEmitter();
this.status$ = new BehaviorSubject(null);
this.name = name;
this.proxyConfig = proxyConfig;
this.loggerBuilder = loggerBuilder;
this.logger = loggerBuilder.child({ name: 'WhatsappSession' });
this.events2 = new DefaultMap<WAHAEvents, SwitchObservable<any>>(
(key) =>
new SwitchObservable((obs$) => {
return obs$.pipe(
catchError((err) => {
this.logger.error(
`Caught error, dropping value from, event: '${key}'`,
);
this.logger.error(err, err.stack);
throw err;
}),
filter(Boolean),
map((data) => {
data._eventId = generatePrefixedId('evt');
return data;
}),
retry(),
share(),
);
}),
);
this.events2.get(WAHAEvents.SESSION_STATUS).switch(
this.status$
// initial value is null
.pipe(filter(Boolean))
// Wait for WORKING status to get all the info
// https://github.com/devlikeapro/waha/issues/409
.pipe(
switchMap((status: WAHASessionStatus) => {
const me = this.getSessionMeInfo();
const hasMe = !!me?.pushName && !!me?.id;
// Delay WORKING by 1 second if condition is met
// Usually we get WORKING with all the info after
if (status === WAHASessionStatus.WORKING && !hasMe) {
return of(status).pipe(delay(2000));
}
return of(status);
}),
// Remove consecutive duplicate WORKING statuses
distinctUntilChanged(
(prev, curr) => prev === curr && curr === WAHASessionStatus.WORKING,
),
)
// Populate the session info
.pipe(
map<WAHASessionStatus, WASessionStatusBody>((status) => {
return { name: this.name, status: status };
}),
),
);
this.sessionStore = sessionStore;
this.mediaManager = mediaManager;
this.sessionConfig = sessionConfig;
@@ -142,6 +221,10 @@ export abstract class WhatsappSession {
this.shouldPrintQR = printQR;
}
public getEventObservable(event: WAHAEvents) {
return this.events2.get(event);
}
protected set status(value: WAHASessionStatus) {
if (this.unpairing && value !== WAHASessionStatus.STOPPED) {
// In case of unpairing
@@ -149,8 +232,7 @@ export abstract class WhatsappSession {
return;
}
this._status = value;
const body: WASessionStatusBody = { name: this.name, status: value };
this.events.emit(WAHAEvents.SESSION_STATUS, body);
this.status$.next(value);
}
public get status() {
@@ -166,25 +248,29 @@ export abstract class WhatsappSession {
// References:
// https://github.com/pedroslopez/whatsapp-web.js/issues/1420
// https://github.com/wppconnect-team/wppconnect/issues/1326
// https://superuser.com/questions/654565/how-to-run-google-chrome-in-a-single-process
// https://www.bannerbear.com/blog/ways-to-speed-up-puppeteer-screenshots/
return [
'--aggressive-cache-discard',
'--disable-accelerated-2d-canvas',
'--disable-application-cache',
'--disable-background-networking',
// '--disable-background-networking', // COMMENTED to test WEBJS stability
// DO NOT disable software rasterizer, it will break the video
// https://github.com/devlikeapro/waha/issues/629
// '--disable-software-rasterizer',
'--disable-cache',
'--disable-client-side-phishing-detection',
'--disable-component-update',
'--disable-default-apps',
'--disable-dev-shm-usage',
'--disable-extensions',
'--disable-features=site-per-process',
'--disable-gpu',
// '--disable-features=site-per-process', // COMMENTED to test WEBJS stability
//'--disable-gpu', // COMMENTED to test WEBJS stability
'--disable-offer-store-unmasked-wallet-cards',
'--disable-offline-load-stale-cache',
'--disable-popup-blocking',
'--disable-setuid-sandbox',
'--disable-site-isolation-trials', // https://superuser.com/questions/654565/how-to-run-google-chrome-in-a-single-process
'--disable-site-isolation-trials',
'--disable-speech-api',
'--disable-sync',
'--disable-translate',
@@ -193,7 +279,8 @@ export abstract class WhatsappSession {
'--hide-scrollbars',
'--ignore-certificate-errors',
'--ignore-ssl-errors',
'--in-process-gpu', // https://superuser.com/questions/654565/how-to-run-google-chrome-in-a-single-process
// https://github.com/devlikeapro/waha/issues/725
// '--in-process-gpu', // COMMENTED to test WEBJS stability
'--metrics-recording-only',
'--mute-audio',
'--no-default-browser-check',
@@ -202,7 +289,7 @@ export abstract class WhatsappSession {
'--no-sandbox',
'--no-zygote',
'--password-store=basic',
'--renderer-process-limit=2', // https://superuser.com/questions/654565/how-to-run-google-chrome-in-a-single-process
// '--renderer-process-limit=2', // COMMENTED to test WEBJS stability
'--safebrowsing-disable-auto-update',
// '--single-process',
'--use-mock-keychain',
@@ -220,30 +307,15 @@ export abstract class WhatsappSession {
/** Stop the session */
abstract stop(): Promise<void>;
protected stopEvents() {
complete(this.events2);
}
/* Unpair the account */
async unpair(): Promise<void> {
return;
}
/** Subscribe the handler to specific hook */
subscribeSessionEvent(
hook: WAHAEvents | string,
handler: (message) => void,
): boolean {
switch (hook) {
case WAHAEvents.SESSION_STATUS:
this.events.on(WAHAEvents.SESSION_STATUS, handler);
return true;
default:
return false;
}
}
abstract subscribeEngineEvent(
hook: WAHAEvents | string,
handler: (message) => void,
): boolean;
/**
* START - Methods for API
*/
@@ -260,21 +332,58 @@ export abstract class WhatsappSession {
throw new NotImplementedByEngineError();
}
public authorizeCode(code: string) {
throw new NotImplementedByEngineError();
}
abstract getScreenshot(): Promise<Buffer>;
public getSessionMeInfo(): MeInfo | null {
return null;
}
public getCaptcha(): Promise<QR> {
/**
* Profile methods
*/
public setProfileName(name: string): Promise<boolean> {
throw new NotImplementedByEngineError();
}
public saveCaptcha(code: string) {
public setProfileStatus(status: string): Promise<boolean> {
throw new NotImplementedByEngineError();
}
public async updateProfilePicture(
file: BinaryFile | RemoteFile | null,
): Promise<boolean> {
if (file) {
await this.setProfilePicture(file);
} else {
await this.deleteProfilePicture();
}
// Refresh profile picture after update
setTimeout(() => {
this.logger.info('Refreshing my profile picture after update...');
this.refreshMyProfilePicture()
.then(() => {
this.logger.info('Refreshed my profile picture after update');
})
.catch((err) => {
this.logger.error('Error refreshing my profile picture after update');
this.logger.error(err, err.stack);
});
}, 3_000);
return true;
}
protected async refreshMyProfilePicture() {
const me = this.getSessionMeInfo();
await this.getContactProfilePicture(me.id, true);
}
protected setProfilePicture(file: BinaryFile | RemoteFile): Promise<boolean> {
throw new NotImplementedByEngineError();
}
protected deleteProfilePicture(): Promise<boolean> {
throw new NotImplementedByEngineError();
}
@@ -317,7 +426,7 @@ export abstract class WhatsappSession {
abstract reply(request: MessageReplyRequest);
abstract sendSeen(chat: ChatRequest);
abstract sendSeen(chat: SendSeenRequest);
abstract startTyping(chat: ChatRequest);
@@ -336,6 +445,12 @@ export abstract class WhatsappSession {
throw new NotImplementedByEngineError();
}
public getChatsOverview(
pagination: PaginationParams,
): Promise<ChatSummary[]> {
throw new NotImplementedByEngineError();
}
public deleteChat(chatId) {
throw new NotImplementedByEngineError();
}
@@ -356,6 +471,18 @@ export abstract class WhatsappSession {
throw new NotImplementedByEngineError();
}
public pinMessage(
chatId: string,
messageId: string,
duration: number,
): Promise<boolean> {
throw new NotImplementedByEngineError();
}
public unpinMessage(chatId: string, messageId: string): Promise<boolean> {
throw new NotImplementedByEngineError();
}
public deleteMessage(chatId: string, messageId: string) {
throw new NotImplementedByEngineError();
}
@@ -431,12 +558,44 @@ export abstract class WhatsappSession {
throw new NotImplementedByEngineError();
}
public getContactAbout(query: ContactQuery) {
public getContactAbout(query: ContactQuery): Promise<{ about: string }> {
throw new NotImplementedByEngineError();
}
public getContactProfilePicture(query: ContactQuery) {
throw new NotImplementedByEngineError();
/**
* Fetch the latest profile picture of the contact (group, newsletter, etc.)
* @param id
*/
abstract fetchContactProfilePicture(id: string): Promise<string | null>;
public async getContactProfilePicture(
id: string,
refresh: boolean,
): Promise<string | null> {
const has: boolean = this.profilePictures.has(id);
if (!has || refresh) {
await this.refreshProfilePicture(id);
}
return this.profilePictures.get(id) || null;
}
protected async refreshProfilePicture(id: string) {
let fn: Promise<string>;
if (isNewsletter(id)) {
fn = this.channelsGetChannel(id).then(
(channel: Channel) => channel.picture || channel.preview,
);
} else {
fn = this.fetchContactProfilePicture(id);
}
this.profilePictures.del(id);
const url = await fn.catch((err) => {
this.logger.warn('Error fetching profile picture');
this.logger.warn(err, err.stack);
return null;
});
this.profilePictures.set(id, url);
return url;
}
public blockContact(request: ContactRequest) {
@@ -454,7 +613,19 @@ export abstract class WhatsappSession {
throw new NotImplementedByEngineError();
}
public getGroups() {
public joinGroup(code: string): Promise<string> {
throw new NotImplementedByEngineError();
}
public joinInfoGroup(code: string): Promise<any> {
throw new NotImplementedByEngineError();
}
public getGroups(pagination: PaginationParams) {
throw new NotImplementedByEngineError();
}
public refreshGroups(): Promise<boolean> {
throw new NotImplementedByEngineError();
}
@@ -534,13 +705,32 @@ export abstract class WhatsappSession {
throw new NotImplementedByEngineError();
}
public subscribePresence(id: string): Promise<void> {
public subscribePresence(id: string): Promise<any> {
throw new NotImplementedByEngineError();
}
/**
* Channels methods
*/
public searchChannelsByView(
query: ChannelSearchByView,
): Promise<ChannelListResult> {
throw new NotImplementedByEngineError();
}
public searchChannelsByText(
query: ChannelSearchByText,
): Promise<ChannelListResult> {
throw new NotImplementedByEngineError();
}
public async previewChannelMessages(
inviteCode: string,
query: PreviewChannelMessages,
): Promise<ChannelMessage[]> {
throw new NotImplementedByEngineError();
}
public channelsList(query: ListChannelsQuery): Promise<Channel[]> {
throw new NotImplementedByEngineError();
}
@@ -652,3 +842,13 @@ export function isNewsletter(jid: string) {
export function getChannelInviteLink(code: string) {
return `https://whatsapp.com/channel/${code}`;
}
export function parseChannelInviteLink(link: string): string {
// https://www.whatsapp.com/channel/123 => 123
const code = link.split('/').pop();
return code;
}
export function getPublicUrlFromDirectPath(directPath: string) {
return `https://pps.whatsapp.net${directPath}`;
}
-26
View File
@@ -1,26 +0,0 @@
import { LoggerBuilder } from '@waha/utils/logging';
import { Logger } from 'pino';
import { WebhookConfig } from '../../structures/webhooks.config.dto';
import { WhatsappSession } from './session.abc';
export abstract class WebhookSender {
protected url: string;
protected logger: Logger;
protected readonly config: WebhookConfig;
constructor(
loggerBuilder: LoggerBuilder,
protected webhookConfig: WebhookConfig,
) {
this.url = webhookConfig.url;
this.logger = loggerBuilder.child({ name: WebhookSender.name });
this.config = webhookConfig;
}
abstract send(json: any): void;
}
export abstract class WebhookConductor {
abstract configure(session: WhatsappSession, webhooks: WebhookConfig[]);
}
+45 -59
View File
@@ -14,10 +14,10 @@ import {
import { SessionManager } from '@waha/core/abc/manager.abc';
import { WebsocketHeartbeatJob } from '@waha/nestjs/ws/WebsocketHeartbeatJob';
import { WebSocket } from '@waha/nestjs/ws/ws';
import { WAHAEvents } from '@waha/structures/enums.dto';
import { WAHAEvents, WAHAEventsWild } from '@waha/structures/enums.dto';
import { EventWildUnmask } from '@waha/utils/events';
import { generatePrefixedId } from '@waha/utils/ids';
import { IncomingMessage } from 'http';
import * as lodash from 'lodash';
import * as url from 'url';
import { Server } from 'ws';
@@ -32,16 +32,14 @@ export class WebsocketGatewayCore
OnGatewayDisconnect,
BeforeApplicationShutdown
{
HEARTBEAT_INTERVAL = 10_000;
HEARTBEAT_INTERVAL = 60_000;
@WebSocketServer()
server: Server;
private listeners: Map<WebSocket, { session: string; events: string[] }> =
new Map();
private readonly logger: LoggerService;
private heartbeat: WebsocketHeartbeatJob;
private eventUnmask = new EventWildUnmask(WAHAEvents, WAHAEventsWild);
constructor(private manager: SessionManager) {
this.logger = new Logger('WebsocketGateway');
@@ -56,86 +54,74 @@ export class WebsocketGatewayCore
const id = generatePrefixedId('wsc');
socket.id = id;
this.logger.debug(`New client connected: ${request.url}`);
const params = this.getParams(id, request, socket);
if (!params) {
return;
}
const { session, events } = params;
const params = this.getParams(request);
const session: string = params.session;
const events: WAHAEvents[] = params.events;
this.logger.debug(
`Client connected to session: '${session}', events: ${events}, ${id}`,
);
this.listeners.set(socket, { session, events });
const sub = this.manager
.getSessionEvents(session, events)
.subscribe((data) => {
this.logger.debug(`Sending data to client, event.id: ${data.id}`, data);
socket.send(JSON.stringify(data), (err) => {
if (!err) {
return;
}
this.logger.error(`Error sending data to client: ${err}`);
});
});
socket.on('close', () => {
this.logger.debug(`Client disconnected - ${socket.id}`);
sub.unsubscribe();
});
}
private getParams(id: string, request: IncomingMessage, socket: WebSocket) {
private getParams(request: IncomingMessage) {
const query = url.parse(request.url, true).query;
const session = (query.session as string) || '*';
if (session !== '*') {
this.logger.warn(
`Only connecting to all sessions is allowed for now, use session=*, ${id}`,
);
const error =
'Only connecting to all sessions is allowed for now, use session=*';
socket.close(4001, JSON.stringify({ error }));
return null;
}
const events = ((query.events as string) || '*').split(',');
if (
!lodash.isEqual(events, ['*']) &&
!lodash.isEqual(events, [WAHAEvents.SESSION_STATUS])
) {
this.logger.warn(
`Only \'session.status\' event is allowed for now, use events=session.status or events=*, ${id}`,
);
const error =
"Only 'session.status' event is allowed for now, use events=session.status or events=*";
socket.close(4001, JSON.stringify({ error }));
return null;
let paramsEvents = (query.events as string[]) || '*';
// if params events string - split by ","
if (typeof paramsEvents === 'string') {
paramsEvents = paramsEvents.split(',');
}
const events = this.eventUnmask.unmask(paramsEvents);
return { session, events };
}
handleDisconnect(socket: WebSocket): any {
this.logger.debug(`Client disconnected - ${socket.id}`);
this.listeners.delete(socket);
}
async beforeApplicationShutdown(signal?: string) {
this.logger.log('Shutting down websocket server');
this.heartbeat?.stop();
// Allow pending messages to be sent, it can be even 1ms, just to release the event loop
await sleep(100);
this.server?.clients.forEach((options, client) => {
// @ts-ignore
this.logger.debug(`Closing client connection - ${client.id}...`);
client.close(1001, 'Server is shutting down');
});
// Do not turn off heartbeat service here,
// it's responsible for terminating the connection that is not alive
// Close clients and server
await this.close(this.server);
this.logger.log('Websocket server is down');
this.heartbeat?.stop();
}
// Cherry-pick from nestjs new version
// https://github.com/nestjs/nest/pull/13531/files
private async close(server: any) {
const closeEventSignal = new Promise((resolve, reject) =>
server.close((err) => (err ? reject(err) : resolve(undefined))),
);
for (const ws of server.clients) {
ws.terminate();
}
await closeEventSignal;
}
afterInit(server: Server) {
this.logger.debug('Websocket server initialized');
this.manager.events.on(
WAHAEvents.SESSION_STATUS,
this.sendToAll.bind(this),
);
this.logger.debug('Subscribed to manager events');
this.logger.debug('Starting heartbeat service...');
this.heartbeat.start(server);
this.logger.debug('Heartbeat service started');
}
sendToAll(data: any) {
this.listeners.forEach((options, client) => {
if (options.events.length === 0) {
return;
}
this.logger.debug('Sending data to client', data);
client.send(JSON.stringify(data));
});
}
}
+11 -1
View File
@@ -5,13 +5,17 @@ import { PassportModule } from '@nestjs/passport';
import { ServeStaticModule } from '@nestjs/serve-static';
import { TerminusModule } from '@nestjs/terminus';
import { ChannelsController } from '@waha/api/channels.controller';
import { ProfileController } from '@waha/api/profile.controller';
import {
ServerController,
ServerDebugController,
} from '@waha/api/server.controller';
import { WebsocketGatewayCore } from '@waha/core/api/websocket.gateway.core';
import { GowsEngineConfigService } from '@waha/core/config/GowsEngineConfigService';
import { WebJSEngineConfigService } from '@waha/core/config/WebJSEngineConfigService';
import { MediaLocalStorageModule } from '@waha/core/media/local/media.local.storage.module';
import { MediaLocalStorageConfig } from '@waha/core/media/local/MediaLocalStorageConfig';
import { ChannelsInfoServiceCore } from '@waha/core/services/ChannelsInfoServiceCore';
import { BufferJsonReplacerInterceptor } from '@waha/nestjs/BufferJsonReplacerInterceptor';
import {
getPinoHttpUseLevel,
@@ -105,7 +109,9 @@ export const IMPORTS_CORE = [
const IMPORTS_MEDIA = [
ConfigModule.forRoot({
validationSchema: Joi.object({
WAHA_MEDIA_STORAGE: Joi.string().valid('LOCAL', 'S3').default('LOCAL'),
WAHA_MEDIA_STORAGE: Joi.string()
.valid('LOCAL', 'S3', 'POSTGRESQL')
.default('LOCAL'),
}),
}),
MediaLocalStorageModule,
@@ -116,6 +122,7 @@ const IMPORTS = [...IMPORTS_CORE, ...IMPORTS_MEDIA];
export const CONTROLLERS = [
AuthController,
SessionsController,
ProfileController,
ChattingController,
ChatsController,
ChannelsController,
@@ -144,8 +151,11 @@ const PROVIDERS = [
provide: APP_INTERCEPTOR,
useClass: BufferJsonReplacerInterceptor,
},
ChannelsInfoServiceCore,
DashboardConfigServiceCore,
SwaggerConfigServiceCore,
WebJSEngineConfigService,
GowsEngineConfigService,
WhatsappConfigService,
EngineConfigService,
WebsocketGatewayCore,
@@ -0,0 +1,26 @@
import { Injectable } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { BootstrapConfig } from '@waha/core/engines/gows/GowsBootstrap';
import { GowsConfig } from '@waha/core/engines/gows/session.gows.core';
@Injectable()
export class GowsEngineConfigService {
constructor(protected configService: ConfigService) {}
getBootstrapConfig(): BootstrapConfig {
return {
path: this.configService.get('WAHA_GOWS_PATH'),
socket: this.getSocket(),
};
}
getSocket() {
return this.configService.get('WAHA_GOWS_SOCKET', '/tmp/gows.sock');
}
getConfig(): GowsConfig {
return {
connection: 'unix:' + this.getSocket(),
};
}
}
@@ -0,0 +1,37 @@
import { Injectable } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { WebJSConfig } from '../../core/engines/webjs/session.webjs.core';
@Injectable()
export class WebJSEngineConfigService {
constructor(protected configService: ConfigService) {}
getConfig(): WebJSConfig {
let webVersion = this.configService.get<string>(
'WAHA_WEBJS_WEB_VERSION',
undefined,
);
if (webVersion === '2.2412.54-videofix') {
// Deprecated version
webVersion = undefined;
}
return {
webVersion: webVersion,
cacheType: this.getCacheType(),
};
}
getCacheType(): 'local' | 'none' {
const cacheType = this.configService
.get<string>('WAHA_WEBJS_CACHE_TYPE', 'none')
.toLowerCase();
if (cacheType != 'local' && cacheType != 'none') {
throw new Error(
'Invalid cache type, only "local" and "none" are allowed',
);
}
return cacheType;
}
}
+3
View File
@@ -0,0 +1,3 @@
export enum Jid {
BROADCAST = 'status@broadcast',
}
@@ -0,0 +1,31 @@
import { EventEmitter } from 'events';
import { Observable, Subscription } from 'rxjs';
interface EventValue<T> {
event: string;
data: T;
}
export class EventsFromObservable<Events extends string> extends EventEmitter {
private subscription: Subscription;
constructor(private observable: Observable<EventValue<any>>) {
super();
}
on(event: Events, listener: (...args: any[]) => void): this {
return super.on(event as string, listener);
}
start() {
this.subscription = this.observable.subscribe({
next: (data) => this.emit(data.event, data.data),
complete: () => this.removeAllListeners(),
error: (err) => null,
});
}
stop() {
this.subscription.unsubscribe();
}
}
+72
View File
@@ -0,0 +1,72 @@
import { EngineBootstrap } from '@waha/core/abc/EngineBootstrap';
import { GowsSubprocess } from '@waha/core/engines/gows/GowsSubprocess';
import { promises as fs } from 'fs';
import { Logger } from 'pino';
export async function isUnixSocket(socketPath: string): Promise<boolean> {
try {
// Get the file stats
const stats = await fs.lstat(socketPath);
// Check if the path is a socket
return stats.isSocket();
} catch (error) {
// Handle errors (e.g., file does not exist)
if (error.code === 'ENOENT') {
return false; // Path does not exist
}
// Re-throw other errors for debugging purposes
throw error;
}
}
export interface BootstrapConfig {
path: string;
socket: string;
}
export class GowsBootstrap implements EngineBootstrap {
private gows: GowsSubprocess;
constructor(
private logger: Logger,
private config: BootstrapConfig,
) {}
async bootstrap(): Promise<void> {
if (!this.config.path) {
this.logger.warn('GOWS path is not set, skipping GOWS initialization.');
this.logger.warn('Make sure to run GOWS manually.');
this.checkSocket(this.config.socket);
return;
}
this.gows = new GowsSubprocess(
this.logger,
this.config.path,
this.config.socket,
);
this.gows.start(() => {
this.logger.info(`GOWS stopped, exiting...`);
process.kill(process.pid, 'SIGTERM');
});
await this.gows.waitWhenReady(10_000);
await this.checkSocket(this.config.socket);
return;
}
async shutdown(): Promise<void> {
if (this.gows) {
await this.gows.stop();
}
}
/**
* Check the provided path exists and is a socket
* @param path
*/
async checkSocket(path: string) {
if (!(await isUnixSocket(path))) {
throw new Error(`Invalid socket path: ${path}`);
}
}
}
@@ -0,0 +1,69 @@
import * as grpc from '@grpc/grpc-js';
import { messages } from '@waha/core/engines/gows/grpc/gows';
import { EnginePayload } from '@waha/structures/webhooks.dto';
import { sleep } from '@waha/utils/promiseTimeout';
import { Logger } from 'pino';
import { Observable } from 'rxjs';
/**
* Observable that listens to a gRPC stream and emits EnginePayload objects.
* Pass a factory function that returns a client and a stream.
*/
export class GowsEventStreamObservable extends Observable<EnginePayload> {
_client: grpc.Client;
CLIENT_CLOSE_TIMEOUT = 1_000;
constructor(
private logger: Logger,
factory: () => {
client: grpc.Client;
stream: grpc.ClientReadableStream<messages.EventJson>;
},
) {
super((subscriber) => {
this.logger.debug('Creating grpc client and stream...');
const { client, stream } = factory();
this._client = client;
stream.on('data', (raw) => {
const obj = raw.toObject();
obj.data = JSON.parse(obj.data);
subscriber?.next(obj);
});
stream.on('end', (...args) => {
this.logger.debug('Stream ended', args);
subscriber?.complete();
subscriber = null;
});
stream.on('error', async (err: any) => {
const CLIENT_CANCELLED_CODE = grpc.status.CANCELLED;
if (err.code === CLIENT_CANCELLED_CODE) {
this.logger.debug('Stream cancelled by client');
return;
}
this.logger.error(err, 'Stream error');
// Give some time to node event loop to process the error
await sleep(100);
subscriber?.error(err);
subscriber = null;
});
return async () => {
this.logger.debug('Closing stream client...');
client.close();
await sleep(this.CLIENT_CLOSE_TIMEOUT);
this.logger.debug('Stream client closed');
this.logger.debug('Cancelling stream...');
stream.cancel();
this.logger.debug('Stream cancelled');
};
});
}
get client(): Omit<grpc.Client, 'close'> {
return this._client;
}
}
+100
View File
@@ -0,0 +1,100 @@
import { sleep, waitUntil } from '@waha/utils/promiseTimeout';
import { spawn } from 'child_process';
import { Logger } from 'pino';
export class GowsSubprocess {
private checkIntervalMs: number = 100;
private readyDelayMs: number = 1_000;
private readyText = 'gRPC server started!';
private child: any;
private ready: boolean = false;
constructor(
private logger: Logger,
readonly path: string,
readonly socket: string,
) {}
start(onExit: (code: number) => void) {
this.logger.info('Starting GOWS subprocess...');
this.logger.debug(`GOWS path '${this.path}', socket: '${this.socket}'...`);
this.child = spawn(this.path, ['--socket', this.socket], {
detached: true,
});
this.logger.debug(`GOWS started with PID: ${this.child.pid}`);
this.child.on('close', async (code, singal) => {
const msg = code
? `GOWS subprocess closed with code ${code}`
: `GOWS subprocess closed by signal ${singal}`;
this.logger.debug(msg);
onExit(code);
});
this.child.on('error', (err) => {
this.logger.error(`GOWS subprocess error: ${err}`);
});
this.child.stderr.setEncoding('utf8');
this.child.stderr.on('data', (data) => {
this.logger.error(data);
});
this.child.stdout.setEncoding('utf8');
this.child.stdout.on('data', async (data) => {
// remove empty line at the end, split by \n
const lines = data.trim().split('\n');
lines.forEach((line) => this.log(line));
});
this.listenReady();
}
listenReady() {
this.child.stdout.on('data', async (data) => {
if (this.ready) {
return;
}
if (!data.includes(this.readyText)) {
return;
}
await sleep(this.readyDelayMs);
this.ready = true;
this.logger.info('GOWS is ready');
});
}
async waitWhenReady(timeout: number) {
const started = await waitUntil(
async () => this.ready,
this.checkIntervalMs,
timeout,
);
if (!started) {
const msg = 'GOWS did not start after 10 seconds';
this.logger.error(msg);
throw new Error(msg);
}
}
async stop() {
this.logger.info('Stopping GOWS subprocess...');
this.child?.kill('SIGTERM');
this.logger.info('GOWS subprocess stopped');
}
private log(msg) {
if (msg.startsWith('ERROR | ')) {
this.logger.error(msg.slice(8));
} else if (msg.startsWith('WARN | ')) {
this.logger.warn(msg.slice(7));
} else if (msg.startsWith('INFO | ')) {
this.logger.info(msg.slice(7));
} else if (msg.startsWith('DEBUG | ')) {
this.logger.debug(msg.slice(8));
} else if (msg.startsWith('TRACE | ')) {
this.logger.trace(msg.slice(8));
} else {
this.logger.info(msg);
}
}
}
File diff suppressed because it is too large. Load diff
+817
View File
@@ -0,0 +1,817 @@
// GENERATED CODE -- DO NOT EDIT!
'use strict';
var grpc = require('@grpc/grpc-js');
var gows_pb = require('./gows_pb.js');
function serialize_messages_ChatPresenceRequest(arg) {
if (!(arg instanceof gows_pb.ChatPresenceRequest)) {
throw new Error('Expected argument of type messages.ChatPresenceRequest');
}
return Buffer.from(arg.serializeBinary());
}
function deserialize_messages_ChatPresenceRequest(buffer_arg) {
return gows_pb.ChatPresenceRequest.deserializeBinary(new Uint8Array(buffer_arg));
}
function serialize_messages_CheckPhonesRequest(arg) {
if (!(arg instanceof gows_pb.CheckPhonesRequest)) {
throw new Error('Expected argument of type messages.CheckPhonesRequest');
}
return Buffer.from(arg.serializeBinary());
}
function deserialize_messages_CheckPhonesRequest(buffer_arg) {
return gows_pb.CheckPhonesRequest.deserializeBinary(new Uint8Array(buffer_arg));
}
function serialize_messages_CheckPhonesResponse(arg) {
if (!(arg instanceof gows_pb.CheckPhonesResponse)) {
throw new Error('Expected argument of type messages.CheckPhonesResponse');
}
return Buffer.from(arg.serializeBinary());
}
function deserialize_messages_CheckPhonesResponse(buffer_arg) {
return gows_pb.CheckPhonesResponse.deserializeBinary(new Uint8Array(buffer_arg));
}
function serialize_messages_CreateNewsletterRequest(arg) {
if (!(arg instanceof gows_pb.CreateNewsletterRequest)) {
throw new Error('Expected argument of type messages.CreateNewsletterRequest');
}
return Buffer.from(arg.serializeBinary());
}
function deserialize_messages_CreateNewsletterRequest(buffer_arg) {
return gows_pb.CreateNewsletterRequest.deserializeBinary(new Uint8Array(buffer_arg));
}
function serialize_messages_DownloadMediaRequest(arg) {
if (!(arg instanceof gows_pb.DownloadMediaRequest)) {
throw new Error('Expected argument of type messages.DownloadMediaRequest');
}
return Buffer.from(arg.serializeBinary());
}
function deserialize_messages_DownloadMediaRequest(buffer_arg) {
return gows_pb.DownloadMediaRequest.deserializeBinary(new Uint8Array(buffer_arg));
}
function serialize_messages_DownloadMediaResponse(arg) {
if (!(arg instanceof gows_pb.DownloadMediaResponse)) {
throw new Error('Expected argument of type messages.DownloadMediaResponse');
}
return Buffer.from(arg.serializeBinary());
}
function deserialize_messages_DownloadMediaResponse(buffer_arg) {
return gows_pb.DownloadMediaResponse.deserializeBinary(new Uint8Array(buffer_arg));
}
function serialize_messages_Empty(arg) {
if (!(arg instanceof gows_pb.Empty)) {
throw new Error('Expected argument of type messages.Empty');
}
return Buffer.from(arg.serializeBinary());
}
function deserialize_messages_Empty(buffer_arg) {
return gows_pb.Empty.deserializeBinary(new Uint8Array(buffer_arg));
}
function serialize_messages_EntityByIdRequest(arg) {
if (!(arg instanceof gows_pb.EntityByIdRequest)) {
throw new Error('Expected argument of type messages.EntityByIdRequest');
}
return Buffer.from(arg.serializeBinary());
}
function deserialize_messages_EntityByIdRequest(buffer_arg) {
return gows_pb.EntityByIdRequest.deserializeBinary(new Uint8Array(buffer_arg));
}
function serialize_messages_EventJson(arg) {
if (!(arg instanceof gows_pb.EventJson)) {
throw new Error('Expected argument of type messages.EventJson');
}
return Buffer.from(arg.serializeBinary());
}
function deserialize_messages_EventJson(buffer_arg) {
return gows_pb.EventJson.deserializeBinary(new Uint8Array(buffer_arg));
}
function serialize_messages_GetChatsRequest(arg) {
if (!(arg instanceof gows_pb.GetChatsRequest)) {
throw new Error('Expected argument of type messages.GetChatsRequest');
}
return Buffer.from(arg.serializeBinary());
}
function deserialize_messages_GetChatsRequest(buffer_arg) {
return gows_pb.GetChatsRequest.deserializeBinary(new Uint8Array(buffer_arg));
}
function serialize_messages_GetContactsRequest(arg) {
if (!(arg instanceof gows_pb.GetContactsRequest)) {
throw new Error('Expected argument of type messages.GetContactsRequest');
}
return Buffer.from(arg.serializeBinary());
}
function deserialize_messages_GetContactsRequest(buffer_arg) {
return gows_pb.GetContactsRequest.deserializeBinary(new Uint8Array(buffer_arg));
}
function serialize_messages_GetMessagesRequest(arg) {
if (!(arg instanceof gows_pb.GetMessagesRequest)) {
throw new Error('Expected argument of type messages.GetMessagesRequest');
}
return Buffer.from(arg.serializeBinary());
}
function deserialize_messages_GetMessagesRequest(buffer_arg) {
return gows_pb.GetMessagesRequest.deserializeBinary(new Uint8Array(buffer_arg));
}
function serialize_messages_GetNewsletterMessagesByInviteRequest(arg) {
if (!(arg instanceof gows_pb.GetNewsletterMessagesByInviteRequest)) {
throw new Error('Expected argument of type messages.GetNewsletterMessagesByInviteRequest');
}
return Buffer.from(arg.serializeBinary());
}
function deserialize_messages_GetNewsletterMessagesByInviteRequest(buffer_arg) {
return gows_pb.GetNewsletterMessagesByInviteRequest.deserializeBinary(new Uint8Array(buffer_arg));
}
function serialize_messages_Json(arg) {
if (!(arg instanceof gows_pb.Json)) {
throw new Error('Expected argument of type messages.Json');
}
return Buffer.from(arg.serializeBinary());
}
function deserialize_messages_Json(buffer_arg) {
return gows_pb.Json.deserializeBinary(new Uint8Array(buffer_arg));
}
function serialize_messages_JsonList(arg) {
if (!(arg instanceof gows_pb.JsonList)) {
throw new Error('Expected argument of type messages.JsonList');
}
return Buffer.from(arg.serializeBinary());
}
function deserialize_messages_JsonList(buffer_arg) {
return gows_pb.JsonList.deserializeBinary(new Uint8Array(buffer_arg));
}
function serialize_messages_MarkReadRequest(arg) {
if (!(arg instanceof gows_pb.MarkReadRequest)) {
throw new Error('Expected argument of type messages.MarkReadRequest');
}
return Buffer.from(arg.serializeBinary());
}
function deserialize_messages_MarkReadRequest(buffer_arg) {
return gows_pb.MarkReadRequest.deserializeBinary(new Uint8Array(buffer_arg));
}
function serialize_messages_MessageReaction(arg) {
if (!(arg instanceof gows_pb.MessageReaction)) {
throw new Error('Expected argument of type messages.MessageReaction');
}
return Buffer.from(arg.serializeBinary());
}
function deserialize_messages_MessageReaction(buffer_arg) {
return gows_pb.MessageReaction.deserializeBinary(new Uint8Array(buffer_arg));
}
function serialize_messages_MessageRequest(arg) {
if (!(arg instanceof gows_pb.MessageRequest)) {
throw new Error('Expected argument of type messages.MessageRequest');
}
return Buffer.from(arg.serializeBinary());
}
function deserialize_messages_MessageRequest(buffer_arg) {
return gows_pb.MessageRequest.deserializeBinary(new Uint8Array(buffer_arg));
}
function serialize_messages_MessageResponse(arg) {
if (!(arg instanceof gows_pb.MessageResponse)) {
throw new Error('Expected argument of type messages.MessageResponse');
}
return Buffer.from(arg.serializeBinary());
}
function deserialize_messages_MessageResponse(buffer_arg) {
return gows_pb.MessageResponse.deserializeBinary(new Uint8Array(buffer_arg));
}
function serialize_messages_Newsletter(arg) {
if (!(arg instanceof gows_pb.Newsletter)) {
throw new Error('Expected argument of type messages.Newsletter');
}
return Buffer.from(arg.serializeBinary());
}
function deserialize_messages_Newsletter(buffer_arg) {
return gows_pb.Newsletter.deserializeBinary(new Uint8Array(buffer_arg));
}
function serialize_messages_NewsletterInfoRequest(arg) {
if (!(arg instanceof gows_pb.NewsletterInfoRequest)) {
throw new Error('Expected argument of type messages.NewsletterInfoRequest');
}
return Buffer.from(arg.serializeBinary());
}
function deserialize_messages_NewsletterInfoRequest(buffer_arg) {
return gows_pb.NewsletterInfoRequest.deserializeBinary(new Uint8Array(buffer_arg));
}
function serialize_messages_NewsletterList(arg) {
if (!(arg instanceof gows_pb.NewsletterList)) {
throw new Error('Expected argument of type messages.NewsletterList');
}
return Buffer.from(arg.serializeBinary());
}
function deserialize_messages_NewsletterList(buffer_arg) {
return gows_pb.NewsletterList.deserializeBinary(new Uint8Array(buffer_arg));
}
function serialize_messages_NewsletterListRequest(arg) {
if (!(arg instanceof gows_pb.NewsletterListRequest)) {
throw new Error('Expected argument of type messages.NewsletterListRequest');
}
return Buffer.from(arg.serializeBinary());
}
function deserialize_messages_NewsletterListRequest(buffer_arg) {
return gows_pb.NewsletterListRequest.deserializeBinary(new Uint8Array(buffer_arg));
}
function serialize_messages_NewsletterSearchPageResult(arg) {
if (!(arg instanceof gows_pb.NewsletterSearchPageResult)) {
throw new Error('Expected argument of type messages.NewsletterSearchPageResult');
}
return Buffer.from(arg.serializeBinary());
}
function deserialize_messages_NewsletterSearchPageResult(buffer_arg) {
return gows_pb.NewsletterSearchPageResult.deserializeBinary(new Uint8Array(buffer_arg));
}
function serialize_messages_NewsletterToggleFollowRequest(arg) {
if (!(arg instanceof gows_pb.NewsletterToggleFollowRequest)) {
throw new Error('Expected argument of type messages.NewsletterToggleFollowRequest');
}
return Buffer.from(arg.serializeBinary());
}
function deserialize_messages_NewsletterToggleFollowRequest(buffer_arg) {
return gows_pb.NewsletterToggleFollowRequest.deserializeBinary(new Uint8Array(buffer_arg));
}
function serialize_messages_NewsletterToggleMuteRequest(arg) {
if (!(arg instanceof gows_pb.NewsletterToggleMuteRequest)) {
throw new Error('Expected argument of type messages.NewsletterToggleMuteRequest');
}
return Buffer.from(arg.serializeBinary());
}
function deserialize_messages_NewsletterToggleMuteRequest(buffer_arg) {
return gows_pb.NewsletterToggleMuteRequest.deserializeBinary(new Uint8Array(buffer_arg));
}
function serialize_messages_PairCodeRequest(arg) {
if (!(arg instanceof gows_pb.PairCodeRequest)) {
throw new Error('Expected argument of type messages.PairCodeRequest');
}
return Buffer.from(arg.serializeBinary());
}
function deserialize_messages_PairCodeRequest(buffer_arg) {
return gows_pb.PairCodeRequest.deserializeBinary(new Uint8Array(buffer_arg));
}
function serialize_messages_PairCodeResponse(arg) {
if (!(arg instanceof gows_pb.PairCodeResponse)) {
throw new Error('Expected argument of type messages.PairCodeResponse');
}
return Buffer.from(arg.serializeBinary());
}
function deserialize_messages_PairCodeResponse(buffer_arg) {
return gows_pb.PairCodeResponse.deserializeBinary(new Uint8Array(buffer_arg));
}
function serialize_messages_PresenceRequest(arg) {
if (!(arg instanceof gows_pb.PresenceRequest)) {
throw new Error('Expected argument of type messages.PresenceRequest');
}
return Buffer.from(arg.serializeBinary());
}
function deserialize_messages_PresenceRequest(buffer_arg) {
return gows_pb.PresenceRequest.deserializeBinary(new Uint8Array(buffer_arg));
}
function serialize_messages_ProfileNameRequest(arg) {
if (!(arg instanceof gows_pb.ProfileNameRequest)) {
throw new Error('Expected argument of type messages.ProfileNameRequest');
}
return Buffer.from(arg.serializeBinary());
}
function deserialize_messages_ProfileNameRequest(buffer_arg) {
return gows_pb.ProfileNameRequest.deserializeBinary(new Uint8Array(buffer_arg));
}
function serialize_messages_ProfilePictureRequest(arg) {
if (!(arg instanceof gows_pb.ProfilePictureRequest)) {
throw new Error('Expected argument of type messages.ProfilePictureRequest');
}
return Buffer.from(arg.serializeBinary());
}
function deserialize_messages_ProfilePictureRequest(buffer_arg) {
return gows_pb.ProfilePictureRequest.deserializeBinary(new Uint8Array(buffer_arg));
}
function serialize_messages_ProfilePictureResponse(arg) {
if (!(arg instanceof gows_pb.ProfilePictureResponse)) {
throw new Error('Expected argument of type messages.ProfilePictureResponse');
}
return Buffer.from(arg.serializeBinary());
}
function deserialize_messages_ProfilePictureResponse(buffer_arg) {
return gows_pb.ProfilePictureResponse.deserializeBinary(new Uint8Array(buffer_arg));
}
function serialize_messages_ProfileStatusRequest(arg) {
if (!(arg instanceof gows_pb.ProfileStatusRequest)) {
throw new Error('Expected argument of type messages.ProfileStatusRequest');
}
return Buffer.from(arg.serializeBinary());
}
function deserialize_messages_ProfileStatusRequest(buffer_arg) {
return gows_pb.ProfileStatusRequest.deserializeBinary(new Uint8Array(buffer_arg));
}
function serialize_messages_SearchNewslettersByTextRequest(arg) {
if (!(arg instanceof gows_pb.SearchNewslettersByTextRequest)) {
throw new Error('Expected argument of type messages.SearchNewslettersByTextRequest');
}
return Buffer.from(arg.serializeBinary());
}
function deserialize_messages_SearchNewslettersByTextRequest(buffer_arg) {
return gows_pb.SearchNewslettersByTextRequest.deserializeBinary(new Uint8Array(buffer_arg));
}
function serialize_messages_SearchNewslettersByViewRequest(arg) {
if (!(arg instanceof gows_pb.SearchNewslettersByViewRequest)) {
throw new Error('Expected argument of type messages.SearchNewslettersByViewRequest');
}
return Buffer.from(arg.serializeBinary());
}
function deserialize_messages_SearchNewslettersByViewRequest(buffer_arg) {
return gows_pb.SearchNewslettersByViewRequest.deserializeBinary(new Uint8Array(buffer_arg));
}
function serialize_messages_Session(arg) {
if (!(arg instanceof gows_pb.Session)) {
throw new Error('Expected argument of type messages.Session');
}
return Buffer.from(arg.serializeBinary());
}
function deserialize_messages_Session(buffer_arg) {
return gows_pb.Session.deserializeBinary(new Uint8Array(buffer_arg));
}
function serialize_messages_SessionStateResponse(arg) {
if (!(arg instanceof gows_pb.SessionStateResponse)) {
throw new Error('Expected argument of type messages.SessionStateResponse');
}
return Buffer.from(arg.serializeBinary());
}
function deserialize_messages_SessionStateResponse(buffer_arg) {
return gows_pb.SessionStateResponse.deserializeBinary(new Uint8Array(buffer_arg));
}
function serialize_messages_SetProfilePictureRequest(arg) {
if (!(arg instanceof gows_pb.SetProfilePictureRequest)) {
throw new Error('Expected argument of type messages.SetProfilePictureRequest');
}
return Buffer.from(arg.serializeBinary());
}
function deserialize_messages_SetProfilePictureRequest(buffer_arg) {
return gows_pb.SetProfilePictureRequest.deserializeBinary(new Uint8Array(buffer_arg));
}
function serialize_messages_StartSessionRequest(arg) {
if (!(arg instanceof gows_pb.StartSessionRequest)) {
throw new Error('Expected argument of type messages.StartSessionRequest');
}
return Buffer.from(arg.serializeBinary());
}
function deserialize_messages_StartSessionRequest(buffer_arg) {
return gows_pb.StartSessionRequest.deserializeBinary(new Uint8Array(buffer_arg));
}
function serialize_messages_SubscribePresenceRequest(arg) {
if (!(arg instanceof gows_pb.SubscribePresenceRequest)) {
throw new Error('Expected argument of type messages.SubscribePresenceRequest');
}
return Buffer.from(arg.serializeBinary());
}
function deserialize_messages_SubscribePresenceRequest(buffer_arg) {
return gows_pb.SubscribePresenceRequest.deserializeBinary(new Uint8Array(buffer_arg));
}
//
// Events
//
var EventStreamService = exports.EventStreamService = {
streamEvents: {
path: '/messages.EventStream/StreamEvents',
requestStream: false,
responseStream: true,
requestType: gows_pb.Session,
responseType: gows_pb.EventJson,
requestSerialize: serialize_messages_Session,
requestDeserialize: deserialize_messages_Session,
responseSerialize: serialize_messages_EventJson,
responseDeserialize: deserialize_messages_EventJson,
},
};
exports.EventStreamClient = grpc.makeGenericClientConstructor(EventStreamService);
var MessageServiceService = exports.MessageServiceService = {
//
// Session management
//
startSession: {
path: '/messages.MessageService/StartSession',
requestStream: false,
responseStream: false,
requestType: gows_pb.StartSessionRequest,
responseType: gows_pb.Empty,
requestSerialize: serialize_messages_StartSessionRequest,
requestDeserialize: deserialize_messages_StartSessionRequest,
responseSerialize: serialize_messages_Empty,
responseDeserialize: deserialize_messages_Empty,
},
stopSession: {
path: '/messages.MessageService/StopSession',
requestStream: false,
responseStream: false,
requestType: gows_pb.Session,
responseType: gows_pb.Empty,
requestSerialize: serialize_messages_Session,
requestDeserialize: deserialize_messages_Session,
responseSerialize: serialize_messages_Empty,
responseDeserialize: deserialize_messages_Empty,
},
getSessionState: {
path: '/messages.MessageService/GetSessionState',
requestStream: false,
responseStream: false,
requestType: gows_pb.Session,
responseType: gows_pb.SessionStateResponse,
requestSerialize: serialize_messages_Session,
requestDeserialize: deserialize_messages_Session,
responseSerialize: serialize_messages_SessionStateResponse,
responseDeserialize: deserialize_messages_SessionStateResponse,
},
requestCode: {
path: '/messages.MessageService/RequestCode',
requestStream: false,
responseStream: false,
requestType: gows_pb.PairCodeRequest,
responseType: gows_pb.PairCodeResponse,
requestSerialize: serialize_messages_PairCodeRequest,
requestDeserialize: deserialize_messages_PairCodeRequest,
responseSerialize: serialize_messages_PairCodeResponse,
responseDeserialize: deserialize_messages_PairCodeResponse,
},
logout: {
path: '/messages.MessageService/Logout',
requestStream: false,
responseStream: false,
requestType: gows_pb.Session,
responseType: gows_pb.Empty,
requestSerialize: serialize_messages_Session,
requestDeserialize: deserialize_messages_Session,
responseSerialize: serialize_messages_Empty,
responseDeserialize: deserialize_messages_Empty,
},
//
// Profile
//
setProfileName: {
path: '/messages.MessageService/SetProfileName',
requestStream: false,
responseStream: false,
requestType: gows_pb.ProfileNameRequest,
responseType: gows_pb.Empty,
requestSerialize: serialize_messages_ProfileNameRequest,
requestDeserialize: deserialize_messages_ProfileNameRequest,
responseSerialize: serialize_messages_Empty,
responseDeserialize: deserialize_messages_Empty,
},
setProfileStatus: {
path: '/messages.MessageService/SetProfileStatus',
requestStream: false,
responseStream: false,
requestType: gows_pb.ProfileStatusRequest,
responseType: gows_pb.Empty,
requestSerialize: serialize_messages_ProfileStatusRequest,
requestDeserialize: deserialize_messages_ProfileStatusRequest,
responseSerialize: serialize_messages_Empty,
responseDeserialize: deserialize_messages_Empty,
},
setProfilePicture: {
path: '/messages.MessageService/SetProfilePicture',
requestStream: false,
responseStream: false,
requestType: gows_pb.SetProfilePictureRequest,
responseType: gows_pb.Empty,
requestSerialize: serialize_messages_SetProfilePictureRequest,
requestDeserialize: deserialize_messages_SetProfilePictureRequest,
responseSerialize: serialize_messages_Empty,
responseDeserialize: deserialize_messages_Empty,
},
//
// Actions
//
sendMessage: {
path: '/messages.MessageService/SendMessage',
requestStream: false,
responseStream: false,
requestType: gows_pb.MessageRequest,
responseType: gows_pb.MessageResponse,
requestSerialize: serialize_messages_MessageRequest,
requestDeserialize: deserialize_messages_MessageRequest,
responseSerialize: serialize_messages_MessageResponse,
responseDeserialize: deserialize_messages_MessageResponse,
},
sendReaction: {
path: '/messages.MessageService/SendReaction',
requestStream: false,
responseStream: false,
requestType: gows_pb.MessageReaction,
responseType: gows_pb.MessageResponse,
requestSerialize: serialize_messages_MessageReaction,
requestDeserialize: deserialize_messages_MessageReaction,
responseSerialize: serialize_messages_MessageResponse,
responseDeserialize: deserialize_messages_MessageResponse,
},
getProfilePicture: {
path: '/messages.MessageService/GetProfilePicture',
requestStream: false,
responseStream: false,
requestType: gows_pb.ProfilePictureRequest,
responseType: gows_pb.ProfilePictureResponse,
requestSerialize: serialize_messages_ProfilePictureRequest,
requestDeserialize: deserialize_messages_ProfilePictureRequest,
responseSerialize: serialize_messages_ProfilePictureResponse,
responseDeserialize: deserialize_messages_ProfilePictureResponse,
},
sendPresence: {
path: '/messages.MessageService/SendPresence',
requestStream: false,
responseStream: false,
requestType: gows_pb.PresenceRequest,
responseType: gows_pb.Empty,
requestSerialize: serialize_messages_PresenceRequest,
requestDeserialize: deserialize_messages_PresenceRequest,
responseSerialize: serialize_messages_Empty,
responseDeserialize: deserialize_messages_Empty,
},
sendChatPresence: {
path: '/messages.MessageService/SendChatPresence',
requestStream: false,
responseStream: false,
requestType: gows_pb.ChatPresenceRequest,
responseType: gows_pb.Empty,
requestSerialize: serialize_messages_ChatPresenceRequest,
requestDeserialize: deserialize_messages_ChatPresenceRequest,
responseSerialize: serialize_messages_Empty,
responseDeserialize: deserialize_messages_Empty,
},
subscribePresence: {
path: '/messages.MessageService/SubscribePresence',
requestStream: false,
responseStream: false,
requestType: gows_pb.SubscribePresenceRequest,
responseType: gows_pb.Empty,
requestSerialize: serialize_messages_SubscribePresenceRequest,
requestDeserialize: deserialize_messages_SubscribePresenceRequest,
responseSerialize: serialize_messages_Empty,
responseDeserialize: deserialize_messages_Empty,
},
markRead: {
path: '/messages.MessageService/MarkRead',
requestStream: false,
responseStream: false,
requestType: gows_pb.MarkReadRequest,
responseType: gows_pb.Empty,
requestSerialize: serialize_messages_MarkReadRequest,
requestDeserialize: deserialize_messages_MarkReadRequest,
responseSerialize: serialize_messages_Empty,
responseDeserialize: deserialize_messages_Empty,
},
checkPhones: {
path: '/messages.MessageService/CheckPhones',
requestStream: false,
responseStream: false,
requestType: gows_pb.CheckPhonesRequest,
responseType: gows_pb.CheckPhonesResponse,
requestSerialize: serialize_messages_CheckPhonesRequest,
requestDeserialize: deserialize_messages_CheckPhonesRequest,
responseSerialize: serialize_messages_CheckPhonesResponse,
responseDeserialize: deserialize_messages_CheckPhonesResponse,
},
//
// Newsletters
//
getSubscribedNewsletters: {
path: '/messages.MessageService/GetSubscribedNewsletters',
requestStream: false,
responseStream: false,
requestType: gows_pb.NewsletterListRequest,
responseType: gows_pb.NewsletterList,
requestSerialize: serialize_messages_NewsletterListRequest,
requestDeserialize: deserialize_messages_NewsletterListRequest,
responseSerialize: serialize_messages_NewsletterList,
responseDeserialize: deserialize_messages_NewsletterList,
},
getNewsletterInfo: {
path: '/messages.MessageService/GetNewsletterInfo',
requestStream: false,
responseStream: false,
requestType: gows_pb.NewsletterInfoRequest,
responseType: gows_pb.Newsletter,
requestSerialize: serialize_messages_NewsletterInfoRequest,
requestDeserialize: deserialize_messages_NewsletterInfoRequest,
responseSerialize: serialize_messages_Newsletter,
responseDeserialize: deserialize_messages_Newsletter,
},
getNewsletterMessagesByInvite: {
path: '/messages.MessageService/GetNewsletterMessagesByInvite',
requestStream: false,
responseStream: false,
requestType: gows_pb.GetNewsletterMessagesByInviteRequest,
responseType: gows_pb.Json,
requestSerialize: serialize_messages_GetNewsletterMessagesByInviteRequest,
requestDeserialize: deserialize_messages_GetNewsletterMessagesByInviteRequest,
responseSerialize: serialize_messages_Json,
responseDeserialize: deserialize_messages_Json,
},
searchNewslettersByView: {
path: '/messages.MessageService/SearchNewslettersByView',
requestStream: false,
responseStream: false,
requestType: gows_pb.SearchNewslettersByViewRequest,
responseType: gows_pb.NewsletterSearchPageResult,
requestSerialize: serialize_messages_SearchNewslettersByViewRequest,
requestDeserialize: deserialize_messages_SearchNewslettersByViewRequest,
responseSerialize: serialize_messages_NewsletterSearchPageResult,
responseDeserialize: deserialize_messages_NewsletterSearchPageResult,
},
searchNewslettersByText: {
path: '/messages.MessageService/SearchNewslettersByText',
requestStream: false,
responseStream: false,
requestType: gows_pb.SearchNewslettersByTextRequest,
responseType: gows_pb.NewsletterSearchPageResult,
requestSerialize: serialize_messages_SearchNewslettersByTextRequest,
requestDeserialize: deserialize_messages_SearchNewslettersByTextRequest,
responseSerialize: serialize_messages_NewsletterSearchPageResult,
responseDeserialize: deserialize_messages_NewsletterSearchPageResult,
},
createNewsletter: {
path: '/messages.MessageService/CreateNewsletter',
requestStream: false,
responseStream: false,
requestType: gows_pb.CreateNewsletterRequest,
responseType: gows_pb.Newsletter,
requestSerialize: serialize_messages_CreateNewsletterRequest,
requestDeserialize: deserialize_messages_CreateNewsletterRequest,
responseSerialize: serialize_messages_Newsletter,
responseDeserialize: deserialize_messages_Newsletter,
},
newsletterToggleMute: {
path: '/messages.MessageService/NewsletterToggleMute',
requestStream: false,
responseStream: false,
requestType: gows_pb.NewsletterToggleMuteRequest,
responseType: gows_pb.Empty,
requestSerialize: serialize_messages_NewsletterToggleMuteRequest,
requestDeserialize: deserialize_messages_NewsletterToggleMuteRequest,
responseSerialize: serialize_messages_Empty,
responseDeserialize: deserialize_messages_Empty,
},
newsletterToggleFollow: {
path: '/messages.MessageService/NewsletterToggleFollow',
requestStream: false,
responseStream: false,
requestType: gows_pb.NewsletterToggleFollowRequest,
responseType: gows_pb.Empty,
requestSerialize: serialize_messages_NewsletterToggleFollowRequest,
requestDeserialize: deserialize_messages_NewsletterToggleFollowRequest,
responseSerialize: serialize_messages_Empty,
responseDeserialize: deserialize_messages_Empty,
},
//
// Media
//
downloadMedia: {
path: '/messages.MessageService/DownloadMedia',
requestStream: false,
responseStream: false,
requestType: gows_pb.DownloadMediaRequest,
responseType: gows_pb.DownloadMediaResponse,
requestSerialize: serialize_messages_DownloadMediaRequest,
requestDeserialize: deserialize_messages_DownloadMediaRequest,
responseSerialize: serialize_messages_DownloadMediaResponse,
responseDeserialize: deserialize_messages_DownloadMediaResponse,
},
//
// Storage
//
getMessageById: {
path: '/messages.MessageService/GetMessageById',
requestStream: false,
responseStream: false,
requestType: gows_pb.EntityByIdRequest,
responseType: gows_pb.Json,
requestSerialize: serialize_messages_EntityByIdRequest,
requestDeserialize: deserialize_messages_EntityByIdRequest,
responseSerialize: serialize_messages_Json,
responseDeserialize: deserialize_messages_Json,
},
getMessages: {
path: '/messages.MessageService/GetMessages',
requestStream: false,
responseStream: false,
requestType: gows_pb.GetMessagesRequest,
responseType: gows_pb.JsonList,
requestSerialize: serialize_messages_GetMessagesRequest,
requestDeserialize: deserialize_messages_GetMessagesRequest,
responseSerialize: serialize_messages_JsonList,
responseDeserialize: deserialize_messages_JsonList,
},
getContacts: {
path: '/messages.MessageService/GetContacts',
requestStream: false,
responseStream: false,
requestType: gows_pb.GetContactsRequest,
responseType: gows_pb.JsonList,
requestSerialize: serialize_messages_GetContactsRequest,
requestDeserialize: deserialize_messages_GetContactsRequest,
responseSerialize: serialize_messages_JsonList,
responseDeserialize: deserialize_messages_JsonList,
},
getContactById: {
path: '/messages.MessageService/GetContactById',
requestStream: false,
responseStream: false,
requestType: gows_pb.EntityByIdRequest,
responseType: gows_pb.Json,
requestSerialize: serialize_messages_EntityByIdRequest,
requestDeserialize: deserialize_messages_EntityByIdRequest,
responseSerialize: serialize_messages_Json,
responseDeserialize: deserialize_messages_Json,
},
getChats: {
path: '/messages.MessageService/GetChats',
requestStream: false,
responseStream: false,
requestType: gows_pb.GetChatsRequest,
responseType: gows_pb.JsonList,
requestSerialize: serialize_messages_GetChatsRequest,
requestDeserialize: deserialize_messages_GetChatsRequest,
responseSerialize: serialize_messages_JsonList,
responseDeserialize: deserialize_messages_JsonList,
},
};
exports.MessageServiceClient = grpc.makeGenericClientConstructor(MessageServiceService);
File diff suppressed because it is too large. Load diff
+42
View File
@@ -0,0 +1,42 @@
import { messages } from '@waha/core/engines/gows/grpc/gows';
import { MessageStatus } from '@waha/core/engines/gows/types';
import { WAMessageAck } from '@waha/structures/enums.dto';
export function optional(value: any, type) {
if (value === null || value === undefined) {
return null;
}
return new type({ value: value });
}
export function parseJson(value: messages.Json | { data?: string }) {
if (value instanceof messages.Json) {
value = value.toObject();
}
if (value.data === undefined) {
return undefined;
}
return JSON.parse(value.data);
}
export function parseJsonList(value: messages.JsonList) {
return value.toObject().elements.map((value) => parseJson(value));
}
export function statusToAck(status: MessageStatus): WAMessageAck | null {
switch (status) {
case MessageStatus.Error:
return WAMessageAck.ERROR;
case MessageStatus.Pending:
return WAMessageAck.PENDING;
case MessageStatus.ServerAck:
return WAMessageAck.SERVER;
case MessageStatus.DeliveryAck:
return WAMessageAck.DEVICE;
case MessageStatus.Read:
return WAMessageAck.READ;
case MessageStatus.Played:
return WAMessageAck.PLAYED;
}
return null;
}
File diff suppressed because it is too large. Load diff
+4
View File
@@ -0,0 +1,4 @@
export interface GowsAuth {
address(): string;
dialect(): string;
}
@@ -0,0 +1,26 @@
import * as path from 'node:path';
import { DataStore } from '@waha/core/abc/DataStore';
import { GowsAuthSimple } from '@waha/core/engines/gows/store/GowsAuthSimple';
import { LocalStore } from '@waha/core/storage/LocalStore';
import { GowsAuth } from './GowsAuth';
export class GowsAuthFactoryCore {
buildAuth(store: DataStore, name: string): Promise<GowsAuth> {
if (store instanceof LocalStore) return this.buildSqlite3(store, name);
throw new Error(`Unsupported store type '${store.constructor.name}'`);
}
protected async buildSqlite3(
store: LocalStore,
name: string,
): Promise<GowsAuth> {
await store.init(name);
const authFolder = store.getSessionDirectory(name);
// resolve path
const authFolderFullPath = path.resolve(authFolder);
const connection = `file:${authFolderFullPath}/gows.db`;
return new GowsAuthSimple(connection, 'sqlite3');
}
}
@@ -0,0 +1,19 @@
import { GowsAuth } from '@waha/core/engines/gows/store/GowsAuth';
export class GowsAuthSimple implements GowsAuth {
private _address: string;
private _dialect: string;
constructor(address: string, dialect: string) {
this._address = address;
this._dialect = dialect;
}
address() {
return this._address;
}
dialect(): string {
return this._dialect;
}
}
+41
View File
@@ -0,0 +1,41 @@
export interface Presence {
From: string;
Unavailable: boolean;
LastSeen: string;
}
export enum ChatPresenceState {
COMPOSING = 'composing',
PAUSED = 'paused',
}
export enum ChatPresenceMedia {
TEXT = '',
AUDIO = 'audio',
}
export interface ChatPresence {
Chat: string;
Sender: string;
IsFromMe: boolean;
IsGroup: boolean;
State: ChatPresenceState;
Media: ChatPresenceMedia;
}
/**
* StatusError Status = 0
* StatusPending Status = 1
* StatusServerAck Status = 2
* StatusDeliveryAck Status = 3
* StatusRead Status = 4
* StatusPlayed Status = 5
*/
export enum MessageStatus {
Error = 0,
Pending = 1,
ServerAck = 2,
DeliveryAck = 3,
Read = 4,
Played = 5,
}
@@ -11,7 +11,7 @@ export class NowebAuthFactoryCore {
protected async buildLocalAuth(store: LocalStore, name: string) {
await store.init(name);
const authFolder = store.getSessionDirectory(name);
const { state, saveCreds } = await useMultiFileAuthState(authFolder);
return { state, saveCreds };
const { state, saveCreds, close } = await useMultiFileAuthState(authFolder);
return { state, saveCreds, close };
}
}
File diff suppressed because it is too large. Load diff
@@ -6,7 +6,10 @@ export interface IChatRepository {
getAllByIds(ids: string[]): Promise<Chat[]>;
getAllWithMessages(pagination: PaginationParams): Promise<Chat[]>;
getAllWithMessages(
pagination: PaginationParams,
broadcast: boolean,
): Promise<Chat[]>;
getById(id: string): Promise<Chat | null>;
@@ -0,0 +1,14 @@
import { GroupMetadata } from '@adiwajshing/baileys';
import { PaginationParams } from '@waha/structures/pagination.dto';
export interface IGroupRepository {
getAll(pagination?: PaginationParams): Promise<GroupMetadata[]>;
getById(id: string): Promise<GroupMetadata | null>;
deleteAll(): Promise<void>;
deleteById(id: string): Promise<void>;
save(group: GroupMetadata): Promise<void>;
}
+3 -18
View File
@@ -1,5 +1,4 @@
import { WAMessage } from '@adiwajshing/baileys';
import { LabelAssociation } from '@adiwajshing/baileys/lib/Types/LabelAssociation';
import { IGroupRepository } from '@waha/core/engines/noweb/store/IGroupRepository';
import { ILabelAssociationRepository } from '@waha/core/engines/noweb/store/ILabelAssociationsRepository';
import { ILabelsRepository } from '@waha/core/engines/noweb/store/ILabelsRepository';
@@ -16,25 +15,11 @@ export abstract class INowebStorage {
abstract getChatRepository(): IChatRepository;
abstract getGroupRepository(): IGroupRepository;
abstract getMessagesRepository(): IMessagesRepository;
abstract getLabelsRepository(): ILabelsRepository;
abstract getLabelAssociationRepository(): ILabelAssociationRepository;
protected getMessagesMetadata(): Map<string, any> {
return new Map()
.set('jid', (msg: WAMessage) => msg.key.remoteJid)
.set('id', (msg: WAMessage) => msg.key.id)
.set('messageTimestamp', (msg: WAMessage) => msg.messageTimestamp);
}
protected getLabelAssociationMetadata() {
return new Map().set(
'id',
(a: LabelAssociation) =>
// @ts-ignore
`${a.type}_${a.labelId}_${a.chatId}_${a.messageId}`,
);
}
}
+6 -1
View File
@@ -4,6 +4,7 @@ import {
Contact,
proto,
} from '@adiwajshing/baileys';
import { GroupMetadata } from '@adiwajshing/baileys/lib/Types/GroupMetadata';
import { Label } from '@adiwajshing/baileys/lib/Types/Label';
import { GetChatMessagesFilter } from '@waha/structures/chats.dto';
import { PaginationParams } from '@waha/structures/pagination.dto';
@@ -27,7 +28,7 @@ export interface INowebStore {
getMessageById(chatId: string, messageId: string): Promise<any>;
getChats(pagination: PaginationParams): Promise<Chat[]>;
getChats(pagination: PaginationParams, broadcast: boolean): Promise<Chat[]>;
getContacts(pagination: PaginationParams): Promise<Contact[]>;
@@ -40,4 +41,8 @@ export interface INowebStore {
getChatsByLabelId(labelId: string): Promise<Chat[]>;
getChatLabels(chatId: string): Promise<Label[]>;
getGroups(pagination: PaginationParams): Promise<GroupMetadata[]>;
resetGroupsCache(): void;
}
@@ -1,8 +1,15 @@
import { Chat, Contact, makeInMemoryStore, proto } from '@adiwajshing/baileys';
import makeWASocket, {
Chat,
Contact,
GroupMetadata,
makeInMemoryStore,
proto,
} from '@adiwajshing/baileys';
import { Label } from '@adiwajshing/baileys/lib/Types/Label';
import { BadRequestException } from '@nestjs/common';
import { GetChatMessagesFilter } from '@waha/structures/chats.dto';
import { PaginationParams } from '@waha/structures/pagination.dto';
import { PaginatorInMemory } from '@waha/utils/Paginator';
import { INowebStore } from './INowebStore';
@@ -10,6 +17,8 @@ import { INowebStore } from './INowebStore';
const logger = require('pino')();
export class NowebInMemoryStore implements INowebStore {
private socket: ReturnType<typeof makeWASocket>;
private store: ReturnType<typeof makeInMemoryStore>;
errorMessage =
'Enable NOWEB store "config.noweb.store.enabled=True" and "config.noweb.store.full_sync=True" when starting a new session. ' +
@@ -33,6 +42,7 @@ export class NowebInMemoryStore implements INowebStore {
bind(ev: any, socket: any) {
this.store.bind(ev);
this.socket = socket;
}
loadMessage(jid: string, id: string): Promise<proto.IWebMessageInfo> {
@@ -51,7 +61,7 @@ export class NowebInMemoryStore implements INowebStore {
throw new BadRequestException(this.errorMessage);
}
getChats(pagination: PaginationParams): Promise<Chat[]> {
getChats(pagination: PaginationParams, broadcast: boolean): Promise<Chat[]> {
throw new BadRequestException(this.errorMessage);
}
@@ -78,4 +88,15 @@ export class NowebInMemoryStore implements INowebStore {
getChatLabels(chatId: string): Promise<Label[]> {
throw new BadRequestException(this.errorMessage);
}
async getGroups(pagination: PaginationParams): Promise<GroupMetadata[]> {
const response = await this.socket?.groupFetchAllParticipating();
const groups = Object.values(response);
const paginator = new PaginatorInMemory(pagination);
return paginator.apply(groups);
}
resetGroupsCache() {
return;
}
}
@@ -1,27 +1,34 @@
import {
import makeWASocket, {
areJidsSameUser,
BaileysEventEmitter,
Chat,
ChatUpdate,
Contact,
GroupParticipant,
isRealMessage,
jidNormalizedUser,
ParticipantAction,
proto,
updateMessageWithReaction,
updateMessageWithReceipt,
} from '@adiwajshing/baileys';
import { GroupMetadata } from '@adiwajshing/baileys/lib/Types/GroupMetadata';
import { Label } from '@adiwajshing/baileys/lib/Types/Label';
import {
LabelAssociation,
LabelAssociationType,
} from '@adiwajshing/baileys/lib/Types/LabelAssociation';
import { IGroupRepository } from '@waha/core/engines/noweb/store/IGroupRepository';
import { ILabelAssociationRepository } from '@waha/core/engines/noweb/store/ILabelAssociationsRepository';
import { ILabelsRepository } from '@waha/core/engines/noweb/store/ILabelsRepository';
import { GetChatMessagesFilter } from '@waha/structures/chats.dto';
import { PaginationParams, SortOrder } from '@waha/structures/pagination.dto';
import { DefaultMap } from '@waha/utils/DefaultMap';
import { sleep, waitUntil } from '@waha/utils/promiseTimeout';
import * as lodash from 'lodash';
import { toNumber } from 'lodash';
import { Logger } from 'pino';
import { toJID } from '../session.noweb.core';
import { IChatRepository } from './IChatRepository';
import { IContactRepository } from './IContactRepository';
import { IMessagesRepository } from './IMessagesRepository';
@@ -31,15 +38,27 @@ import { INowebStore } from './INowebStore';
// eslint-disable-next-line @typescript-eslint/no-var-requires
const AsyncLock = require('async-lock');
type ms = number;
const HOUR: ms = 60 * 60 * 1000;
export class NowebPersistentStore implements INowebStore {
private socket: any;
private socket: ReturnType<typeof makeWASocket>;
private chatRepo: IChatRepository;
private groupRepo: IGroupRepository;
private contactRepo: IContactRepository;
private messagesRepo: IMessagesRepository;
private labelsRepo: ILabelsRepository;
private labelAssociationsRepo: ILabelAssociationRepository;
public presences: any;
private lock: any;
private groupsFetchLock: any = new AsyncLock({
maxPending: Infinity,
maxExecutionTime: 60_000,
});
private lastTimeGroupUpdate: Date = new Date(0);
private lastTimeGroupFetch: Date = new Date(0);
private GROUP_METADATA_CACHE_TIME = 24 * HOUR;
constructor(
private logger: Logger,
@@ -47,6 +66,7 @@ export class NowebPersistentStore implements INowebStore {
) {
this.socket = null;
this.chatRepo = storage.getChatRepository();
this.groupRepo = storage.getGroupRepository();
this.contactRepo = storage.getContactsRepository();
this.messagesRepo = storage.getMessagesRepository();
this.labelsRepo = storage.getLabelsRepository();
@@ -88,6 +108,19 @@ export class NowebPersistentStore implements INowebStore {
ev.on('chats.delete', (data) =>
this.withLock('chats', () => this.onChatDelete(data)),
);
// Groups
ev.on('groups.upsert', (data) =>
this.withLock('groups', () => this.onGroupUpsert(data)),
);
ev.on('groups.update', (data) =>
this.withLock('groups', () => this.onGroupUpdate(data)),
);
ev.on('group-participants.update', (data) =>
this.withLock(`group-${data.id}`, () =>
this.onGroupParticipantsUpdate(data),
),
);
// Contacts
ev.on('contacts.upsert', (data) =>
this.withLock('contacts', () => this.onContactsUpsert(data)),
@@ -192,17 +225,94 @@ export class NowebPersistentStore implements INowebStore {
private async onChatUpsert(chats: Chat[]) {
for (const chat of chats) {
delete chat['messages'];
chat.conversationTimestamp = toNumber(chat.conversationTimestamp);
chat.conversationTimestamp = toNumber(chat.conversationTimestamp) || null;
await this.chatRepo.save(chat);
}
this.logger.info(`history sync - '${chats.length}' synced chats`);
this.logger.info(`store sync - '${chats.length}' synced chats`);
}
private async onGroupUpsert(groups: GroupMetadata[]) {
for (const group of groups) {
await this.groupRepo.save(group);
}
this.logger.info(`store sync - '${groups.length}' synced groups`);
}
private async onGroupUpdate(groups: Partial<GroupMetadata>[]) {
for (const update of groups) {
let group = await this.groupRepo.getById(update.id);
group = Object.assign(group || {}, update) as GroupMetadata;
await this.groupRepo.save(group);
}
this.logger.info(`store sync - '${groups.length}' updated groups`);
this.lastTimeGroupUpdate = new Date();
}
private async onGroupParticipantsUpdate(data) {
const id: string = data.id;
const participants: string[] = data.participants;
const action: ParticipantAction = data.action;
if (action == 'remove') {
// Remove the group if the current user is removed
const myJid = this.socket?.authState?.creds?.me?.id;
const participantsIncludesMe = lodash.find(participants, (p) =>
areJidsSameUser(p, myJid),
);
if (participantsIncludesMe) {
await this.groupRepo.deleteById(id);
return;
}
}
let group = await this.groupRepo.getById(id);
if (!group) {
group = { id: id, participants: [] } as GroupMetadata;
}
const participantsById = new DefaultMap<string, GroupParticipant>((key) => {
return { id: key, admin: null } as GroupParticipant;
});
for (const participant of group.participants) {
participantsById.set(participant.id, participant);
}
for (const participant of participants) {
this.participantUpdate(participantsById, participant, action);
}
group.participants = Array.from(participantsById.values());
await this.groupRepo.save(group);
}
private participantUpdate(
participantsById: DefaultMap<string, GroupParticipant>,
participant: string,
action: ParticipantAction,
) {
switch (action) {
case 'add':
// if there's no participant - add it (by id)
participantsById.get(participant);
break;
case 'remove':
// remove the participant (by id)
participantsById.delete(participant);
break;
case 'promote':
// set admin: admin
participantsById.get(participant).admin = 'admin';
break;
case 'demote':
participantsById.get(participant).admin = null;
break;
}
}
private async onChatUpdate(updates: ChatUpdate[]) {
for (const update of updates) {
const chat = (await this.chatRepo.getById(update.id)) || ({} as Chat);
Object.assign(chat, update);
chat.conversationTimestamp = toNumber(chat.conversationTimestamp);
chat.conversationTimestamp = toNumber(chat.conversationTimestamp) || null;
delete chat['messages'];
await this.chatRepo.save(chat);
}
@@ -233,7 +343,7 @@ export class NowebPersistentStore implements INowebStore {
private async onContactUpdate(updates: Partial<Contact>[]) {
for (const update of updates) {
const contact = await this.contactRepo.getById(update.id);
let contact = await this.contactRepo.getById(update.id);
if (!contact) {
this.logger.warn(
@@ -241,7 +351,7 @@ export class NowebPersistentStore implements INowebStore {
update,
)}'`,
);
continue;
contact = {} as Contact;
// TODO: Find contact by hash if not found
// find contact by attrs.hash, when user is not saved as a contact
// check the in-memory for that
@@ -338,10 +448,45 @@ export class NowebPersistentStore implements INowebStore {
return this.messagesRepo.getByJidById(chatId, messageId);
}
getChats(pagination: PaginationParams): Promise<Chat[]> {
getChats(pagination: PaginationParams, broadcast: boolean): Promise<Chat[]> {
pagination.sortBy ||= 'conversationTimestamp';
pagination.sortOrder ||= SortOrder.DESC;
return this.chatRepo.getAllWithMessages(pagination);
return this.chatRepo.getAllWithMessages(pagination, broadcast);
}
private shouldFetchGroup(): boolean {
const timePassed = new Date().getTime() - this.lastTimeGroupFetch.getTime();
return timePassed > this.GROUP_METADATA_CACHE_TIME;
}
private async fetchGroups() {
await this.groupsFetchLock.acquire('groups-fetch', async () => {
if (!this.shouldFetchGroup()) {
// Update has been done by another request
return;
}
const lastTimeGroupUpdate = this.lastTimeGroupUpdate;
await this.groupRepo.deleteAll();
await this.socket?.groupFetchAllParticipating();
// Wait until the groups update is done
await waitUntil(
async () => this.lastTimeGroupUpdate > lastTimeGroupUpdate,
100,
5_000,
);
this.lastTimeGroupFetch = new Date();
});
}
resetGroupsCache() {
this.lastTimeGroupFetch = new Date(0);
}
async getGroups(pagination: PaginationParams): Promise<GroupMetadata[]> {
if (this.shouldFetchGroup()) {
await this.fetchGroups();
}
return this.groupRepo.getAll(pagination);
}
getContactById(jid) {
@@ -1,9 +1,19 @@
import { LocalStore } from '@waha/core/storage/LocalStore';
import { DataStore } from '../../../abc/DataStore';
import { INowebStorage } from './INowebStorage';
import { Sqlite3Storage } from './sqlite3/Sqlite3Storage';
export class NowebStorageFactoryCore {
createStorage(store: DataStore, name: string): INowebStorage {
return new Sqlite3Storage(':memory:');
if (store instanceof LocalStore) {
return this.buildStorageSqlite3(store, name);
}
throw new Error(`Unsupported store type '${store.constructor.name}'`);
}
private buildStorageSqlite3(store: LocalStore, name: string) {
const filePath = store.getFilePath(name, 'store.sqlite3');
return new Sqlite3Storage(filePath);
}
}
-58
View File
@@ -1,58 +0,0 @@
import { Field, Index, Schema } from '@waha/core/storage/sqlite3/Schema';
export const NOWEB_STORE_SCHEMA = [
new Schema(
'contacts',
[new Field('id', 'TEXT'), new Field('data', 'TEXT')],
[new Index('contacts_id_index', ['id'])],
),
new Schema(
'chats',
[
new Field('id', 'TEXT'),
new Field('conversationTimestamp', 'INTEGER'),
new Field('data', 'TEXT'),
],
[
new Index('chats_id_index', ['id']),
new Index('chats_conversationTimestamp_index', ['conversationTimestamp']),
],
),
new Schema(
'messages',
[
new Field('jid', 'TEXT'),
new Field('id', 'TEXT'),
new Field('messageTimestamp', 'INTEGER'),
new Field('data', 'TEXT'),
],
[
new Index('messages_id_index', ['id']),
new Index('messages_jid_id_index', ['jid', 'id']),
new Index('messages_jid_timestamp_index', ['jid', 'messageTimestamp']),
new Index('timestamp_index', ['messageTimestamp']),
],
),
new Schema(
'labels',
[new Field('id', 'TEXT'), new Field('data', 'TEXT')],
[new Index('labels_id_index', ['id'])],
),
new Schema(
'labelAssociations',
[
new Field('id', 'TEXT'),
new Field('type', 'TEXT'),
new Field('labelId', 'TEXT'),
new Field('chatId', 'TEXT'),
new Field('messageId', 'TEXT'),
new Field('data', 'TEXT'),
],
[
new Index('label_assoc_id_index', ['id']),
new Index('label_assoc_type_label_index', ['type', 'labelId']),
new Index('label_assoc_type_chat_index', ['type', 'chatId']),
new Index('label_assoc_type_message_index', ['type', 'messageId']),
],
),
];
+14
View File
@@ -0,0 +1,14 @@
import { WAMessage } from '@adiwajshing/baileys';
import { LabelAssociation } from '@adiwajshing/baileys/lib/Types/LabelAssociation';
export const NowebMessagesMetadata = new Map()
.set('jid', (msg: WAMessage) => msg.key.remoteJid)
.set('id', (msg: WAMessage) => msg.key.id)
.set('messageTimestamp', (msg: WAMessage) => msg.messageTimestamp);
export const NowebLabelAssociationsMetadata = new Map().set(
'id',
(a: LabelAssociation) =>
// @ts-ignore
`${a.type}_${a.labelId}_${a.chatId}_${a.messageId}`,
);
+103
View File
@@ -0,0 +1,103 @@
import { Field, Index, Schema } from '@waha/core/storage/Schema';
export const NowebContactSchema = new Schema(
'contacts',
[new Field('id', 'TEXT'), new Field('data', 'TEXT')],
[new Index('contacts_id_index', ['id'])],
);
export const NowebChatSchema = new Schema(
'chats',
[
new Field('id', 'TEXT'),
new Field('conversationTimestamp', 'INTEGER'),
new Field('data', 'TEXT'),
],
[
new Index('chats_id_index', ['id']),
new Index('chats_conversationTimestamp_index', ['conversationTimestamp']),
],
);
export const NowebGroupsSchema = new Schema(
'groups',
[new Field('id', 'TEXT'), new Field('data', 'TEXT')],
[new Index('groups_id_index', ['id'])],
);
export const NowebMessagesSchema = new Schema(
'messages',
[
new Field('jid', 'TEXT'),
new Field('id', 'TEXT'),
new Field('messageTimestamp', 'INTEGER'),
new Field('data', 'TEXT'),
],
[
new Index('messages_id_index', ['id']),
new Index('messages_jid_id_index', ['jid', 'id']),
new Index('messages_jid_timestamp_index', ['jid', 'messageTimestamp']),
new Index('timestamp_index', ['messageTimestamp']),
],
);
export const NowebLabelsSchema = new Schema(
'labels',
[new Field('id', 'TEXT'), new Field('data', 'TEXT')],
[new Index('labels_id_index', ['id'])],
);
export const NowebLabelAssociationsSchema = new Schema(
'labelAssociations',
[
new Field('id', 'TEXT'),
new Field('type', 'TEXT'),
new Field('labelId', 'TEXT'),
new Field('chatId', 'TEXT'),
new Field('messageId', 'TEXT'),
new Field('data', 'TEXT'),
],
[
new Index('label_assoc_id_index', ['id']),
new Index('label_assoc_type_label_index', ['type', 'labelId']),
new Index('label_assoc_type_chat_index', ['type', 'chatId']),
new Index('label_assoc_type_message_index', ['type', 'messageId']),
],
);
export const NOWEB_STORE_SCHEMA = [
NowebContactSchema,
NowebChatSchema,
NowebGroupsSchema,
NowebMessagesSchema,
NowebLabelsSchema,
NowebLabelAssociationsSchema,
];
export const Migrations: string[] = [
// Contacts
'CREATE TABLE IF NOT EXISTS contacts (id TEXT PRIMARY KEY, data TEXT)',
'CREATE UNIQUE INDEX IF NOT EXISTS contacts_id_index ON contacts (id)',
// Chats
'CREATE TABLE IF NOT EXISTS chats (id TEXT PRIMARY KEY, "conversationTimestamp" INTEGER, data TEXT)',
'CREATE UNIQUE INDEX IF NOT EXISTS chats_id_index ON chats (id)',
'CREATE INDEX IF NOT EXISTS "chats_conversationTimestamp_index" ON chats ("conversationTimestamp")',
// Groups
'CREATE TABLE IF NOT EXISTS groups (id TEXT PRIMARY KEY, data TEXT)',
'CREATE UNIQUE INDEX IF NOT EXISTS groups_id_index ON groups (id)',
// Messages
'CREATE TABLE IF NOT EXISTS messages (jid TEXT, id TEXT, "messageTimestamp" INTEGER, data TEXT)',
'CREATE UNIQUE INDEX IF NOT EXISTS messages_id_index ON messages (id)',
'CREATE INDEX IF NOT EXISTS messages_jid_id_index ON messages (jid, id)',
'CREATE INDEX IF NOT EXISTS messages_jid_timestamp_index ON messages (jid, "messageTimestamp")',
'CREATE INDEX IF NOT EXISTS timestamp_index ON messages ("messageTimestamp")',
// Labels
'CREATE TABLE IF NOT EXISTS labels (id TEXT PRIMARY KEY, data TEXT)',
'CREATE UNIQUE INDEX IF NOT EXISTS labels_id_index ON labels (id)',
// Label associations
'CREATE TABLE IF NOT EXISTS "labelAssociations" (id TEXT PRIMARY KEY, type TEXT, "labelId" TEXT, "chatId" TEXT, "messageId" TEXT, data TEXT)',
'CREATE UNIQUE INDEX IF NOT EXISTS label_assoc_id_index ON "labelAssociations" (id)',
'CREATE INDEX IF NOT EXISTS label_assoc_type_label_index ON "labelAssociations" (type, "labelId")',
'CREATE INDEX IF NOT EXISTS label_assoc_type_chat_index ON "labelAssociations" (type, "chatId")',
'CREATE INDEX IF NOT EXISTS label_assoc_type_message_index ON "labelAssociations" (type, "messageId")',
];
@@ -0,0 +1,23 @@
import { Chat } from '@adiwajshing/baileys';
import { SqlKVRepository } from '@waha/core/storage/sql/SqlKVRepository';
import { PaginationParams } from '@waha/structures/pagination.dto';
export class SqlChatMethods {
constructor(private repository: SqlKVRepository<any>) {}
async getAllWithMessages(
pagination: PaginationParams,
broadcast: boolean,
): Promise<Chat[]> {
// Get chats with conversationTimestamp is not Null
let query = this.repository.select().whereNotNull('conversationTimestamp');
if (!broadcast) {
// filter out chat by id if it ends at @newsletter or @broadcast
query = query
.andWhereNot('id', 'like', '%@broadcast')
.andWhereNot('id', 'like', '%@newsletter');
}
query = this.repository.pagination(query, pagination);
return await this.repository.all(query);
}
}
@@ -0,0 +1,40 @@
import {
LabelAssociation,
LabelAssociationType,
} from '@adiwajshing/baileys/lib/Types/LabelAssociation';
import { SqlKVRepository } from '@waha/core/storage/sql/SqlKVRepository';
export class SqlLabelAssociationsMethods {
constructor(private repository: SqlKVRepository<any>) {}
async deleteOne(association: LabelAssociation): Promise<void> {
await this.repository.deleteBy({
type: association.type,
chatId: association.chatId,
labelId: association.labelId,
// @ts-ignore: messageId doesn't existing in ChatLabelAssociation
messageId: association.messageId || null,
});
}
async deleteByLabelId(labelId: string): Promise<void> {
await this.repository.deleteBy({ labelId: labelId });
}
getAssociationsByLabelId(
labelId: string,
type: LabelAssociationType,
): Promise<LabelAssociation[]> {
return this.repository.getAllBy({
type: type,
labelId: labelId,
});
}
getAssociationsByChatId(chatId: string): Promise<LabelAssociation[]> {
return this.repository.getAllBy({
chatId: chatId,
type: LabelAssociationType.Chat,
});
}
}
@@ -0,0 +1,73 @@
import { ALL_JID } from '@waha/core/engines/noweb/session.noweb.core';
import { SqlKVRepository } from '@waha/core/storage/sql/SqlKVRepository';
import { GetChatMessagesFilter } from '@waha/structures/chats.dto';
import { PaginationParams } from '@waha/structures/pagination.dto';
export class SqlMessagesMethods {
constructor(private repository: SqlKVRepository<any>) {}
upsert(messages: any[]): Promise<void> {
return this.repository.upsertMany(messages);
}
async getAllByJid(
jid: string,
filter: GetChatMessagesFilter,
pagination: PaginationParams,
): Promise<any[]> {
let query = this.repository.select();
if (jid !== ALL_JID) {
query = this.repository.select().where({ jid: jid });
}
if (filter['filter.timestamp.lte'] != null) {
query = query.where(
'messageTimestamp',
'<=',
filter['filter.timestamp.lte'],
);
}
if (filter['filter.timestamp.gte'] != null) {
query = query.where(
'messageTimestamp',
'>=',
filter['filter.timestamp.gte'],
);
}
if (filter['filter.fromMe'] != null) {
// filter by data json inside
const sql = this.repository.filterJson('data', 'key.fromMe');
query = query.whereRaw(sql, [filter['filter.fromMe'] ? 'true' : 'false']);
}
query = this.repository.pagination(query, pagination);
return this.repository.all(query);
}
async getByJidById(jid: string, id: string): Promise<any> {
return this.repository.getBy({ jid: jid, id: id });
}
async updateByJidAndId(
jid: string,
id: string,
update: any,
): Promise<boolean> {
const entity = await this.getByJidById(jid, id);
if (!entity) {
return false;
}
Object.assign(entity, update);
await this.repository.upsertOne(entity);
}
async deleteByJidByIds(jid: string, ids: string[]): Promise<void> {
const query = `DELETE
FROM "${this.repository.table}"
WHERE jid = ?
AND id IN (${ids.map(() => '?').join(', ')})`;
await this.repository.raw(query, [jid, ...ids]);
}
deleteAllByJid(jid: string): Promise<void> {
return this.repository.deleteBy({ jid: jid });
}
}
@@ -1,4 +1,6 @@
import { Chat } from '@adiwajshing/baileys';
import { NowebChatSchema } from '@waha/core/engines/noweb/store/schemas';
import { SqlChatMethods } from '@waha/core/engines/noweb/store/sql/SqlChatMethods';
import { PaginationParams } from '@waha/structures/pagination.dto';
import { KnexPaginator } from '@waha/utils/Paginator';
@@ -15,10 +17,18 @@ export class Sqlite3ChatRepository
{
protected Paginator = ChatPaginator;
async getAllWithMessages(pagination: PaginationParams): Promise<Chat[]> {
// Get chats with conversationTimestamp is not Null
let query = this.select().whereNotNull('conversationTimestamp');
query = this.pagination(query, pagination);
return await this.all(query);
get schema() {
return NowebChatSchema;
}
get methods() {
return new SqlChatMethods(this);
}
getAllWithMessages(
pagination: PaginationParams,
broadcast: boolean,
): Promise<Chat[]> {
return this.methods.getAllWithMessages(pagination, broadcast);
}
}
@@ -1,4 +1,5 @@
import { Contact } from '@adiwajshing/baileys';
import { NowebContactSchema } from '@waha/core/engines/noweb/store/schemas';
import { KnexPaginator } from '@waha/utils/Paginator';
import { IContactRepository } from '../IContactRepository';
@@ -13,4 +14,8 @@ export class Sqlite3ContactRepository
implements IContactRepository
{
protected Paginator = ContactPaginator;
get schema() {
return NowebContactSchema;
}
}
@@ -0,0 +1,21 @@
import { GroupMetadata } from '@adiwajshing/baileys/lib/Types/GroupMetadata';
import { IGroupRepository } from '@waha/core/engines/noweb/store/IGroupRepository';
import { NowebGroupsSchema } from '@waha/core/engines/noweb/store/schemas';
import { KnexPaginator } from '@waha/utils/Paginator';
import { NOWEBSqlite3KVRepository } from './NOWEBSqlite3KVRepository';
class Paginator extends KnexPaginator {
indexes = ['id'];
}
export class Sqlite3GroupRepository
extends NOWEBSqlite3KVRepository<GroupMetadata>
implements IGroupRepository
{
protected Paginator = Paginator;
get schema() {
return NowebGroupsSchema;
}
}
@@ -3,6 +3,9 @@ import {
LabelAssociationType,
} from '@adiwajshing/baileys/lib/Types/LabelAssociation';
import { ILabelAssociationRepository } from '@waha/core/engines/noweb/store/ILabelAssociationsRepository';
import { NowebLabelAssociationsMetadata } from '@waha/core/engines/noweb/store/metadata';
import { NowebLabelAssociationsSchema } from '@waha/core/engines/noweb/store/schemas';
import { SqlLabelAssociationsMethods } from '@waha/core/engines/noweb/store/sql/SqlLabelAssociationsMethods';
import { NOWEBSqlite3KVRepository } from './NOWEBSqlite3KVRepository';
@@ -10,31 +13,34 @@ export class Sqlite3LabelAssociationsRepository
extends NOWEBSqlite3KVRepository<LabelAssociation>
implements ILabelAssociationRepository
{
get schema() {
return NowebLabelAssociationsSchema;
}
get metadata() {
return NowebLabelAssociationsMetadata;
}
get methods() {
return new SqlLabelAssociationsMethods(this);
}
async deleteOne(association: LabelAssociation): Promise<void> {
await this.deleteBy({
type: association.type,
chatId: association.chatId,
labelId: association.labelId,
// @ts-ignore: messageId doesn't existing in ChatLabelAssociation
messageId: association.messageId || null,
});
return this.methods.deleteOne(association);
}
async deleteByLabelId(labelId: string): Promise<void> {
await this.deleteBy({ labelId: labelId });
return this.methods.deleteByLabelId(labelId);
}
getAssociationsByLabelId(
labelId: string,
type: LabelAssociationType,
): Promise<LabelAssociation[]> {
return this.getAllBy({
type: type,
labelId: labelId,
});
return this.methods.getAssociationsByLabelId(labelId, type);
}
getAssociationsByChatId(chatId: string): Promise<LabelAssociation[]> {
return this.getAllBy({ chatId: chatId, type: LabelAssociationType.Chat });
return this.methods.getAssociationsByChatId(chatId);
}
}
@@ -1,8 +1,14 @@
import { Label } from '@adiwajshing/baileys/lib/Types/Label';
import { ILabelsRepository } from '@waha/core/engines/noweb/store/ILabelsRepository';
import { NowebLabelsSchema } from '@waha/core/engines/noweb/store/schemas';
import { NOWEBSqlite3KVRepository } from './NOWEBSqlite3KVRepository';
export class Sqlite3LabelsRepository
extends NOWEBSqlite3KVRepository<Label>
implements ILabelsRepository {}
implements ILabelsRepository
{
get schema() {
return NowebLabelsSchema;
}
}
@@ -1,4 +1,6 @@
import { ALL_JID } from '@waha/core/engines/noweb/session.noweb.core';
import { NowebMessagesMetadata } from '@waha/core/engines/noweb/store/metadata';
import { NowebMessagesSchema } from '@waha/core/engines/noweb/store/schemas';
import { SqlMessagesMethods } from '@waha/core/engines/noweb/store/sql/SqlMessagesMethods';
import { GetChatMessagesFilter } from '@waha/structures/chats.dto';
import { PaginationParams } from '@waha/structures/pagination.dto';
@@ -9,8 +11,20 @@ export class Sqlite3MessagesRepository
extends NOWEBSqlite3KVRepository<any>
implements IMessagesRepository
{
get schema() {
return NowebMessagesSchema;
}
get methods() {
return new SqlMessagesMethods(this);
}
get metadata() {
return NowebMessagesMetadata;
}
upsert(messages: any[]): Promise<void> {
return this.upsertMany(messages);
return this.methods.upsert(messages);
}
async getAllByJid(
@@ -18,36 +32,11 @@ export class Sqlite3MessagesRepository
filter: GetChatMessagesFilter,
pagination: PaginationParams,
): Promise<any[]> {
let query = this.select();
if (jid !== ALL_JID) {
query = this.select().where({ jid: jid });
}
if (filter['filter.timestamp.lte'] != null) {
query = query.where(
'messageTimestamp',
'<=',
filter['filter.timestamp.lte'],
);
}
if (filter['filter.timestamp.gte'] != null) {
query = query.where(
'messageTimestamp',
'>=',
filter['filter.timestamp.gte'],
);
}
if (filter['filter.fromMe'] != null) {
// filter by data->"$.key.fromMe"
query = query.whereRaw("data->'$.key.fromMe' = ?", [
filter['filter.fromMe'] ? 'true' : 'false',
]);
}
query = this.pagination(query, pagination);
return this.all(query);
return this.methods.getAllByJid(jid, filter, pagination);
}
async getByJidById(jid: string, id: string): Promise<any> {
return this.getBy({ jid: jid, id: id });
return this.methods.getByJidById(jid, id);
}
async updateByJidAndId(
@@ -55,23 +44,14 @@ export class Sqlite3MessagesRepository
id: string,
update: any,
): Promise<boolean> {
const entity = await this.getByJidById(jid, id);
if (!entity) {
return false;
}
Object.assign(entity, update);
await this.upsertOne(entity);
return this.methods.updateByJidAndId(jid, id, update);
}
async deleteByJidByIds(jid: string, ids: string[]): Promise<void> {
const query = `DELETE
FROM ${this.table}
WHERE jid = ?
AND id IN (${ids.map(() => '?').join(', ')})`;
this.db.prepare(query).run([jid, ...ids]);
return this.methods.deleteByJidByIds(jid, ids);
}
deleteAllByJid(jid: string): Promise<void> {
return this.deleteBy({ jid: jid });
return this.methods.deleteAllByJid(jid);
}
}
@@ -1,4 +1,4 @@
import { Schema } from '@waha/core/storage/sqlite3/Schema';
import { Schema } from '@waha/core/storage/Schema';
// eslint-disable-next-line @typescript-eslint/no-var-requires
const Database = require('better-sqlite3');
@@ -1,13 +1,12 @@
import { WAMessage } from '@adiwajshing/baileys';
import { LabelAssociation } from '@adiwajshing/baileys/lib/Types/LabelAssociation';
import { ILabelAssociationRepository } from '@waha/core/engines/noweb/store/ILabelAssociationsRepository';
import { ILabelsRepository } from '@waha/core/engines/noweb/store/ILabelsRepository';
import { Sqlite3GroupRepository } from '@waha/core/engines/noweb/store/sqlite3/Sqlite3GroupRepository';
import { Sqlite3LabelAssociationsRepository } from '@waha/core/engines/noweb/store/sqlite3/Sqlite3LabelAssociationsRepository';
import { Sqlite3LabelsRepository } from '@waha/core/engines/noweb/store/sqlite3/Sqlite3LabelsRepository';
import { Field, Index, Schema } from '@waha/core/storage/sqlite3/Schema';
import { Schema } from '@waha/core/storage/Schema';
import { INowebStorage } from '../INowebStorage';
import { NOWEB_STORE_SCHEMA } from '../Schema';
import { Migrations, NOWEB_STORE_SCHEMA } from '../schemas';
import { Sqlite3ChatRepository } from './Sqlite3ChatRepository';
import { Sqlite3ContactRepository } from './Sqlite3ContactRepository';
import { Sqlite3MessagesRepository } from './Sqlite3MessagesRepository';
@@ -43,109 +42,36 @@ export class Sqlite3Storage extends INowebStorage {
}
private migration0001init() {
// Contacts
this.db.exec(
'CREATE TABLE IF NOT EXISTS contacts (id TEXT PRIMARY KEY, data TEXT)',
);
this.db.exec(
'CREATE UNIQUE INDEX IF NOT EXISTS contacts_id_index ON contacts (id)',
);
// Chats
this.db.exec(
'CREATE TABLE IF NOT EXISTS chats (id TEXT PRIMARY KEY, conversationTimestamp INTEGER, data TEXT)',
);
this.db.exec(
'CREATE UNIQUE INDEX IF NOT EXISTS chats_id_index ON chats (id)',
);
this.db.exec(
'CREATE INDEX IF NOT EXISTS chats_conversationTimestamp_index ON chats (conversationTimestamp)',
);
// Messages
this.db.exec(
'CREATE TABLE IF NOT EXISTS messages (jid TEXT, id TEXT, messageTimestamp INTEGER, data TEXT)',
);
this.db.exec(
'CREATE UNIQUE INDEX IF NOT EXISTS messages_id_index ON messages (id)',
);
this.db.exec(
'CREATE INDEX IF NOT EXISTS messages_jid_id_index ON messages (jid, id)',
);
this.db.exec(
'CREATE INDEX IF NOT EXISTS messages_jid_timestamp_index ON messages (jid, messageTimestamp)',
);
this.db.exec(
'CREATE INDEX IF NOT EXISTS timestamp_index ON messages (messageTimestamp)',
);
//
// Labels
//
this.db.exec(
'CREATE TABLE IF NOT EXISTS labels (id TEXT PRIMARY KEY, data TEXT)',
);
this.db.exec(
'CREATE UNIQUE INDEX IF NOT EXISTS labels_id_index ON labels (id)',
);
// Label associations
this.db.exec(
'CREATE TABLE IF NOT EXISTS labelAssociations (id TEXT PRIMARY KEY, type TEXT, labelId TEXT, chatId TEXT, messageId TEXT, data TEXT)',
);
this.db.exec(
'CREATE UNIQUE INDEX IF NOT EXISTS label_assoc_id_index ON labelAssociations (id)',
);
this.db.exec(
'CREATE INDEX IF NOT EXISTS label_assoc_type_label_index ON labelAssociations (type, labelId)',
);
this.db.exec(
'CREATE INDEX IF NOT EXISTS label_assoc_type_chat_index ON labelAssociations (type, chatId)',
);
this.db.exec(
'CREATE INDEX IF NOT EXISTS label_assoc_type_message_index ON labelAssociations (type, messageId)',
);
for (const migration of Migrations) {
this.db.exec(migration);
}
}
async close() {
this.db.close();
return this.db.close();
}
getContactsRepository() {
return new Sqlite3ContactRepository(this.db, this.getSchema('contacts'));
return new Sqlite3ContactRepository(this.db);
}
getChatRepository() {
return new Sqlite3ChatRepository(this.db, this.getSchema('chats'));
return new Sqlite3ChatRepository(this.db);
}
getGroupRepository() {
return new Sqlite3GroupRepository(this.db);
}
getLabelsRepository(): ILabelsRepository {
return new Sqlite3LabelsRepository(this.db, this.getSchema('labels'));
return new Sqlite3LabelsRepository(this.db);
}
getLabelAssociationRepository(): ILabelAssociationRepository {
const metadata = this.getLabelAssociationMetadata();
return new Sqlite3LabelAssociationsRepository(
this.db,
this.getSchema('labelAssociations'),
metadata,
);
return new Sqlite3LabelAssociationsRepository(this.db);
}
getMessagesRepository() {
const metadata = this.getMessagesMetadata();
return new Sqlite3MessagesRepository(
this.db,
this.getSchema('messages'),
metadata,
);
}
getSchema(name: string) {
const schema = this.tables.find((table) => table.name === name);
if (!schema) {
throw new Error(`Schema not found: ${name}`);
}
return schema;
return new Sqlite3MessagesRepository(this.db);
}
}
@@ -4,9 +4,12 @@ import {
proto,
} from '@adiwajshing/baileys';
import { BufferJSON, initAuthCreds } from '@adiwajshing/baileys/lib/Utils';
import { mkdir, readFile, stat, unlink, writeFile } from 'fs/promises';
import { mkdir, readFile, stat, unlink } from 'fs/promises';
import { join } from 'path';
// eslint-disable-next-line @typescript-eslint/no-var-requires
const writeFileAtomic = require('write-file-atomic');
// eslint-disable-next-line @typescript-eslint/no-var-requires
const AsyncLock = require('async-lock');
@@ -29,11 +32,15 @@ export const useMultiFileAuthState = async (
): Promise<{
state: AuthenticationState;
saveCreds: () => Promise<void>;
close: () => Promise<void>;
}> => {
const writeData = (data: any, file: string) => {
const filePath = join(folder, fixFileName(file));
return fileLock.acquire(filePath, () =>
writeFile(join(filePath), JSON.stringify(data, BufferJSON.replacer)),
writeFileAtomic(
join(filePath),
JSON.stringify(data, BufferJSON.replacer),
),
);
};
@@ -111,5 +118,8 @@ export const useMultiFileAuthState = async (
saveCreds: () => {
return writeData(creds, 'creds.json');
},
close: async () => {
return;
},
};
};
+9 -1
View File
@@ -1,7 +1,7 @@
import { extractMessageContent, proto } from '@adiwajshing/baileys';
export function extractMediaContent(
content: proto.IMessage | null | undefined,
content: any | proto.IMessage | null | undefined,
) {
content = extractMessageContent(content);
const mediaContent =
@@ -78,6 +78,14 @@ const isObjectALong = (value: any): value is Long => {
);
};
export function ensureNumber(value: number | Long): number {
if (!value) {
// @ts-ignore
return value;
}
return typeof value === 'number' ? value : toNumber(value);
}
const toNumber = (longValue: Long): number => {
const { low, high, unsigned } = longValue;
const result = unsigned ? low >>> 0 : low + high * 0x100000000;
@@ -0,0 +1,52 @@
<!DOCTYPE html><html id="whatsapp-web" class="" lang="en" dir="ltr"><head><link data-default-icon="https://static.whatsapp.net/rsrc.php/v3/yP/r/rYZqPCBaG70.png" rel="shortcut icon" href="https://static.whatsapp.net/rsrc.php/v3/yP/r/rYZqPCBaG70.png" /><meta name="bingbot" content="noarchive" /><meta name="viewport" content="width=device-width,initial-scale=1,maximum-scale=2,shrink-to-fit=no" /><link rel="manifest" href="/data/manifest.json" crossorigin="use-credentials" /><meta name="google" content="notranslate" /><meta name="format-detection" content="telephone=no" /><meta name="description" content="Quickly send and receive WhatsApp messages right from your computer." /><meta name="og:description" content="Quickly send and receive WhatsApp messages right from your computer." /><meta name="og:url" content="https://web.whatsapp.com/" /><meta name="og:title" content="WhatsApp Web" /><meta name="og:image" content="https://static.whatsapp.net/rsrc.php/v3/yR/r/y8-PTBaP90a.png" /><meta name="theme-color" content="#111b21" media="(prefers-color-scheme: dark)" /><meta name="theme-color" content="#f0f2f5" /><link rel="apple-touch-icon" sizes="192x192" href="https://static.whatsapp.net/rsrc.php/v3/yc/r/hUUuVTz6ZVi.png" /><style data-btmanifest="1018072227_main" nonce="o8S8zE9I">:root,.__fb-light-mode:root,.__fb-light-mode{--splashscreen-primary-title:#4125d;--splashscreen-progress-background:#e9edef;--splashscreen-progress-primary:#00c298;--splashscreen-secondary-lighter:#8696a0;--splashscreen-startup-background:#f0f2f5;--splashscreen-startup-background-rgb:240, 242, 245;--splashscreen-startup-icon:#bbc5cb}</style><style data-btmanifest="1018072227_main" nonce="o8S8zE9I">.__fb-dark-mode{--splashscreen-primary-title:rgba(233, 237, 239, .88);--splashscreen-progress-background:#233138;--splashscreen-progress-primary:#0b846d;--splashscreen-secondary-lighter:#667781;--splashscreen-startup-background:#111b21;--splashscreen-startup-background-rgb:17, 27, 33;--splashscreen-startup-icon:#676f73}@media (prefers-color-scheme: dark){:root{--splashscreen-primary-title:rgba(233, 237, 239, .88);--splashscreen-progress-background:#233138;--splashscreen-progress-primary:#0b846d;--splashscreen-secondary-lighter:#667781;--splashscreen-startup-background:#111b21;--splashscreen-startup-background-rgb:17, 27, 33;--splashscreen-startup-icon:#676f73}}</style><script id="envjson" type="application/json" nonce="o8S8zE9I">{"useTrustedTypes":false,"isTrustedTypesReportOnly":false,"use_fbt_virtual_modules":true,"compat_iframe_token":null,"clear_js_factory_after_used":true,"brsid":"7435115086220858941"}</script><script src="data:application/x-javascript; charset=utf-8;base64,Oy8qRkJfUEtHX0RFTElNKi8KCnZhciBkYXRhRWxlbWVudD1kb2N1bWVudC5nZXRFbGVtZW50QnlJZCgiZW52anNvbiIpO2lmKGRhdGFFbGVtZW50IT1udWxsKXt2YXIgY29weVZhcmlhYmxlcz1mdW5jdGlvbihhKXtmb3IodmFyIGIgaW4gdmFyaWFibGVzKWFbYl09dmFyaWFibGVzW2JdfSx2YXJpYWJsZXM9SlNPTi5wYXJzZShkYXRhRWxlbWVudC50ZXh0Q29udGVudCk7d2luZG93LnJlcXVpcmVMYXp5P3dpbmRvdy5yZXF1aXJlTGF6eShbIkVudiJdLGNvcHlWYXJpYWJsZXMpOih3aW5kb3cuRW52PXdpbmRvdy5FbnZ8fHt9LGNvcHlWYXJpYWJsZXMod2luZG93LkVudikpfQovLyMgc291cmNlVVJMPWh0dHBzOi8vc3RhdGljLndoYXRzYXBwLm5ldC9yc3JjLnBocC92My95TC9yL2RiZ3JNc25EMThwLmpzCg==" data-bootloader-hash="nxRKQO6" crossorigin="anonymous" data-p=":0" data-btmanifest="1018072227_main" data-c="1"></script><script src="data:application/x-javascript; charset=utf-8;base64,Oy8qRkJfUEtHX0RFTElNKi8KCl9fYW5ub3RhdG9yPWZ1bmN0aW9uKGEpe3JldHVybiBhfSxfX2Rfc3R1Yj1bXSxfX2Q9ZnVuY3Rpb24oYSxiLGMsZCl7X19kX3N0dWIucHVzaChbYSxiLGMsZF0pfSxfX3JsX3N0dWI9W10scmVxdWlyZUxhenk9ZnVuY3Rpb24oKXtfX3JsX3N0dWIucHVzaChhcmd1bWVudHMpfTsKLy8jIHNvdXJjZVVSTD1odHRwczovL3N0YXRpYy53aGF0c2FwcC5uZXQvcnNyYy5waHAvdjMveTIvci9jTWRfU1pvdjlFaS5qcwo=" data-bootloader-hash="W7CcKyc" crossorigin="anonymous" data-p=":0" data-btmanifest="1018072227_main" data-c="1"></script><script src="data:application/x-javascript; charset=utf-8;base64,Oy8qRkJfUEtHX0RFTElNKi8KCl9idGxkcj17fTsKLy8jIHNvdXJjZVVSTD1odHRwczovL3N0YXRpYy53aGF0c2FwcC5uZXQvcnNyYy5waHAvdjMveUgvci9aTGNLT2JOdU8xYi5qcwo=" data-bootloader-hash="678jA6i" crossorigin="anonymous" data-p=":0" data-btmanifest="1018072227_main" data-c="1"></script><script src="data:application/x-javascript; charset=utf-8;base64,Oy8qRkJfUEtHX0RFTElNKi8KCndpbmRvdy5vcGVuRGF0YWJhc2UmJih3aW5kb3cub3BlbkRhdGFiYXNlPWZ1bmN0aW9uKCl7dGhyb3cgbmV3IEVycm9yKCl9KTsKLy8jIHNvdXJjZVVSTD1odHRwczovL3N0YXRpYy53aGF0c2FwcC5uZXQvcnNyYy5waHAvdjMveVkvci85Qk95ZURLTC1kQy5qcwo=" data-bootloader-hash="k6/EjOG" crossorigin="anonymous" data-p=":0" data-btmanifest="1018072227_main" data-c="1"></script><script src="data:application/x-javascript; charset=utf-8;base64,Oy8qRkJfUEtHX0RFTElNKi8KCmZ1bmN0aW9uIHBhcmVudElzTm90SGVhZE5vckJvZHkoYSl7cmV0dXJuIGEucGFyZW50RWxlbWVudCE9PWRvY3VtZW50LmJvZHkmJmEucGFyZW50RWxlbWVudCE9PWRvY3VtZW50LmhlYWR9ZnVuY3Rpb24gaXNUYWdTdXBwb3J0ZWQoYSl7cmV0dXJuIGEubm9kZU5hbWU9PT0iU0NSSVBUInx8YS5ub2RlTmFtZT09PSJMSU5LIiYmKChhPWdldE5vZGVEYXRhU2V0KGEpKT09bnVsbD92b2lkIDA6YS5hc3Line truncated
<script type="application/json" data-content-len="92" data-sjs>{"require":[["qplTimingsServerJS",null,null,["7435115086220858941-server","htmlStart",23]]]}</script>
<link type="text/css" rel="stylesheet" href="https://static.whatsapp.net/rsrc.php/v4/y4/l/0,cross/nJSuJRxe0LRIuQQZ3xdyKtes83aNhixgx.css" data-bootloader-hash="L0/3ndb" crossorigin="anonymous" data-p=":3,27,19" data-btmanifest="1018072227_main" data-c="1" />
<link rel="preload" href="https://static.whatsapp.net/rsrc.php/v3/yl/r/HOQztTu5JYT.js" as="script" crossorigin="anonymous" nonce="o8S8zE9I" /><script src="https://static.whatsapp.net/rsrc.php/v3/yl/r/HOQztTu5JYT.js" data-bootloader-hash="A8pgoR4" async="1" crossorigin="anonymous" data-p=":1" data-btmanifest="1018072227_main" data-c="1"></script>
<link rel="preload" href="https://static.whatsapp.net/rsrc.php/v3/yw/r/dYGBdB10S1Z.js" as="script" crossorigin="anonymous" nonce="o8S8zE9I" /><script src="https://static.whatsapp.net/rsrc.php/v3/yw/r/dYGBdB10S1Z.js" data-bootloader-hash="Bbw1TQP" async="1" crossorigin="anonymous" data-p=":2" data-btmanifest="1018072227_main" data-c="1"></script>
<link rel="preload" href="https://static.whatsapp.net/rsrc.php/v3/yS/r/UhwbwVdbQX0.js" as="script" crossorigin="anonymous" nonce="o8S8zE9I" /><script src="https://static.whatsapp.net/rsrc.php/v3/yS/r/UhwbwVdbQX0.js" data-bootloader-hash="F/l6arV" async="1" crossorigin="anonymous" data-p=":14" data-btmanifest="1018072227_main" data-c="1"></script>
<link rel="preload" href="https://static.whatsapp.net/rsrc.php/v3/yx/r/ryQSgyNOW8X.js" as="script" crossorigin="anonymous" nonce="o8S8zE9I" /><script src="https://static.whatsapp.net/rsrc.php/v3/yx/r/ryQSgyNOW8X.js" data-bootloader-hash="krMlcAs" async="1" crossorigin="anonymous" data-p=":8" data-btmanifest="1018072227_main" data-c="1"></script>
<link rel="preload" href="https://static.whatsapp.net/rsrc.php/v3iWBv4/yk/l/rt/IN3s4Kru2-t2RSfFSjWLjPTFOQq3__cln_2SsOE-ssqJx0Eef7DOYopYnK0rykQBR7vsWqehdEtWw.js" as="script" crossorigin="anonymous" nonce="o8S8zE9I" /><script src="https://static.whatsapp.net/rsrc.php/v3iWBv4/yk/l/rt/IN3s4Kru2-t2RSfFSjWLjPTFOQq3__cln_2SsOE-ssqJx0Eef7DOYopYnK0rykQBR7vsWqehdEtWw.js" data-bootloader-hash="U5nfRMR" async="1" crossorigin="anonymous" data-tsrc="https://static.whatsapp.net/rsrc-translations.php/v7iWBv4/yV/l/en_US/IN3s4Kru2-t2RSfFSjWLjPTFOQq3__cln_2SsOE-ssqJx0Eef7DOYopYnK0rykQBR7vsWqehdEtWw.js" data-p=":25,5,13,23,20,22,7" data-btmanifest="1018072227_main" data-c="1"></script><link rel="preload" href="https://static.whatsapp.net/rsrc-translations.php/v7iWBv4/yV/l/en_US/IN3s4Kru2-t2RSfFSjWLjPTFOQq3__cln_2SsOE-ssqJx0Eef7DOYopYnK0rykQBR7vsWqehdEtWw.js" as="fetch" crossorigin="anonymous" />
<link rel="preload" href="https://static.whatsapp.net/rsrc.php/v3iHCc4/yu/l/rt/rW3lOI79YLL.js" as="script" crossorigin="anonymous" nonce="o8S8zE9I" /><script src="https://static.whatsapp.net/rsrc.php/v3iHCc4/yu/l/rt/rW3lOI79YLL.js" data-bootloader-hash="Bv8Xq6G" async="1" crossorigin="anonymous" data-tsrc="https://static.whatsapp.net/rsrc-translations.php/v7iHCc4/ys/l/en_US/rW3lOI79YLL.js" data-p=":16" data-btmanifest="1018072227_main" data-c="1"></script><link rel="preload" href="https://static.whatsapp.net/rsrc-translations.php/v7iHCc4/ys/l/en_US/rW3lOI79YLL.js" as="fetch" crossorigin="anonymous" />
<link rel="preload" href="https://static.whatsapp.net/rsrc.php/v3/yR/r/h4Qy0FiYsZ_.js" as="script" crossorigin="anonymous" nonce="o8S8zE9I" /><script src="https://static.whatsapp.net/rsrc.php/v3/yR/r/h4Qy0FiYsZ_.js" data-bootloader-hash="bUA3nqa" async="1" crossorigin="anonymous" data-p=":4" data-btmanifest="1018072227_main" data-c="1"></script>
<link rel="preload" href="https://static.whatsapp.net/rsrc.php/v3iuY-4/ym/l/rt/yTGZCBMSfU8.js" as="script" crossorigin="anonymous" nonce="o8S8zE9I" /><script src="https://static.whatsapp.net/rsrc.php/v3iuY-4/ym/l/rt/yTGZCBMSfU8.js" data-bootloader-hash="VS6S+f6" async="1" crossorigin="anonymous" data-tsrc="https://static.whatsapp.net/rsrc-translations.php/v7iuY-4/ye/l/en_US/yTGZCBMSfU8.js" data-p=":9" data-btmanifest="1018072227_main" data-c="1"></script><link rel="preload" href="https://static.whatsapp.net/rsrc-translations.php/v7iuY-4/ye/l/en_US/yTGZCBMSfU8.js" as="fetch" crossorigin="anonymous" />
<link rel="preload" href="https://static.whatsapp.net/rsrc.php/v3/y6/r/OicDJTSlcdh.js" as="script" crossorigin="anonymous" nonce="o8S8zE9I" /><script src="https://static.whatsapp.net/rsrc.php/v3/y6/r/OicDJTSlcdh.js" data-bootloader-hash="hER3bup" async="1" crossorigin="anonymous" data-p=":6" data-btmanifest="1018072227_main" data-c="1"></script>
<script type="application/json" data-content-len="459" data-sjs>{"require":[["MakeHasteTranslations","fetchTranslationsForEarlyFlush",null,[{"U5nfRMR":"https:\/\/static.whatsapp.net\/rsrc-translations.php\/v7iWBv4\/yV\/l\/en_US\/IN3s4Kru2-t2RSfFSjWLjPTFOQq3__cln_2SsOE-ssqJx0Eef7DOYopYnK0rykQBR7vsWqehdEtWw.js","Bv8Xq6G":"https:\/\/static.whatsapp.net\/rsrc-translations.php\/v7iHCc4\/ys\/l\/en_US\/rW3lOI79YLL.js","VS6S+f6":"https:\/\/static.whatsapp.net\/rsrc-translations.php\/v7iuY-4\/ye\/l\/en_US\/yTGZCBMSfU8.js"}]]]}</script>
<!--EF-->
<title>WhatsApp Web</title>
<script type="application/json" data-content-len="1247" data-sjs>{"require":[["HasteSupportData","handle",null,[{"gkxData":{"2199":{"result":false,"hash":null},"7742":{"result":false,"hash":null},"8068":{"result":true,"hash":null},"20935":{"result":true,"hash":null},"20936":{"result":false,"hash":null},"1393":{"result":false,"hash":null},"3485":{"result":false,"hash":null},"5918":{"result":false,"hash":null},"5971":{"result":false,"hash":null},"7686":{"result":false,"hash":null},"7687":{"result":true,"hash":null},"21050":{"result":false,"hash":null},"21075":{"result":false,"hash":null},"21076":{"result":true,"hash":null},"22792":{"result":false,"hash":null},"23433":{"result":false,"hash":null},"4341":{"result":false,"hash":null},"6323":{"result":false,"hash":null},"8523":{"result":false,"hash":null},"9861":{"result":false,"hash":null},"21062":{"result":false,"hash":null},"21063":{"result":false,"hash":null},"21069":{"result":false,"hash":null},"21071":{"result":false,"hash":null},"21072":{"result":false,"hash":null},"33056":{"result":false,"hash":null}},"qexData":{"526":{"r":null},"538":{"r":null},"543":{"r":null},"362":{"r":null},"1028":{"r":null},"104":{"r":null},"128":{"r":null},"344":{"r":null},"388":{"r":null}},"justknobxData":{"3323":{"r":true},"1806":{"r":true},"2819":{"r":true}}}]]]}</script>
</head><body class=""><div id="wa_web_initial_startup" class="_apdl"><div style="transform: translate(calc(50% - 52px / 2))" class="_apdm"><span><svg width="250" height="52" xmlns="http://www.w3.org/2000/svg"><path d="M37.7 31.2c-.6-.4-3.8-2-4.4-2.1-.6-.2-1-.4-1.4.3l-2 2.5c-.4.4-.8.5-1.5.2-.6-.3-2.7-1-5.1-3.2-2-1.7-3.2-3.8-3.6-4.5-.4-.6 0-1 .3-1.3l1-1.1.6-1.1c.2-.4 0-.8 0-1.1l-2-4.8c-.6-1.3-1.1-1-1.5-1.1h-1.2c-.5 0-1.2.1-1.8.8-.5.6-2.2 2.2-2.2 5.3 0 3.2 2.3 6.3 2.6 6.7.3.4 4.6 7 11 9.7l3.7 1.4c1.5.5 3 .4 4 .2 1.3-.1 3.9-1.5 4.4-3 .5-1.5.5-2.8.4-3-.2-.4-.6-.5-1.3-.8M26 47.2c-3.9 0-7.6-1-11-3l-.7-.4-8.1 2L8.4 38l-.6-.8A21.4 21.4 0 0126 4.4a21.3 21.3 0 0121.4 21.4c0 11.8-9.6 21.4-21.4 21.4M44.2 7.6a25.8 25.8 0 00-40.6 31L0 52l13.7-3.6A25.8 25.8 0 0044.3 7.5" fill="currentColor"></path></svg></span></div><div class="_apdq"><progress value="0" max="100" dir="ltr"></progress></div><div class="_apdr">WhatsApp</div><div class="_apds"><span><svg width="10" height="12" xmlns="http://www.w3.org/2000/svg"><path d="M5 1.6c1.4 0 2.5 1 2.6 2.4v1.5h.2c.5 0 1 .4 1 1V10c0 .6-.5 1-1 1H2.3a1 1 0 01-1.1-1V6.5c0-.6.5-1 1-1h.2V4.2c0-1.4 1-2.5 2.4-2.6H5zm0 1.2c-.7 0-1.3.6-1.3 1.3v1.4h2.6V4.2c0-.7-.4-1.2-1-1.3H5z" fill="currentColor"></path></svg></span><span> </span>End-to-end encrypted</div></div><script type="application/json" data-content-len="91" data-sjs>{"require":[["qplTimingsServerJS",null,null,["7435115086220858941","splash_screen_show"]]]}</script>
<div id="mount_0_0_kK"></div>
<script type="application/json" data-content-len="53" data-sjs>{"require":[["maybeDisableAnimations",null,null,[]]]}</script>
<script type="application/json" data-content-len="49" data-sjs>{"require":[["replaceNativeTimer",null,null,[]]]}</script>
<script type="application/json" data-content-len="60" data-sjs>{"require":[["bootstrapWebSession",null,null,[1731122631]]]}</script>
<script type="application/json" data-content-len="109" data-sjs>{"require":[["qplTagServerJS",null,null,[["comet_aa_coinflip:false","should_use_bt_safe_js","logged_out"]]]]}</script>
<script type="application/json" data-content-len="157" data-sjs>{"require":[["qplTimingsServerJS",null,null,["7435115086220858941","tierOne"]],["qplTimingsServerJS",null,null,["7435115086220858941-server","tierOne",36]]]}</script>
<script type="application/json" data-content-len="82" data-sjs>{"require":[["JSScheduler","makeSchedulerGlobalEntry",null,["null",false,false]]]}</script>
<script type="application/json" data-content-len="7774" data-sjs>{"require":[["ScheduledServerJS","handle",null,[{"__bbox":{"define":[["cr:310",["RunWWW"],{"__rc":["RunWWW",null]},-1],["cr:1078",[],{"__rc":[null,null]},-1],["cr:1080",["unexpectedUseInComet"],{"__rc":["unexpectedUseInComet",null]},-1],["cr:1126",["TimeSliceSham"],{"__rc":["TimeSliceSham",null]},-1],["cr:3725",["clearTimeoutWWWOrMobile"],{"__rc":["clearTimeoutWWWOrMobile",null]},-1],["cr:4344",["setTimeoutWWWOrMobile"],{"__rc":["setTimeoutWWWOrMobile",null]},-1],["cr:6640",[],{"__rc":[null,null]},-1],["cr:7385",["clearIntervalWWW"],{"__rc":["clearIntervalWWW",null]},-1],["cr:7386",["clearTimeoutWWW"],{"__rc":["clearTimeoutWWW",null]},-1],["cr:7388",["setIntervalWWW"],{"__rc":["setIntervalWWW",null]},-1],["cr:7389",["setIntervalAcrossTransitionsWWW"],{"__rc":["setIntervalAcrossTransitionsWWW",null]},-1],["cr:7390",["setTimeoutWWW"],{"__rc":["setTimeoutWWW",null]},-1],["cr:7391",["setTimeoutAcrossTransitionsWWW"],{"__rc":["setTimeoutAcrossTransitionsWWW",null]},-1],["cr:8959",["DTSG"],{"__rc":["DTSG",null]},-1],["cr:8960",["DTSG_ASYNC"],{"__rc":["DTSG_ASYNC",null]},-1],["cr:696703",["JSScheduler"],{"__rc":["JSScheduler",null]},-1],["cr:806696",["clearTimeoutComet"],{"__rc":["clearTimeoutComet",null]},-1],["cr:807042",["setTimeoutComet"],{"__rc":["setTimeoutComet",null]},-1],["cr:896461",["setIntervalComet"],{"__rc":["setIntervalComet",null]},-1],["cr:896462",["setIntervalComet"],{"__rc":["setIntervalComet",null]},-1],["cr:925100",["RunComet"],{"__rc":["RunComet",null]},-1],["cr:986633",["setTimeoutComet"],{"__rc":["setTimeoutComet",null]},-1],["cr:1003267",["clearIntervalComet"],{"__rc":["clearIntervalComet",null]},-1],["cr:734",[],{"__rc":[null,null]},-1],["cr:1293",["ReactDOM.classic"],{"__rc":["ReactDOM.classic",null]},-1],["cr:4874",[],{"__rc":[null,null]},-1],["cr:7162",["ReactDOMCompatibilityLayer"],{"__rc":["ReactDOMCompatibilityLayer",null]},-1],["cr:7451",[],{"__rc":[null,null]},-1],["cr:1064332",[],{"__rc":[null,null]},-1],["cr:1106516",[],{"__rc":[null,null]},-1],["cr:1108857",[],{"__rc":[null,null]},-1],["cr:1294158",["React.classic"],{"__rc":["React.classic",null]},-1],["cr:1294159",["ReactDOM.classic"],{"__rc":["ReactDOM.classic",null]},-1],["CometPersistQueryParams",[],{"relative":{},"domain":{}},6231],["GetAsyncParamsExtraData",[],{"extra_data":{}},7511],["BootloaderConfig",[],{"deferBootloads":false,"jsRetries":[200,500],"jsRetryAbortNum":2,"jsRetryAbortTime":5,"silentDups":true,"timeout":60000,"tieredLoadingFromTier":100,"hypStep4":true,"phdOn":false,"btCutoffIndex":302,"fastPathForAlreadyRequired":true,"earlyRequireLazy":false,"enableTimeoutLoggingForNonComet":false,"deferLongTailManifest":true,"lazySoT":false,"translationRetries":[200,500],"translationRetryAbortNum":3,"translationRetryAbortTime":50},329],["CSSLoaderConfig",[],{"timeout":5000,"loadEventSupported":true},619],["CurrentUserInitialData",[],{"ACCOUNT_ID":"0","USER_ID":"0","NAME":"","SHORT_NAME":null,"IS_BUSINESS_PERSON_ACCOUNT":false,"HAS_SECONDARY_BUSINESS_PERSON":false,"IS_FACEBOOK_WORK_ACCOUNT":false,"IS_INSTAGRAM_BUSINESS_PERSON":false,"IS_MESSENGER_ONLY_USER":false,"IS_DEACTIVATED_ALLOWED_ON_MESSENGER":false,"IS_MESSENGER_CALL_GUEST_USER":false,"IS_WORK_MESSENGER_CALL_GUEST_USER":false,"IS_WORKROOMS_USER":false,"APP_ID":"2220391788200892","IS_BUSINESS_DOMAIN":false,"NON_FACEBOOK_USER_ID":"0"},270],["DTSGInitialData",[],{},258],["LSD",[],{"token":"wEKbRN9wZlWrDjkzN7hiVV"},323],["ServerNonce",[],{"ServerNonce":"EXAGZxp_fullevDJmcCA-j"},141],["SiteData",[],{"server_revision":1018072227,"client_revision":1018072227,"push_phase":"C3","pkg_cohort":"HYP:whatsapp_comet_pkg","haste_session":"20036.HYP:whatsapp_comet_pkg.2.1..0.1","pr":1,"manifest_base_uri":"https:\/\/static.whatsapp.net","manifest_origin":"whatsapp","manifest_version_prefix":"","be_one_ahead":true,"is_rtl":false,"is_experimental_tier":false,"is_jit_warmed_up":true,"hsi":"7435115086220858941","semr_host_bucket":"5","bl_hash_version":2,"comet_env":17,"wbloks_env":false,"ef_page":"comet.whatsapp.web.\/","compose_bootloads":false,"spin":4,"__spin_r":1018072227,"__spin_b":"trunk","__spin_t":1731122631,"vip":"157.240.229.60"},317],["SprinkleConfig",[],{"param_name":"jazoest","version":2,"should_randomize":false},2111],["PromiseUsePolyfillSetImmediateGK",[],{"www_always_use_polyfill_setimmediate":false},2190],["JSErrorLoggingConfig",[],{"appId":2220391788200892,"extra":[],"reportInterval":50,"sampleWeight":null,"sampleWeightKey":"__jssesw","projectBlocklist":[]},2776],["ImmediateImplementationExperiments",[],{"prefer_message_channel":true},3419],["DTSGInitData",[],{"token":"","async_get_token":""},3515],["UriNeedRawQuerySVConfig",[],{"uris":["dms.netmng.com","doubleclick.net","r.msn.com","watchit.sky.com","graphite.instagram.com","www.kfc.co.th","learn.pantheon.io","www.landmarkshops.in","www.ncl.com","s0.wp.com","www.tatacliq.com","bs.serving-sys.com","kohls.com","lazada.co.th","xg4ken.com","technopark.ruLine truncated
<script type="application/json" data-content-len="83" data-sjs>{"require":[["qplTimingsServerJS",null,null,["7435115086220858941","tierOneEnd"]]]}</script>
<script type="application/json" data-content-len="157" data-sjs>{"require":[["qplTimingsServerJS",null,null,["7435115086220858941","tierTwo"]],["qplTimingsServerJS",null,null,["7435115086220858941-server","tierTwo",80]]]}</script>
<script type="application/json" data-content-len="3594" data-sjs>{"require":[["HasteSupportData","handle",null,[{"bxData":{"9547":{"uri":"https:\/\/static.whatsapp.net\/rsrc.php\/v3\/yl\/r\/gi_DckOUM5a.png"},"9548":{"uri":"https:\/\/static.whatsapp.net\/rsrc.php\/v3\/y_\/r\/TPtFe9SpDSX.png"},"9555":{"uri":"https:\/\/static.whatsapp.net\/rsrc.php\/v3\/y4\/r\/XEPQuYy90FJ.png"},"9556":{"uri":"https:\/\/static.whatsapp.net\/rsrc.php\/v3\/ye\/r\/fWOSgX_xpHG.png"},"9561":{"uri":"https:\/\/static.whatsapp.net\/rsrc.php\/yW\/r\/BS_BUUXbKq5.mp3"},"9562":{"uri":"https:\/\/static.whatsapp.net\/rsrc.php\/yu\/r\/lsK0FC6wi8C.m4a"},"10639":{"uri":"https:\/\/static.whatsapp.net\/rsrc.php\/v3\/y3\/r\/AZwgE-H9Ubq.png"},"11319":{"uri":"https:\/\/static.whatsapp.net\/rsrc.php\/v3\/yX\/r\/JYPizEwERE4.png"},"11370":{"uri":"https:\/\/static.whatsapp.net\/rsrc.php\/v3\/y-\/r\/_8J_xqERSbv.png"},"31401":{"uri":"https:\/\/static.whatsapp.net\/rsrc.php\/v3\/y1\/r\/m5BEg2K4OR4.png"},"31402":{"uri":"https:\/\/static.whatsapp.net\/rsrc.php\/v3\/yb\/r\/uspVxRFx2oU.png"},"31422":{"uri":"https:\/\/static.whatsapp.net\/rsrc.php\/v3\/yq\/r\/MHVytaGe3gh.png"},"9544":{"uri":"https:\/\/static.whatsapp.net\/rsrc.php\/yv\/r\/6Ib-MkrweJ6.ttf"},"9545":{"uri":"https:\/\/static.whatsapp.net\/rsrc.php\/yk\/r\/ln-uwGsChQW.ttf"},"9551":{"uri":"https:\/\/static.whatsapp.net\/rsrc.php\/v3\/yq\/r\/xCHrap4ZvgY.png"},"9552":{"uri":"https:\/\/static.whatsapp.net\/rsrc.php\/v3\/y6\/r\/R6k6NrF2Cbl.png"},"9553":{"uri":"https:\/\/static.whatsapp.net\/rsrc.php\/v3\/yd\/r\/jLlIEwASpgj.png"},"9554":{"uri":"https:\/\/static.whatsapp.net\/rsrc.php\/v3\/y5\/r\/_U7GO8EnPFR.png"},"9559":{"uri":"https:\/\/static.whatsapp.net\/rsrc.php\/v3\/yy\/r\/noBSk9ItwJJ.png"},"9583":{"uri":"https:\/\/static.whatsapp.net\/rsrc.php\/v3\/yP\/r\/re6de1QF8pQ.png"},"10591":{"uri":"https:\/\/static.whatsapp.net\/rsrc.php\/v3\/yp\/r\/ReKDgNfJEFB.png"}},"clpData":{"6476":{"r":1000,"s":1},"1744178":{"r":1,"s":1},"1838142":{"r":1,"s":1}},"gkxData":{"1624":{"result":false,"hash":null},"2160":{"result":false,"hash":null},"4112":{"result":false,"hash":null},"5134":{"result":true,"hash":null},"5434":{"result":false,"hash":null},"5679":{"result":false,"hash":null},"5943":{"result":false,"hash":null},"6748":{"result":true,"hash":null},"6969":{"result":false,"hash":null},"7541":{"result":false,"hash":null},"7685":{"result":false,"hash":null},"20836":{"result":false,"hash":null},"20940":{"result":false,"hash":null},"21043":{"result":false,"hash":null},"21051":{"result":false,"hash":null},"21052":{"result":false,"hash":null},"21053":{"result":false,"hash":null},"21054":{"result":false,"hash":null},"21055":{"result":false,"hash":null},"21056":{"result":false,"hash":null},"21057":{"result":false,"hash":null},"21058":{"result":true,"hash":null},"21106":{"result":true,"hash":null},"21107":{"result":false,"hash":null},"21112":{"result":true,"hash":null},"26256":{"result":false,"hash":null},"26258":{"result":true,"hash":null},"26259":{"result":false,"hash":null},"26262":{"result":true,"hash":null},"24344":{"result":false,"hash":null},"24351":{"result":false,"hash":null},"24383":{"result":false,"hash":null},"30214":{"result":false,"hash":null}},"qplData":{"106":{}},"justknobxData":{"371":{"r":true},"819":{"r":true},"822":{"r":true},"860":{"r":100},"1617":{"r":160},"1618":{"r":8},"1619":{"r":1},"1620":{"r":2},"1621":{"r":4},"1622":{"r":0},"1623":{"r":6},"1624":{"r":1},"1662":{"r":2},"1663":{"r":14},"1664":{"r":2},"1784":{"r":false},"1854":{"r":false},"2086":{"r":4},"2148":{"r":false},"2157":{"r":true},"2237":{"r":false},"2268":{"r":true},"2269":{"r":true},"2552":{"r":false},"2702":{"r":true}}}]]]}</script>
<script type="application/json" data-content-len="40671" data-sjs>{"require":[["Bootloader","handlePayload",null,[{"consistency":{"rev":1018072227},"rsrcMap":{"xFqg8AH":{"type":"js","src":"https:\/\/static.whatsapp.net\/rsrc.php\/v3\/yL\/r\/BlIuoNYtWPV.js","c":1,"p":":195","m":"1018072227_main"},"AJOaQIc":{"type":"js","src":"https:\/\/static.whatsapp.net\/rsrc.php\/v3\/y8\/r\/G_AUzITVDxz.js","c":1,"p":":11","m":"1018072227_main"},"a\/ErpYx":{"type":"js","src":"https:\/\/static.whatsapp.net\/rsrc.php\/v3\/y_\/r\/Erq9JIEfuV6.js","c":1,"p":":10","m":"1018072227_main"},"a5fZ54F":{"type":"js","src":"https:\/\/static.whatsapp.net\/rsrc.php\/v3iekP4\/yH\/l\/rt\/NXnawuAbdD4.js","tsrc":"https:\/\/static.whatsapp.net\/rsrc-translations.php\/v7iekP4\/yP\/l\/en_US\/NXnawuAbdD4.js","c":1,"p":":21","m":"1018072227_main"},"EeF59cM":{"type":"js","src":"https:\/\/static.whatsapp.net\/rsrc.php\/v3i9cC4\/yD\/l\/rt\/mzFfMLLw6HK.js","tsrc":"https:\/\/static.whatsapp.net\/rsrc-translations.php\/v7i9cC4\/yX\/l\/en_US\/mzFfMLLw6HK.js","c":1,"p":":36","m":"1018072227_main"},"Xkc0WVi":{"type":"js","src":"https:\/\/static.whatsapp.net\/rsrc.php\/v3iREA4\/yI\/l\/rt\/8Aolmhvy8LU.js","tsrc":"https:\/\/static.whatsapp.net\/rsrc-translations.php\/v7iREA4\/yN\/l\/en_US\/8Aolmhvy8LU.js","c":1,"p":":34","m":"1018072227_main"},"vR+ju+x":{"type":"js","src":"https:\/\/static.whatsapp.net\/rsrc.php\/v3i91J4\/yt\/l\/rt\/dNU4SErCrmv.js","tsrc":"https:\/\/static.whatsapp.net\/rsrc-translations.php\/v7i91J4\/ya\/l\/en_US\/dNU4SErCrmv.js","c":1,"p":":28","m":"1018072227_main"},"XRiLJ5h":{"type":"css","src":"https:\/\/static.whatsapp.net\/rsrc.php\/v4\/y7\/l\/0,cross\/WfrI-AAXpPh.css","c":1,"p":":18","m":"1018072227_main"},"4zHjo\/t":{"type":"js","src":"https:\/\/static.whatsapp.net\/rsrc.php\/v3\/yq\/r\/sMiW0MeCLnG.js","c":1,"p":":15","m":"1018072227_main"},"zBLIrrt":{"type":"js","src":"https:\/\/static.whatsapp.net\/rsrc.php\/v3\/yq\/r\/hmLfv8YDyld.js","c":1,"p":":194","m":"1018072227_main"},"gXHW2I\/":{"type":"js","src":"https:\/\/static.whatsapp.net\/rsrc.php\/v3\/yw\/r\/-LbRSVJO6sn.js","c":1,"p":":40","m":"1018072227_main"},"Kyj0DuM":{"type":"js","src":"https:\/\/static.whatsapp.net\/rsrc.php\/v3\/yE\/r\/CtlyoX5avh8.js","c":1,"p":":39","m":"1018072227_main"},"zzVLof4":{"type":"js","src":"https:\/\/static.whatsapp.net\/rsrc.php\/v3i6XI4\/y-\/l\/rt\/AObUHhfOII7.js","tsrc":"https:\/\/static.whatsapp.net\/rsrc-translations.php\/v7i6XI4\/yf\/l\/en_US\/AObUHhfOII7.js","c":1,"p":":149","m":"1018072227_main"},"080D39R":{"type":"js","src":"https:\/\/static.whatsapp.net\/rsrc.php\/v3\/yX\/r\/ctArhl7hZ9J.js","c":1,"p":":219","m":"1018072227_main"},"zZM5CG8":{"type":"js","src":"https:\/\/static.whatsapp.net\/rsrc.php\/v3\/y0\/r\/qV4CPdytvui.js","c":1,"p":":80","m":"1018072227_main"},"Fw+YAB0":{"type":"js","src":"https:\/\/static.whatsapp.net\/rsrc.php\/v3\/yJ\/r\/41rr4p6vs7j.js","c":1,"p":":147","m":"1018072227_main"},"7jdcV+B":{"type":"js","src":"https:\/\/static.whatsapp.net\/rsrc.php\/v3\/yf\/r\/fNcbr8WPj3N.js","c":1,"p":":228","m":"1018072227_main"},"tTfYNAo":{"type":"js","src":"https:\/\/static.whatsapp.net\/rsrc.php\/v3\/yv\/r\/OtiwkkrbnEg.js","c":1,"p":":67","m":"1018072227_main"},"9YKdw6R":{"type":"js","src":"https:\/\/static.whatsapp.net\/rsrc.php\/v3\/yI\/r\/SMKPIPut05F.js","c":1,"p":":192","m":"1018072227_main"},"lz41h9C":{"type":"js","src":"https:\/\/static.whatsapp.net\/rsrc.php\/v3\/yT\/r\/sqn5gRbbgEd.js","c":1,"p":":142","m":"1018072227_main"},"UcOlgtU":{"type":"js","src":"https:\/\/static.whatsapp.net\/rsrc.php\/v3\/yR\/r\/xjqPhXjJVzw.js","c":1,"p":":172","m":"1018072227_main"},"K4DN3ov":{"type":"js","src":"https:\/\/static.whatsapp.net\/rsrc.php\/v3\/yH\/r\/YUTlfecSbQp.js","c":1,"p":":218","m":"1018072227_main"},"WhncEwJ":{"type":"js","src":"https:\/\/static.whatsapp.net\/rsrc.php\/v3\/yy\/r\/jTuTGnMJXdo.js","c":1,"p":":90","m":"1018072227_main"},"Fr6Lkg3":{"type":"js","src":"https:\/\/static.whatsapp.net\/rsrc.php\/v3\/yu\/r\/D_fZidk8XTz.js","c":1,"p":":198","m":"1018072227_main"},"dzSjMyH":{"type":"js","src":"https:\/\/static.whatsapp.net\/rsrc.php\/v3\/y4\/r\/pWX5sxLaZTx.js","c":1,"p":":48","m":"1018072227_main"},"l9YeuzY":{"type":"js","src":"https:\/\/static.whatsapp.net\/rsrc.php\/v3\/ya\/r\/mEsaFp2_2Sa.js","c":1,"p":":50","m":"1018072227_main"},"pZbl5CZ":{"type":"js","src":"https:\/\/static.whatsapp.net\/rsrc.php\/v3\/yD\/r\/Hfkat6cPNHo.js","c":1,"p":":241","m":"1018072227_main"},"j65SaHn":{"type":"js","src":"https:\/\/static.whatsapp.net\/rsrc.php\/v3\/yC\/r\/sDl5KW9QJ97.js","c":1,"p":":150","m":"1018072227_main"},"bRKmx2D":{"type":"js","src":"https:\/\/static.whatsapp.net\/rsrc.php\/v3\/yj\/r\/wLUf1sFtuSI.js","c":1,"p":":163","m":"1018072227_main"},"wDNapDv":{"type":"js","src":"https:\/\/static.whatsapp.net\/rsrc.php\/v3\/yP\/r\/xc6XM-JdW6p.js","c":1,"p":":274","m":"1018072227_main"},"rc5Kz5Y":{"type":"js","src":"https:\/\/static.whatsapp.net\/rsrc.php\/v3\/y7\/r\/EZZ9HoHBlyi.js","c":1,"p":":79","m":"1018072227_main"},"IkfPTaq":{"tLine truncated
<script type="application/json" data-content-len="65298" data-sjs>{"require":[["ScheduledServerJS","handle",null,[{"__bbox":{"define":[["cr:686",[],{"__rc":[null,null]},-1],["cr:755",["warningWWW"],{"__rc":["warningWWW",null]},-1],["cr:3128",[],{"__rc":[null,null]},-1],["cr:4122",[],{"__rc":[null,null]},-1],["cr:4324",[],{"__rc":[null,null]},-1],["cr:4533",[],{"__rc":[null,null]},-1],["cr:4722",[],{"__rc":[null,null]},-1],["cr:4857",[],{"__rc":[null,null]},-1],["cr:4940",[],{"__rc":[null,null]},-1],["cr:4941",[],{"__rc":[null,null]},-1],["cr:5151",[],{"__rc":[null,null]},-1],["cr:5292",[],{"__rc":[null,null]},-1],["cr:5411",[],{"__rc":[null,null]},-1],["cr:5624",["WAWebCleanAssets"],{"__rc":["WAWebCleanAssets",null]},-1],["cr:5664",[],{"__rc":[null,null]},-1],["cr:5981",[],{"__rc":[null,null]},-1],["cr:6000",[],{"__rc":[null,null]},-1],["cr:6009",[],{"__rc":[null,null]},-1],["cr:6022",["WAWebFeatureDetectionSwSupport"],{"__rc":["WAWebFeatureDetectionSwSupport",null]},-1],["cr:6167",[],{"__rc":[null,null]},-1],["cr:7730",["getFbtResult"],{"__rc":["getFbtResult",null]},-1],["cr:7907",[],{"__rc":[null,null]},-1],["cr:8958",["FBJSON"],{"__rc":["FBJSON",null]},-1],["cr:9859",[],{"__rc":[null,null]},-1],["cr:10195",[],{"__rc":[null,null]},-1],["cr:10196",[],{"__rc":[null,null]},-1],["cr:10197",[],{"__rc":[null,null]},-1],["cr:10198",[],{"__rc":[null,null]},-1],["cr:10199",[],{"__rc":[null,null]},-1],["cr:10201",[],{"__rc":[null,null]},-1],["cr:10212",[],{"__rc":[null,null]},-1],["cr:10234",[],{"__rc":[null,null]},-1],["cr:951783",[],{"__rc":[null,null]},-1],["cr:1984081",[],{"__rc":[null,null]},-1],["IntlCurrentLocale",[],{"code":"en_US"},5954],["WAMediaWasmWorkerResource",[],{"url":"https:\/\/static.whatsapp.net\/rsrc.php\/v3\/y0\/r\/nM6SFfo6YdP.js","name":"WAMediaWasmWorkerBundle","dynamicModules":{"cr:6640":{"__bbox":{"__rc":[null,null]}}},"hsdp":{"bxData":{"236":{"uri":"https:\/\/static.whatsapp.net\/rsrc.php\/y_\/r\/IbbOXFaNJTy.wasm"},"237":{"uri":"https:\/\/static.whatsapp.net\/rsrc.php\/yR\/r\/ayqr5HQtlkb.wasm"},"6945":{"uri":"https:\/\/static.whatsapp.net\/rsrc.php\/yO\/r\/tL59TT3QfUA.wasm"},"6946":{"uri":"https:\/\/static.whatsapp.net\/rsrc.php\/yf\/r\/rogm88TRRiw.wasm"},"31390":{"uri":"https:\/\/static.whatsapp.net\/rsrc.php\/yk\/r\/jtnU4lyaNTU.wasm"}},"gkxData":{"8169":{"result":false,"hash":null}},"qplData":{"106":{}}}},7438],["CookiePrivacySandboxConfig",[],{"is_affected_by_samesite_lax":false},7723],["WAWebMediaWorkerResource",[],{"url":"https:\/\/static.whatsapp.net\/rsrc.php\/v3\/yd\/r\/TWBPKyJqVOD.js","name":"WAMediaWorkerBundle","useInitScript":true},7034],["CookieDomain",[],{"domain":"whatsapp.com"},6421],["IntlVariationHoldout",[],{"disable_variation":false},6533],["IntlNumberTypeProps",["IntlCLDRNumberType05"],{"module":{"__m":"IntlCLDRNumberType05"}},7027],["WAWebRgbaToWebpWorkerResource",[],{"url":"https:\/\/static.whatsapp.net\/rsrc.php\/v3\/yx\/r\/qFfYRbc9sfE.js","name":"WAWebRgbaToWebpWorkerBundle","dynamicModules":{"cr:6640":{"__bbox":{"__rc":[null,null]}}},"hsdp":{}},7480],["WAWebFtsWorkerResource",[],{"url":"https:\/\/static.whatsapp.net\/rsrc.php\/v3\/ya\/r\/UWqggsl4Tnn.js","name":"WAWebFtsWorkerBundle","dynamicModules":{"cr:6640":{"__bbox":{"__rc":[null,null]}}},"hsdp":{"gkxData":{"5943":{"result":false,"hash":null},"26258":{"result":true,"hash":null},"26259":{"result":false,"hash":null}},"justknobxData":{"1617":{"r":160},"1618":{"r":8},"1619":{"r":1},"1620":{"r":2},"1621":{"r":4},"1622":{"r":0},"1623":{"r":6},"1624":{"r":1},"1662":{"r":2},"1663":{"r":14},"1664":{"r":2},"1854":{"r":false}}}},7301],["WAWebOpusRecorderWorkerResource",[],{"url":"https:\/\/static.whatsapp.net\/rsrc.php\/v3\/yg\/r\/LgrJOIYRuL4.js","name":"WAWebOpusRecorderWorkerBundle","hsdp":{}},7347],["WAWebWebpToRgbaWorkerResource",[],{"url":"https:\/\/static.whatsapp.net\/rsrc.php\/v3\/yF\/r\/HOvMdg5_EUB.js","name":"WAWebWebpToRgbaWorkerBundle","dynamicModules":{"cr:6640":{"__bbox":{"__rc":[null,null]}}},"hsdp":{}},7481],["CookieCoreConfig",[],{"dpr":{"t":604800,"s":"None"},"locale":{"t":604800,"s":"Lax"},"wa_lang_pref":{"t":604800,"s":"None"},"wa_web_lang_pref":{"t":2419200,"s":"Lax"}},2104],["IntlPhonologicalRules",[],{"meta":{"\/_B\/":"([.,!?\\s]|^)","\/_E\/":"([.,!?\\s]|$)"},"patterns":{"\/\u0001(.*)('|&#039;)s\u0001(?:'|&#039;)s(.*)\/":"\u0001$1$2s\u0001$3","\/_\u0001([^\u0001]*)\u0001\/":"javascript"}},1496],["IntlViewerContext",[],{"GENDER":3,"regionalLocale":null},772],["NumberFormatConfig",[],{"decimalSeparator":".","numberDelimiter":",","minDigitsForThousandsSeparator":4,"standardDecimalPatternInfo":{"primaryGroupSize":3,"secondaryGroupSize":3},"numberingSystemData":null},54],["WebWorkerConfig",[],{"logging":{"enabled":false,"config":"WebWorkerLoggerConfig"},"evalWorkerURL":"\/rsrc.php\/v3\/y0\/r\/ypsvPwCD3ku.js"},297],["ZeroCategoryHeader",[],{},1127],["ZeroRewriteRules",[],{"rewrite_rules":{},"whitelist":{"\/hr\/r":1,"\/hr\/p":1,"\/zero\/unsupported_browser\/":1,"\/zero\/policy\/optin":1,"\/zero\/optin\Line truncated
<script type="application/json" data-content-len="83" data-sjs>{"require":[["qplTimingsServerJS",null,null,["7435115086220858941","tierTwoEnd"]]]}</script>
<script type="application/json" data-content-len="162" data-sjs>{"require":[["qplTimingsServerJS",null,null,["7435115086220858941","tierThree"]],["qplTimingsServerJS",null,null,["7435115086220858941-server","tierThree",108]]]}</script>
<link rel="preload" href="https://static.whatsapp.net/rsrc.php/v4/y7/l/0,cross/WfrI-AAXpPh.css" as="style" crossorigin="anonymous" />
<script type="application/json" data-content-len="1277" data-sjs>{"require":[["HasteSupportData","handle",null,[{"bxData":{"482":{"uri":"https:\/\/static.whatsapp.net\/rsrc.php\/v3\/yS\/r\/aGcVD59xVTb.png"},"484":{"uri":"https:\/\/static.whatsapp.net\/rsrc.php\/v3\/y6\/r\/wa669aeJeom.png"},"9546":{"uri":"https:\/\/static.whatsapp.net\/rsrc.php\/v3\/yf\/r\/4l5Jzvrg84p.png"},"9563":{"uri":"https:\/\/static.whatsapp.net\/rsrc.php\/yQ\/r\/WElxnwAGYY2.mp3"},"9564":{"uri":"https:\/\/static.whatsapp.net\/rsrc.php\/yv\/r\/ze2kHBOq8T0.mp3"},"10047":{"uri":"https:\/\/static.whatsapp.net\/rsrc.php\/v1\/y6\/r\/kNeGAbVVWRf.jpg"},"14943":{"uri":"https:\/\/static.whatsapp.net\/rsrc.php\/v3\/ye\/r\/W2MDyeo0zkf.png"},"9557":{"uri":"https:\/\/static.whatsapp.net\/rsrc.php\/v3\/y5\/r\/cJXjAoffU5Q.png"},"9558":{"uri":"https:\/\/static.whatsapp.net\/rsrc.php\/v3\/yB\/r\/7Y1jh45L_8V.png"},"9565":{"uri":"https:\/\/static.whatsapp.net\/rsrc.php\/yn\/r\/6swO8kJM8z2.mp4"},"9566":{"uri":"https:\/\/static.whatsapp.net\/rsrc.php\/yk\/r\/GoZyw2bTK6s.mp4"},"9560":{"uri":"https:\/\/static.whatsapp.net\/rsrc.php\/v3\/yo\/r\/Oh4t5oP-H5G.png"}},"clpData":{"1848815":{"r":10000,"s":1},"1828945":{"r":100,"s":1},"5779":{"r":100,"s":1}},"gkxData":{"22826":{"result":false,"hash":null}},"justknobxData":{"2450":{"r":true},"2451":{"r":true},"770":{"r":false}}}]]]}</script>
<script type="application/json" data-content-len="43795" data-sjs>{"require":[["Bootloader","handlePayload",null,[{"consistency":{"rev":1018072227},"rsrcMap":{"D9ar2nt":{"type":"js","src":"https:\/\/static.whatsapp.net\/rsrc.php\/v3i6sf4\/y_\/l\/rt\/65HPwmPTxvE.js","tsrc":"https:\/\/static.whatsapp.net\/rsrc-translations.php\/v7i6sf4\/yW\/l\/en_US\/65HPwmPTxvE.js","c":1,"p":":31","m":"1018072227_main"},"hnModTo":{"type":"js","src":"https:\/\/static.whatsapp.net\/rsrc.php\/v3iis_4\/yd\/l\/rt\/ZqmYe3zRs7GwaJyZPA-vVYdNU4SErCrmvFPlNguXyB9esMiW0MeCLnGG_AUzITVDxz.js","tsrc":"https:\/\/static.whatsapp.net\/rsrc-translations.php\/v7iis_4\/ye\/l\/en_US\/ZqmYe3zRs7GwaJyZPA-vVYdNU4SErCrmvFPlNguXyB9esMiW0MeCLnGG_AUzITVDxz.js","c":1,"p":":17,29,28,30,15,11","m":"1018072227_main"},"rtkaIRw":{"type":"js","src":"https:\/\/static.whatsapp.net\/rsrc.php\/v3iXrJ4\/yS\/l\/rt\/KpdmaCxXrbk.js","tsrc":"https:\/\/static.whatsapp.net\/rsrc-translations.php\/v7iXrJ4\/yF\/l\/en_US\/KpdmaCxXrbk.js","c":1,"p":":33","m":"1018072227_main"},"nH8nCaN":{"type":"js","src":"https:\/\/static.whatsapp.net\/rsrc.php\/v3i4114\/y9\/l\/rt\/TPp4r4DTd6U.js","tsrc":"https:\/\/static.whatsapp.net\/rsrc-translations.php\/v7i4114\/y6\/l\/en_US\/TPp4r4DTd6U.js","c":1,"p":":12","m":"1018072227_main"},"NQLlfG2":{"type":"js","src":"https:\/\/static.whatsapp.net\/rsrc.php\/v3iY-Y4\/yl\/l\/rt\/E4aGTVxC2nj.js","tsrc":"https:\/\/static.whatsapp.net\/rsrc-translations.php\/v7iY-Y4\/yw\/l\/en_US\/E4aGTVxC2nj.js","c":1,"p":":24","m":"1018072227_main"},"0DhT4t6":{"type":"js","src":"https:\/\/static.whatsapp.net\/rsrc.php\/v3iCUV4\/yP\/l\/rt\/eT8o1iCEdXa.js","tsrc":"https:\/\/static.whatsapp.net\/rsrc-translations.php\/v7iCUV4\/ym\/l\/en_US\/eT8o1iCEdXa.js","c":1,"p":":35","m":"1018072227_main"},"qWX8sWF":{"type":"js","src":"https:\/\/static.whatsapp.net\/rsrc.php\/v3iJ3t4\/ys\/l\/rt\/jwMe24ZeyPw.js","tsrc":"https:\/\/static.whatsapp.net\/rsrc-translations.php\/v7iJ3t4\/yo\/l\/en_US\/jwMe24ZeyPw.js","c":1,"p":":128","m":"1018072227_main"},"Ni4Wnff":{"type":"js","src":"https:\/\/static.whatsapp.net\/rsrc.php\/v3iXNj4\/yi\/l\/rt\/j3PVDVEGfbS.js","tsrc":"https:\/\/static.whatsapp.net\/rsrc-translations.php\/v7iXNj4\/y-\/l\/en_US\/j3PVDVEGfbS.js","c":1,"p":":26","m":"1018072227_main"},"HxQDhQJ":{"type":"js","src":"https:\/\/static.whatsapp.net\/rsrc.php\/v3i3uu4\/y2\/l\/rt\/I6OEshxaJLb.js","tsrc":"https:\/\/static.whatsapp.net\/rsrc-translations.php\/v7i3uu4\/yo\/l\/en_US\/I6OEshxaJLb.js","c":1,"p":":154","m":"1018072227_main"},"4hny5Ac":{"type":"js","src":"https:\/\/static.whatsapp.net\/rsrc.php\/v3iOEA4\/yi\/l\/rt\/8JxXFm1twal.js","tsrc":"https:\/\/static.whatsapp.net\/rsrc-translations.php\/v7iOEA4\/yc\/l\/en_US\/8JxXFm1twal.js","c":1,"p":":182","m":"1018072227_main"},"VmsME0b":{"type":"js","src":"https:\/\/static.whatsapp.net\/rsrc.php\/v3iB5w4\/y2\/l\/rt\/zTPZwgAxofs.js","tsrc":"https:\/\/static.whatsapp.net\/rsrc-translations.php\/v7iB5w4\/yE\/l\/en_US\/zTPZwgAxofs.js","c":1,"p":":53","m":"1018072227_main"},"IZZqytq":{"type":"js","src":"https:\/\/static.whatsapp.net\/rsrc.php\/v3iw2n4\/yT\/l\/rt\/JMYlCfvGpMJ.js","tsrc":"https:\/\/static.whatsapp.net\/rsrc-translations.php\/v7iw2n4\/yd\/l\/en_US\/JMYlCfvGpMJ.js","c":1,"p":":122","m":"1018072227_main"},"\/9QLqMI":{"type":"js","src":"https:\/\/static.whatsapp.net\/rsrc.php\/v3\/yb\/r\/qXYvgBWQS1q.js","c":1,"p":":114","m":"1018072227_main"},"WL4swjd":{"type":"js","src":"https:\/\/static.whatsapp.net\/rsrc.php\/v3iNrs4\/yY\/l\/rt\/donqKK_Dv61.js","tsrc":"https:\/\/static.whatsapp.net\/rsrc-translations.php\/v7iNrs4\/y0\/l\/en_US\/donqKK_Dv61.js","c":1,"p":":116","m":"1018072227_main"},"YWTJlBx":{"type":"js","src":"https:\/\/static.whatsapp.net\/rsrc.php\/v3\/yA\/r\/GSkPF-VZFTk.js","c":1,"p":":206","m":"1018072227_main"},"kfiiT2r":{"type":"js","src":"https:\/\/static.whatsapp.net\/rsrc.php\/v3iq274\/y4\/l\/rt\/KtBBEKCYzsi.js","tsrc":"https:\/\/static.whatsapp.net\/rsrc-translations.php\/v7iq274\/yR\/l\/en_US\/KtBBEKCYzsi.js","c":1,"p":":176","m":"1018072227_main"},"hUx8D1I":{"type":"js","src":"https:\/\/static.whatsapp.net\/rsrc.php\/v3inCB4\/yG\/l\/rt\/VzcTp4tGD9e.js","tsrc":"https:\/\/static.whatsapp.net\/rsrc-translations.php\/v7inCB4\/yz\/l\/en_US\/VzcTp4tGD9e.js","c":1,"p":":201","m":"1018072227_main"},"wCz0Cyh":{"type":"js","src":"https:\/\/static.whatsapp.net\/rsrc.php\/v3iiDn4\/yv\/l\/rt\/duJxhZWl9PJ.js","tsrc":"https:\/\/static.whatsapp.net\/rsrc-translations.php\/v7iiDn4\/y8\/l\/en_US\/duJxhZWl9PJ.js","c":1,"p":":103","m":"1018072227_main"},"H0SsTnU":{"type":"js","src":"https:\/\/static.whatsapp.net\/rsrc.php\/v3itu04\/yb\/l\/rt\/2b-Iw6nVyr-.js","tsrc":"https:\/\/static.whatsapp.net\/rsrc-translations.php\/v7itu04\/yg\/l\/en_US\/2b-Iw6nVyr-.js","c":1,"p":":178","m":"1018072227_main"},"mpVqeE4":{"type":"js","src":"https:\/\/static.whatsapp.net\/rsrc.php\/v3\/yO\/r\/yPowwKlCoGk.js","c":1,"p":":101","m":"1018072227_main"},"RIMg8Zt":{"type":"js","src":"https:\/\/static.whatsapp.net\/rsrc.php\/v3\/yM\/r\/Lz7N8MRDWLine truncated
<script type="application/json" data-content-len="130" data-sjs>{"require":[["DeferredJSResourceScheduler",null,null,[["D9ar2nt","a\/ErpYx","hnModTo","rtkaIRw","a5fZ54F","nH8nCaN","NQLlfG2"]]]]}</script>
<script type="application/json" data-content-len="6173" data-sjs>{"require":[["ScheduledServerJSWithCSS","handle",null,[{"__bbox":{"define":[["cr:104",[],{"__rc":[null,null]},-1],["cr:1066",[],{"__rc":[null,null]},-1],["cr:2015",[],{"__rc":[null,null]},-1],["cr:4937",[],{"__rc":[null,null]},-1],["cr:5037",[],{"__rc":[null,null]},-1],["cr:5735",[],{"__rc":[null,null]},-1],["cr:5736",[],{"__rc":[null,null]},-1],["cr:5923",[],{"__rc":[null,null]},-1],["cr:5932",[],{"__rc":[null,null]},-1],["cr:5959",[],{"__rc":[null,null]},-1],["cr:5993",[],{"__rc":[null,null]},-1],["cr:5997",[],{"__rc":[null,null]},-1],["cr:6003",[],{"__rc":[null,null]},-1],["cr:6004",[],{"__rc":[null,null]},-1],["cr:6005",[],{"__rc":[null,null]},-1],["cr:6018",[],{"__rc":[null,null]},-1],["cr:7004",["ReactReconcilerConstants.classic"],{"__rc":["ReactReconcilerConstants.classic",null]},-1],["cr:7007",["ReactReconciler.classic"],{"__rc":["ReactReconciler.classic",null]},-1],["cr:7456",[],{"__rc":[null,null]},-1],["cr:8750",[],{"__rc":[null,null]},-1],["cr:8751",[],{"__rc":[null,null]},-1],["cr:8752",[],{"__rc":[null,null]},-1],["cr:8753",[],{"__rc":[null,null]},-1],["cr:10202",[],{"__rc":[null,null]},-1],["cr:10204",[],{"__rc":[null,null]},-1],["cr:10207",[],{"__rc":[null,null]},-1],["cr:10208",[],{"__rc":[null,null]},-1],["cr:10209",[],{"__rc":[null,null]},-1],["cr:10210",[],{"__rc":[null,null]},-1],["cr:10211",[],{"__rc":[null,null]},-1],["cr:12061",["useWAWebNoop"],{"__rc":["useWAWebNoop",null]},-1],["cr:12093",[],{"__rc":[null,null]},-1],["cr:8828",[],{"__rc":[null,null]},-1],["cr:1094907",[],{"__rc":[null,null]},-1],["cr:921407",["useNoopDebuggingInfoComponent"],{"__rc":["useNoopDebuggingInfoComponent",null]},-1],["cr:1634616",["CometUserActivity"],{"__rc":["CometUserActivity",null]},-1],["cr:2695",[],{"__rc":[null,null]},-1],["cr:6008",[],{"__rc":[null,null]},-1],["cr:7002",["ReactReconcilerConstants-prod.classic"],{"__rc":["ReactReconcilerConstants-prod.classic",null]},-1],["cr:6994",["ReactReconciler-prod.classic"],{"__rc":["ReactReconciler-prod.classic",null]},-1]],"require":[["CometSuspenseFalcoEvent"],["WAWebMainWrapper"],["WAFtsMultiLangTokenizer"],["WAWebEmojiAssetLoaderConfig"],["asn1js-2.1.1"],["WAWebEmojiAssetLoader"],["WAWebSetWorkerSafeHandlerApi"],["WAWebSetFrontendHandlerApi"],["WAWebPipVideoStreaming"],["WAWebClearAppStates"],["WAWebLinkDeviceScreen.react"],["FbtLogging"],["IntlQtEventFalcoEvent"],["emptyFunction","thatReturns",["RequireDeferredReference"],[[{"__dr":"CometSuspenseFalcoEvent"},{"__dr":"WAWebMainWrapper"},{"__dr":"WAFtsMultiLangTokenizer"},{"__dr":"WAWebEmojiAssetLoaderConfig"},{"__dr":"asn1js-2.1.1"},{"__dr":"WAWebEmojiAssetLoader"},{"__dr":"WAWebSetWorkerSafeHandlerApi"},{"__dr":"WAWebSetFrontendHandlerApi"},{"__dr":"WAWebPipVideoStreaming"},{"__dr":"WAWebClearAppStates"},{"__dr":"WAWebLinkDeviceScreen.react"},{"__dr":"FbtLogging"},{"__dr":"IntlQtEventFalcoEvent"}]]],["CometBTManifestLoader","init",[],[]],["WAWebEmojiSetup"],["Bootloader","markComponentsAsImmediate",[],[["WAWebNewsletterContextMenu","WAWebAddToFavoritesModal.react","WAWebCaptureMediaDrawer.react","WAWebContactUsModal.react","WAWebCommunityFlow.react","WAWebProfileDrawer.react","pkijs","WAWebSendLogsPopup.react","WAWebSendMsgMultiModal.react","WAWebStarredDrawerV2.react","WAWebNewSubgroupSuggestionFlow.react","WAWebCreateTextStatusFlow.react","WAWebNewsletterTabFlow.react","WAWebVerificationDrawer.react","WAWebAddContactToGroupFlow.react","WAWebArchivedFlow.react","WAWebAttachMediaFlow.react","WAWebAttachVcardsFlow.react","WAWebContactMsgInfoFlow.react","WAWebEditCaptureFlow.react","WAWebGroupMsgInfoFlow.react","WAWebGroupsV4InviteFlow.react","WAWebInfoFlow.react","WAWebMediaViewerFlow.react","WAWebStatusFlow.react","WAWebStatusQuotedFlow.react","WAWebNewChatFlow.react","WAWebNewGroupFlow.react","WAWebSendGroupInviteFlow.react","WAWebSettingsFlow.react","WAWebDirectConnectionCaBundle","WAWebBizCartFlow","relay-runtime","WAWebPhotoPicker.react","WAWebLottieAnimation.react","WAWebStickerStoreFlow","WAWebBizOrderStatusUpdateFlow.react","WAWebDeactivateCommunityDrawer.react","WAWebCommunityAdminPromotionNotificationFlow.react","WAWebManageCommunityGroupsFlow.react","WAWebBizCatalogManagementFlow.react","WAWebBizLabelFlow.react","WAWebBizManageLabelFlow.react","WAWebBizMerchantDetailsFormFlow.react","WAWebBizOrderManagementFlow.react","WAWebBizProductDetailsFlow.react","WAWebBizQuickRepliesFlow.react","WAWebBizSendProductModal.react","WAWebBizAttachProductModal.react","WAWebBizRemoveDirectConnectionKeysBridge","WAWebGroupInviteLinkDrawer.react","WAWebForwardMessageModal.react","WAWebBizMerchantDetailsDrawer.react","WAWebViewAllRepliesFlow.react","WAWebStatusDrawerFlow.react","WAWebAdsManagementDrawer.react","pdfjs-dist","WAWebSeeReviewDetailScreenDrawer.react","WAWebBizToolsFlow.react","WAWebStatusPrivacySettingsModalFlow.react","WAWebNewsletterDSAReportDetailsDrawer.react","WAWeb-lottie-light-canvas","WAWebEventInfoFlow.react","WAWebNewsletteLine truncated
<script type="application/json" data-content-len="85" data-sjs>{"require":[["qplTimingsServerJS",null,null,["7435115086220858941","tierThreeEnd"]]]}</script>
<script type="application/json" data-content-len="3280" data-sjs>{"require":[["CometQPLPayloadStore","storePayloadMap",null,[{"7435115086220858941":{"css\/cgu0bcly5o8w04ww.pkg,css\/8ssldt5oxp8g4o80.pkg,css\/77bvrgeabmo0ccc4.pkg.__composite__.css":{"url":"https:\/\/static.whatsapp.net\/rsrc.php\/v4\/y4\/l\/0,cross\/nJSuJRxe0LRIuQQZ3xdyKtes83aNhixgx.css","refs":["htmlStart","tierOne","tierTwo","tierThree"]},"js\/adwjvc6bn6ogsocw.pkg.js":{"url":"https:\/\/static.whatsapp.net\/rsrc.php\/v3\/yl\/r\/HOQztTu5JYT.js","refs":["htmlStart","tierOne","tierTwo","tierThree"]},"js\/c3ax5tl3nrc4kc88.pkg.js":{"url":"https:\/\/static.whatsapp.net\/rsrc.php\/v3\/yw\/r\/dYGBdB10S1Z.js","refs":["htmlStart","tierOne","tierTwo","tierThree"]},"js\/94dcx39n0x8ooocc.pkg.js":{"url":"https:\/\/static.whatsapp.net\/rsrc.php\/v3\/yS\/r\/UhwbwVdbQX0.js","refs":["htmlStart","tierOne","tierTwo","tierThree"]},"js\/boe4n1nexrwc0ws8.pkg.js":{"url":"https:\/\/static.whatsapp.net\/rsrc.php\/v3\/yx\/r\/ryQSgyNOW8X.js","refs":["htmlStart","tierOne","tierTwo","tierThree"]},"js\/11h6rv0quww0048k.pkg,js\/ajfm1uik02woo48s.pkg,js\/3q7umf15ec00c8og.pkg,js\/4x9cumf05twko8s8.pkg,js\/4sx05anpb4ao84gw.pkg,js\/btvqx8uxs9sgkw4s.pkg,js\/csa9qir353c4goc0.pkg.__composite__.js":{"url":"https:\/\/static.whatsapp.net\/rsrc.php\/v3iWBv4\/yk\/l\/rt\/IN3s4Kru2-t2RSfFSjWLjPTFOQq3__cln_2SsOE-ssqJx0Eef7DOYopYnK0rykQBR7vsWqehdEtWw.js","refs":["htmlStart","tierTwo","tierThree"]},"js\/bls5blk1p3k8gc04.pkg.js":{"url":"https:\/\/static.whatsapp.net\/rsrc.php\/v3iHCc4\/yu\/l\/rt\/rW3lOI79YLL.js","refs":["htmlStart","tierTwo","tierThree"]},"js\/6lvucz4kv18g0og0.pkg.js":{"url":"https:\/\/static.whatsapp.net\/rsrc.php\/v3\/yR\/r\/h4Qy0FiYsZ_.js","refs":["htmlStart","tierTwo","tierThree"]},"js\/ci5trb6air4sskow.pkg.js":{"url":"https:\/\/static.whatsapp.net\/rsrc.php\/v3iuY-4\/ym\/l\/rt\/yTGZCBMSfU8.js","refs":["htmlStart","tierTwo","tierThree"]},"js\/7smu524m1iko4ogs.pkg.js":{"url":"https:\/\/static.whatsapp.net\/rsrc.php\/v3\/y6\/r\/OicDJTSlcdh.js","refs":["htmlStart","tierTwo","tierThree"]},"js\/7ir3e8fs6rokcc0w.pkg.js":{"url":"https:\/\/static.whatsapp.net\/rsrc.php\/v3i6sf4\/y_\/l\/rt\/65HPwmPTxvE.js","refs":["tierThree"]},"js\/6h6os3dm0kg0cog0.pkg.js":{"url":"https:\/\/static.whatsapp.net\/rsrc.php\/v3\/y_\/r\/Erq9JIEfuV6.js","refs":["tierThree"]},"js\/dt5kj0sja4o4kwos.pkg,js\/bqvubmtfy1w08804.pkg,js\/25aaww03azr4soww.pkg,js\/cdkr9wbfnr40ssg0.pkg,js\/7z0ji06w2wgskkwc.pkg,js\/6h5n7x3sc2ccgsoc.pkg.__composite__.js":{"url":"https:\/\/static.whatsapp.net\/rsrc.php\/v3iis_4\/yd\/l\/rt\/ZqmYe3zRs7GwaJyZPA-vVYdNU4SErCrmvFPlNguXyB9esMiW0MeCLnGG_AUzITVDxz.js","refs":["tierThree"]},"js\/cqp6eiggu00ksgww.pkg.js":{"url":"https:\/\/static.whatsapp.net\/rsrc.php\/v3iXrJ4\/yS\/l\/rt\/KpdmaCxXrbk.js","refs":["tierThree"]},"js\/mzhqcy7m8ao0oocw.pkg.js":{"url":"https:\/\/static.whatsapp.net\/rsrc.php\/v3iekP4\/yH\/l\/rt\/NXnawuAbdD4.js","refs":["tierThree"]},"js\/8etvpcy3s2kggwkg.pkg.js":{"url":"https:\/\/static.whatsapp.net\/rsrc.php\/v3i4114\/y9\/l\/rt\/TPp4r4DTd6U.js","refs":["tierThree"]},"js\/6h1am4ng39s800kg.pkg.js":{"url":"https:\/\/static.whatsapp.net\/rsrc.php\/v3iY-Y4\/yl\/l\/rt\/E4aGTVxC2nj.js","refs":["tierThree"]},"css\/dp3ugr89mbwo4osk.pkg.css":{"url":"https:\/\/static.whatsapp.net\/rsrc.php\/v4\/y7\/l\/0,cross\/WfrI-AAXpPh.css","refs":["tierThree"]}}}]]]}</script>
<script type="application/json" data-content-len="69" data-sjs>{"require":[["qplTagServerJS",null,null,[["lastServerTagFlushed"]]]]}</script>
<script type="application/json" data-content-len="1583" data-sjs>{"require":[["qplTimingsServerJS",null,null,["7435115086220858941-server","earlyFlushStart",11]],["qplTimingsServerJS",null,null,["7435115086220858941-server","genEFRsrcStart",15]],["qplTimingsServerJS",null,null,["7435115086220858941-server","genEFRsrcEnd",19]],["qplTimingsServerJS",null,null,["7435115086220858941-server","earlyFlushEnd",23]],["qplTimingsServerJS",null,null,["7435115086220858941-server","cometPageRender",25]],["qplTimingsServerJS",null,null,["7435115086220858941-server","genTierOneStart",25]],["qplTimingsServerJS",null,null,["7435115086220858941-server","genRenderSplashScreenStart",25]],["qplTimingsServerJS",null,null,["7435115086220858941-server","genRenderSplashScreenEnd",26]],["qplTimingsServerJS",null,null,["7435115086220858941-server","genTierOneEnd",36]],["qplTimingsServerJS",null,null,["7435115086220858941-server","tierOnePreloadersFinished",37]],["qplTimingsServerJS",null,null,["7435115086220858941-server","genTierTwoStart",37]],["qplTimingsServerJS",null,null,["7435115086220858941-server","genTierTwoEnd",78]],["qplTimingsServerJS",null,null,["7435115086220858941-server","tierTwoPreloadersFinished",83]],["qplTimingsServerJS",null,null,["7435115086220858941-server","genTierThreeStart",83]],["qplTimingsServerJS",null,null,["7435115086220858941-server","genTierThreeEnd",107]],["qplTimingsServerJS",null,null,["7435115086220858941-server","tierThreePreloadersFinished",109]],["qplTimingsServerJS",null,null,["7435115086220858941-server","responseStart",1]],["qplTimingsServerJS",null,null,["7435115086220858941-server","responseEnd",109]]]}</script>
<script type="application/json" data-content-len="2" data-sjs>{}</script>
<script type="application/json" data-content-len="169" data-sjs>{"require":[["CometQPLPayloadStore","storePayloadBytesSent",null,[{"htmlStart":[14607,4599],"tierOne":[11806,4255],"tierTwo":[110183,31134],"tierThree":[52210,7574]}]]]}</script>
<div id='has-finished-comet-page' hidden style='display: none;'></div></body></html>
@@ -3,7 +3,7 @@ import { Label } from '@waha/structures/labels.dto';
import { PaginationParams } from '@waha/structures/pagination.dto';
import { TextStatus } from '@waha/structures/status.dto';
import * as lodash from 'lodash';
import { Client } from 'whatsapp-web.js';
import { Client, Events } from 'whatsapp-web.js';
import { Message } from 'whatsapp-web.js/src/structures';
// eslint-disable-next-line @typescript-eslint/no-var-requires
@@ -18,19 +18,32 @@ const { LoadPaginator } = require('./_Paginator.js');
// eslint-disable-next-line @typescript-eslint/no-var-requires
const ChatFactory = require('whatsapp-web.js/src/factories/ChatFactory');
export class WebjsClient extends Client {
async inject() {
// Even tho this.inject is not defined as interface in Client.ts
// We can still call and override it
// @ts-ignore
await super.inject();
export class WebjsClientCore extends Client {
constructor(options) {
super(options);
// Wait until it's READY and inject more utils
this.on(Events.READY, async () => {
await this.injectWaha();
});
}
// Load util functions (serializers, helper functions)
async injectWaha() {
await this.pupPage.evaluate(LoadLodash);
await this.pupPage.evaluate(LoadPaginator);
await this.pupPage.evaluate(LoadWAHA);
}
async setPushName(name: string) {
await this.pupPage.evaluate(async (pushName) => {
return await window['WAHA'].WAWebSetPushnameConnAction.setPushname(
pushName,
);
}, name);
if (this.info) {
this.info.pushname = name;
}
}
async unpair() {
await this.pupPage.evaluate(async () => {
if (
+10
View File
@@ -11,4 +11,14 @@ exports.LoadWAHA = () => {
const chatPromises = chats.map((chat) => window.WWebJS.getChatModel(chat));
return await Promise.all(chatPromises);
};
// Set push name
window.WAHA.WAWebSetPushnameConnAction = window.require(
'WAWebSetPushnameConnAction',
);
// Get my status
window.WAHA.WAWebTextStatusCollection = window.require(
'WAWebTextStatusCollection',
);
};
+344 -131
View File
@@ -1,12 +1,10 @@
import { UnprocessableEntityException } from '@nestjs/common';
import {
getChannelInviteLink,
WAHAInternalEvent,
WhatsappSession,
} from '@waha/core/abc/session.abc';
import { toJID } from '@waha/core/engines/noweb/session.noweb.core';
import { LocalAuth } from '@waha/core/engines/webjs/LocalAuth';
import { WebjsClient } from '@waha/core/engines/webjs/WebjsClient';
import { WebjsClientCore } from '@waha/core/engines/webjs/WebjsClientCore';
import {
AvailableInPlusVersion,
NotImplementedByEngineError,
@@ -15,16 +13,20 @@ import { IMediaEngineProcessor } from '@waha/core/media/IMediaEngineProcessor';
import { QR } from '@waha/core/QR';
import { splitAt } from '@waha/helpers';
import { PairingCodeResponse } from '@waha/structures/auth.dto';
import { CallData } from '@waha/structures/calls.dto';
import {
Channel,
ChannelListResult,
ChannelMessage,
ChannelRole,
ChannelSearchByText,
ChannelSearchByView,
CreateChannelRequest,
ListChannelsQuery,
PreviewChannelMessages,
} from '@waha/structures/channels.dto';
import {
ChatArchiveEvent,
ChatSortField,
ChatSummary,
GetChatMessageQuery,
GetChatMessagesFilter,
GetChatMessagesQuery,
@@ -55,26 +57,30 @@ import {
WAHASessionStatus,
WAMessageAck,
} from '@waha/structures/enums.dto';
import { BinaryFile, RemoteFile } from '@waha/structures/files.dto';
import {
CreateGroupRequest,
GroupSortField,
ParticipantsRequest,
SettingsSecurityChangeInfo,
} from '@waha/structures/groups.dto';
import { Label, LabelDTO, LabelID } from '@waha/structures/labels.dto';
import { ReplyToMessage } from '@waha/structures/message.dto';
import { PaginationParams } from '@waha/structures/pagination.dto';
import { PaginationParams, SortOrder } from '@waha/structures/pagination.dto';
import { WAMessage, WAMessageReaction } from '@waha/structures/responses.dto';
import { MeInfo } from '@waha/structures/sessions.dto';
import { StatusRequest, TextStatus } from '@waha/structures/status.dto';
import {
BROADCAST_ID,
StatusRequest,
TextStatus,
} from '@waha/structures/status.dto';
import { WAMessageRevokedBody } from '@waha/structures/webhooks.dto';
EnginePayload,
WAMessageRevokedBody,
} from '@waha/structures/webhooks.dto';
import { PaginatorInMemory } from '@waha/utils/Paginator';
import { sleep, waitUntil } from '@waha/utils/promiseTimeout';
import { SingleDelayedJobRunner } from '@waha/utils/SingleDelayedJobRunner';
import {
import * as lodash from 'lodash';
import { fromEvent, merge, mergeMap, Observable } from 'rxjs';
import { map } from 'rxjs/operators';
import WAWebJS, {
Call,
Channel as WEBJSChannel,
Chat,
@@ -91,11 +97,9 @@ import {
} from 'whatsapp-web.js';
import { Message as MessageInstance } from 'whatsapp-web.js/src/structures';
// eslint-disable-next-line @typescript-eslint/no-var-requires
const QRCode = require('qrcode');
export interface WebJSConfig {
webVersion?: string;
cacheType: 'local' | 'none';
}
export class WhatsappSessionWebJSCore extends WhatsappSession {
@@ -108,7 +112,7 @@ export class WhatsappSessionWebJSCore extends WhatsappSession {
private engineStateCheckDelayedJob: SingleDelayedJobRunner;
private shouldRestart: boolean;
whatsapp: WebjsClient;
whatsapp: WebjsClientCore;
protected qr: QR;
public constructor(config) {
@@ -139,8 +143,12 @@ export class WhatsappSessionWebJSCore extends WhatsappSession {
protected getClientOptions(): ClientOptions {
const path = this.getClassDirName();
const webVersion =
this.engineConfig?.webVersion || '2.3000.1014522270-alpha';
// this.log.debug(`Using web version: '${webVersion}'`);
this.engineConfig?.webVersion || '2.3000.1018072227-alpha';
const cacheType = this.engineConfig?.cacheType || 'none';
this.logger.info(`Using cache type: '${cacheType}'`);
if (cacheType === 'local') {
this.logger.info(`Using web version: '${webVersion}'`);
}
return {
puppeteer: {
headless: true,
@@ -150,23 +158,24 @@ export class WhatsappSessionWebJSCore extends WhatsappSession {
},
webVersion: webVersion,
webVersionCache: {
type: 'none',
// path: path,
// strict: true,
type: cacheType,
path: path,
strict: true,
},
};
}
protected async buildClient() {
const clientOptions = this.getClientOptions();
const base = process.env.WAHA_LOCAL_STORE_BASE_DIR || './.sessions';
clientOptions.authStrategy = new LocalAuth({
clientId: this.name,
dataPath: undefined,
dataPath: `${base}/webjs/default`,
logger: this.logger,
rmMaxRetries: undefined,
});
this.addProxyConfig(clientOptions);
return new WebjsClient(clientOptions);
return new WebjsClientCore(clientOptions);
}
private restartClient() {
@@ -243,7 +252,7 @@ export class WhatsappSessionWebJSCore extends WhatsappSession {
this.listenEngineEventsInDebugMode();
}
this.listenConnectionEvents();
this.events.emit(WAHAInternalEvent.ENGINE_START);
this.subscribeEngineEvents2();
}
async start() {
@@ -259,8 +268,9 @@ export class WhatsappSessionWebJSCore extends WhatsappSession {
async stop() {
this.shouldRestart = false;
this.status = WAHASessionStatus.STOPPED;
this.events.removeAllListeners();
this.stopEvents();
this.startDelayedJob.cancel();
this.mediaManager.close();
await this.end();
}
@@ -330,8 +340,7 @@ export class WhatsappSessionWebJSCore extends WhatsappSession {
this.whatsapp.on(Events.QR_RECEIVED, async (qr) => {
this.logger.debug('QR received');
// Convert to image and save
const url = await QRCode.toDataURL(qr);
this.qr.save(url, qr);
this.qr.save(qr);
this.printQR(this.qr);
this.status = WAHASessionStatus.SCAN_QR_CODE;
});
@@ -343,7 +352,6 @@ export class WhatsappSessionWebJSCore extends WhatsappSession {
});
this.whatsapp.on(Events.AUTHENTICATED, (args) => {
this.status = WAHASessionStatus.WORKING;
this.qr.save('');
this.logger.info({ args: args }, `Session has been authenticated!`);
});
@@ -448,6 +456,30 @@ export class WhatsappSessionWebJSCore extends WhatsappSession {
};
}
/**
* Profile methods
*/
public async setProfileName(name: string): Promise<boolean> {
await this.whatsapp.setPushName(name);
return true;
}
public async setProfileStatus(status: string): Promise<boolean> {
await this.whatsapp.setStatus(status);
return true;
}
protected setProfilePicture(file: BinaryFile | RemoteFile): Promise<boolean> {
throw new AvailableInPlusVersion();
}
protected deleteProfilePicture(): Promise<boolean> {
throw new AvailableInPlusVersion();
}
/**
* Other methods
*/
sendText(request: MessageTextRequest) {
const options = this.getMessageOptions(request);
return this.whatsapp.sendMessage(
@@ -477,7 +509,11 @@ export class WhatsappSessionWebJSCore extends WhatsappSession {
reply(request: MessageReplyRequest) {
const options = this.getMessageOptions(request);
return this.whatsapp.sendMessage(request.chatId, request.text, options);
return this.whatsapp.sendMessage(
this.ensureSuffix(request.chatId),
request.text,
options,
);
}
sendImage(request: MessageImageRequest) {
@@ -497,12 +533,16 @@ export class WhatsappSessionWebJSCore extends WhatsappSession {
name: request.title,
});
const options = this.getMessageOptions(request);
return this.whatsapp.sendMessage(request.chatId, location, options);
return this.whatsapp.sendMessage(
this.ensureSuffix(request.chatId),
location,
options,
);
}
async forwardMessage(request: MessageForwardRequest): Promise<WAMessage> {
const forwardMessage = this.recreateMessage(request.messageId);
const msg = await forwardMessage.forward(request.chatId);
const msg = await forwardMessage.forward(this.ensureSuffix(request.chatId));
// Return "sent: true" for now
// need to research how to get the data from WebJS
// @ts-ignore
@@ -510,17 +550,23 @@ export class WhatsappSessionWebJSCore extends WhatsappSession {
}
async sendSeen(request: SendSeenRequest) {
const chat: Chat = await this.whatsapp.getChatById(request.chatId);
const chat: Chat = await this.whatsapp.getChatById(
this.ensureSuffix(request.chatId),
);
await chat.sendSeen();
}
async startTyping(request: ChatRequest) {
const chat: Chat = await this.whatsapp.getChatById(request.chatId);
const chat: Chat = await this.whatsapp.getChatById(
this.ensureSuffix(request.chatId),
);
await chat.sendStateTyping();
}
async stopTyping(request: ChatRequest) {
const chat: Chat = await this.whatsapp.getChatById(request.chatId);
const chat: Chat = await this.whatsapp.getChatById(
this.ensureSuffix(request.chatId),
);
await chat.clearState();
}
@@ -564,6 +610,41 @@ export class WhatsappSessionWebJSCore extends WhatsappSession {
return this.whatsapp.getChats(pagination);
}
public async getChatsOverview(
pagination: PaginationParams,
): Promise<ChatSummary[]> {
pagination = {
...pagination,
sortBy: ChatSortField.CONVERSATION_TIMESTAMP,
sortOrder: SortOrder.DESC,
};
const chats = await this.getChats(pagination);
const promises = [];
for (const chat of chats) {
promises.push(this.fetchChatSummary(chat));
}
const result = await Promise.all(promises);
return result;
}
protected async fetchChatSummary(chat: Chat): Promise<ChatSummary> {
const picture = await this.getContactProfilePicture(
chat.id._serialized,
false,
);
const lastMessage = !!chat.lastMessage
? this.toWAMessage(chat.lastMessage)
: null;
return {
id: chat.id._serialized,
name: chat.name || null,
picture: picture,
lastMessage: lastMessage,
_chat: chat,
};
}
public async getChatMessages(
chatId: string,
query: GetChatMessagesQuery,
@@ -575,7 +656,6 @@ export class WhatsappSessionWebJSCore extends WhatsappSession {
);
}
const limit = query.limit;
const downloadMedia = query.downloadMedia;
// Test there's chat with id
await this.whatsapp.getChatById(this.ensureSuffix(chatId));
@@ -604,8 +684,25 @@ export class WhatsappSessionWebJSCore extends WhatsappSession {
return await this.processIncomingMessage(message, query.downloadMedia);
}
public async pinMessage(
chatId: string,
messageId: string,
duration: number,
): Promise<boolean> {
const message = await this.whatsapp.getMessageById(messageId);
return message.pin(duration);
}
public async unpinMessage(
chatId: string,
messageId: string,
): Promise<boolean> {
const message = await this.whatsapp.getMessageById(messageId);
return message.unpin();
}
async deleteChat(chatId) {
const chat = await this.whatsapp.getChatById(chatId);
const chat = await this.whatsapp.getChatById(this.ensureSuffix(chatId));
return chat.delete();
}
@@ -706,11 +803,10 @@ export class WhatsappSessionWebJSCore extends WhatsappSession {
return { about: await contact.getAbout() };
}
public async getContactProfilePicture(query: ContactQuery) {
const contact = await this.whatsapp.getContactById(
this.ensureSuffix(query.contactId),
);
return { profilePictureURL: await contact.getProfilePicUrl() };
public async fetchContactProfilePicture(id: string) {
const contact = await this.whatsapp.getContactById(this.ensureSuffix(id));
const url = await contact.getProfilePicUrl();
return url;
}
public async blockContact(request: ContactRequest) {
@@ -737,6 +833,14 @@ export class WhatsappSessionWebJSCore extends WhatsappSession {
return this.whatsapp.createGroup(request.name, participantIds);
}
public joinGroup(code: string) {
return this.whatsapp.acceptInvite(code);
}
public joinInfoGroup(code: string) {
return this.whatsapp.getInviteInfo(code);
}
public async getInfoAdminsOnly(id): Promise<SettingsSecurityChangeInfo> {
const groupChat = (await this.whatsapp.getChatById(id)) as GroupChat;
return {
@@ -766,10 +870,25 @@ export class WhatsappSessionWebJSCore extends WhatsappSession {
return groupChat.setMessagesAdminsOnly(value);
}
public getGroups() {
return this.whatsapp
.getChats()
.then((chats) => chats.filter((chat) => chat.isGroup));
public async getGroups(pagination: PaginationParams) {
const chats = await this.whatsapp.getChats();
const groups = lodash.filter(chats, (chat) => chat.isGroup);
switch (pagination.sortBy) {
case GroupSortField.ID:
pagination.sortBy = 'id._serialized';
break;
case GroupSortField.SUBJECT:
pagination.sortBy = 'groupMetadata.subject';
break;
}
const paginator = new PaginatorInMemory(pagination);
return paginator.apply(groups);
}
public async refreshGroups(): Promise<boolean> {
return true;
}
public getGroup(id) {
@@ -847,6 +966,25 @@ export class WhatsappSessionWebJSCore extends WhatsappSession {
/**
* Channels methods
*/
public searchChannelsByView(
query: ChannelSearchByView,
): Promise<ChannelListResult> {
throw new AvailableInPlusVersion();
}
public searchChannelsByText(
query: ChannelSearchByText,
): Promise<ChannelListResult> {
throw new AvailableInPlusVersion();
}
public async previewChannelMessages(
inviteCode: string,
query: PreviewChannelMessages,
): Promise<ChannelMessage[]> {
throw new AvailableInPlusVersion();
}
protected ChatToChannel(chat: WEBJSChannel): Channel {
// @ts-ignore
const metadata = chat.channelMetadata;
@@ -863,6 +1001,7 @@ export class WhatsappSessionWebJSCore extends WhatsappSession {
picture: null,
verified: metadata.verified,
role: role,
subscribersCount: null,
};
}
@@ -880,6 +1019,7 @@ export class WhatsappSessionWebJSCore extends WhatsappSession {
picture: metadata.pictureUrl,
verified: metadata.isVerified,
role: role,
subscribersCount: metadata.subscribersCount,
};
}
@@ -891,6 +1031,10 @@ export class WhatsappSessionWebJSCore extends WhatsappSession {
channels = channels.filter((channel) => channel.role === query.role);
}
// Exclude GUEST, browser saves the data
// when we search channels or getting messages
channels = channels.filter((channel) => channel.role === 'GUEST');
const promises = channels.map(async (channel) =>
this.whatsapp.getProfilePicUrl(channel.id),
);
@@ -979,7 +1123,7 @@ export class WhatsappSessionWebJSCore extends WhatsappSession {
* Status methods
*/
protected checkStatusRequest(request: StatusRequest) {
if (request.contacts?.length > 0) {
if (request.contacts && request.contacts?.length > 0) {
const msg =
"WEBJS doesn't accept 'contacts'. Remove the field to send status to all contacts.";
throw new UnprocessableEntityException(msg);
@@ -994,88 +1138,152 @@ export class WhatsappSessionWebJSCore extends WhatsappSession {
/**
* END - Methods for API
*/
subscribeEngineEvent(event, handler): boolean {
switch (event) {
case WAHAEvents.MESSAGE:
this.whatsapp.on(Events.MESSAGE_RECEIVED, (message) =>
this.processIncomingMessage(message).then(handler),
);
return true;
case WAHAEvents.MESSAGE_WAITING:
this.whatsapp.on(Events.MESSAGE_CIPHERTEXT, (message) =>
this.processIncomingMessage(message).then(handler),
);
return true;
case WAHAEvents.MESSAGE_REVOKED:
this.whatsapp.on(
Events.MESSAGE_REVOKED_EVERYONE,
async (after, before) => {
const afterMessage = after ? await this.toWAMessage(after) : null;
const beforeMessage = before
? await this.toWAMessage(before)
: null;
const body: WAMessageRevokedBody = {
after: afterMessage,
before: beforeMessage,
subscribeEngineEvents2() {
//
// All
//
const events: Observable<EnginePayload>[] = [];
for (const key in Events) {
const event = Events[key];
const event$ = fromEvent(this.whatsapp, event);
events.push(
event$.pipe(
map((data) => {
return {
event: event,
data: data,
};
handler(body);
},
);
return true;
case WAHAEvents.MESSAGE_REACTION:
this.whatsapp.on('message_reaction', (message) =>
handler(this.processMessageReaction(message)),
);
return true;
case WAHAEvents.MESSAGE_ANY:
this.whatsapp.on(Events.MESSAGE_CREATE, (message) =>
this.processIncomingMessage(message).then(handler),
);
return true;
case WAHAEvents.STATE_CHANGE:
this.whatsapp.on(Events.STATE_CHANGED, handler);
return true;
case WAHAEvents.MESSAGE_ACK:
// We do not download media here
this.whatsapp.on(Events.MESSAGE_ACK, (message) =>
this.toWAMessage(message).then(handler),
);
return true;
case WAHAEvents.GROUP_JOIN:
this.whatsapp.on(Events.GROUP_JOIN, handler);
return true;
case WAHAEvents.GROUP_LEAVE:
this.whatsapp.on(Events.GROUP_LEAVE, handler);
return true;
case WAHAEvents.CHAT_ARCHIVE:
this.whatsapp.on('chat_archived', (chat, archived, _) => {
const body: ChatArchiveEvent = {
id: chat.id._serialized,
archived: archived,
timestamp: chat.timestamp,
};
handler(body);
});
return true;
case WAHAEvents.CALL_RECEIVED:
this.whatsapp.on('call', (call: Call) => {
const body: CallData = {
id: call.id,
from: call.from,
timestamp: call.timestamp,
isVideo: call.isVideo,
isGroup: call.isGroup,
};
handler(body);
});
return true;
default:
return false;
}),
),
);
}
const all$ = merge(...events);
this.events2.get(WAHAEvents.ENGINE_EVENT).switch(all$);
//
// Messages
//
const messageReceived$ = fromEvent(this.whatsapp, Events.MESSAGE_RECEIVED);
const messagesFromOthers$ = messageReceived$.pipe(
mergeMap((msg: any) => this.processIncomingMessage(msg, true)),
);
this.events2.get(WAHAEvents.MESSAGE).switch(messagesFromOthers$);
const messageCreate$ = fromEvent(this.whatsapp, Events.MESSAGE_CREATE);
const messagesFromAll$ = messageCreate$.pipe(
mergeMap((msg: any) => this.processIncomingMessage(msg, true)),
);
this.events2.get(WAHAEvents.MESSAGE_ANY).switch(messagesFromAll$);
const messageCiphertext$ = fromEvent(
this.whatsapp,
Events.MESSAGE_CIPHERTEXT,
);
const messagesWaiting$ = messageCiphertext$.pipe(
mergeMap((msg: any) => this.processIncomingMessage(msg, true)),
);
this.events2.get(WAHAEvents.MESSAGE_WAITING).switch(messagesWaiting$);
const messageRevoked$ = fromEvent(
this.whatsapp,
Events.MESSAGE_REVOKED_EVERYONE,
(after, before) => {
return { after, before };
},
);
const messagesRevoked$ = messageRevoked$.pipe(
map((event): WAMessageRevokedBody => {
const afterMessage = event.after ? this.toWAMessage(event.after) : null;
const beforeMessage = event.before
? this.toWAMessage(event.before)
: null;
return {
after: afterMessage,
before: beforeMessage,
};
}),
);
this.events2.get(WAHAEvents.MESSAGE_REVOKED).switch(messagesRevoked$);
const messageReaction$ = fromEvent(this.whatsapp, 'message_reaction');
const messagesReaction$ = messageReaction$.pipe(
map(this.processMessageReaction.bind(this)),
);
this.events2.get(WAHAEvents.MESSAGE_REACTION).switch(messagesReaction$);
const messageAck$ = fromEvent(
this.whatsapp,
Events.MESSAGE_ACK,
(message, ack) => {
return { message, ack };
},
);
const messagesAck$ = messageAck$.pipe(
map((event) => event.message),
map(this.toWAMessage.bind(this)),
);
this.events2.get(WAHAEvents.MESSAGE_ACK).switch(messagesAck$);
//
// Others
//
const stateChanged$ = fromEvent(this.whatsapp, Events.STATE_CHANGED);
this.events2.get(WAHAEvents.STATE_CHANGE).switch(stateChanged$);
//
// Groups
//
const groupJoin$ = fromEvent(this.whatsapp, Events.GROUP_JOIN);
this.events2.get(WAHAEvents.GROUP_JOIN).switch(groupJoin$);
const groupLeave$ = fromEvent(this.whatsapp, Events.GROUP_LEAVE);
this.events2.get(WAHAEvents.GROUP_LEAVE).switch(groupLeave$);
//
// Chats
//
const chatArchived$ = fromEvent(
this.whatsapp,
'chat_archived',
(chat, archived, _) => {
return {
chat: chat,
archived: archived,
};
},
);
const chatsArchived$ = chatArchived$.pipe(
map((event) => {
return {
id: event.chat.id._serialized,
archived: event.archived,
timestamp: event.chat.timestamp,
};
}),
);
this.events2.get(WAHAEvents.CHAT_ARCHIVE).switch(chatsArchived$);
//
// Calls
//
const call$ = fromEvent(this.whatsapp, 'call');
const calls$ = call$.pipe(
map((call: Call) => {
return {
id: call.id,
from: call.from,
timestamp: call.timestamp,
isVideo: call.isVideo,
isGroup: call.isGroup,
};
}),
);
this.events2.get(WAHAEvents.CALL_RECEIVED).switch(calls$);
}
private async processIncomingMessage(message: Message, downloadMedia = true) {
protected async processIncomingMessage(
message: Message,
downloadMedia = true,
) {
if (downloadMedia) {
try {
message = await this.downloadMedia(message);
@@ -1084,7 +1292,7 @@ export class WhatsappSessionWebJSCore extends WhatsappSession {
this.logger.error(e, e.stack);
}
}
return await this.toWAMessage(message);
return this.toWAMessage(message);
}
private processMessageReaction(reaction: Reaction): WAMessageReaction {
@@ -1102,10 +1310,10 @@ export class WhatsappSessionWebJSCore extends WhatsappSession {
};
}
protected toWAMessage(message: Message): Promise<WAMessage> {
protected toWAMessage(message: Message): WAMessage {
const replyTo = this.extractReplyTo(message);
// @ts-ignore
return Promise.resolve({
return {
id: message.id._serialized,
timestamp: message.timestamp,
from: message.from,
@@ -1114,9 +1322,9 @@ export class WhatsappSessionWebJSCore extends WhatsappSession {
body: message.body,
// Media
// @ts-ignore
hasMedia: Boolean(message.media),
hasMedia: Boolean(message.hasMedia),
// @ts-ignore
media: message.media,
media: message.media || null,
// @ts-ignore
mediaUrl: message.media?.url,
// @ts-ignore
@@ -1126,12 +1334,12 @@ export class WhatsappSessionWebJSCore extends WhatsappSession {
vCards: message.vCards,
replyTo: replyTo,
_data: message.rawData,
});
};
}
protected extractReplyTo(message: Message): ReplyToMessage | null {
// @ts-ignore
const quotedMsg = message.rawData.quotedMsg;
const quotedMsg = message.rawData?.quotedMsg;
if (!quotedMsg) {
return;
}
@@ -1170,6 +1378,7 @@ export class WhatsappSessionWebJSCore extends WhatsappSession {
return {
mentions: mentions,
quotedMessageId: quotedMessageId,
linkPreview: request.linkPreview,
};
}
}
@@ -1185,6 +1394,10 @@ export class WEBJSEngineMediaProcessor
return message.type !== 'revoked';
}
getChatId(message: Message): string {
return message.id.remote;
}
getMessageId(message: Message): string {
return message.id._serialized;
}
@@ -0,0 +1,54 @@
import { populateSessionInfo } from '@waha/core/abc/manager.abc';
import { WhatsappSession } from '@waha/core/abc/session.abc';
import { WebhookSender } from '@waha/core/integrations/webhooks/WebhookSender';
import { WAHAEvents, WAHAEventsWild } from '@waha/structures/enums.dto';
import { WebhookConfig } from '@waha/structures/webhooks.config.dto';
import { EventWildUnmask } from '@waha/utils/events';
import { LoggerBuilder } from '@waha/utils/logging';
import { Logger } from 'pino';
export class WebhookConductor {
private logger: Logger;
private eventUnmask = new EventWildUnmask(WAHAEvents, WAHAEventsWild);
constructor(protected loggerBuilder: LoggerBuilder) {
this.logger = loggerBuilder.child({ name: WebhookConductor.name });
}
protected buildSender(webhookConfig: WebhookConfig): WebhookSender {
return new WebhookSender(this.loggerBuilder, webhookConfig);
}
private getSuitableEvents(events: WAHAEvents[] | string[]): WAHAEvents[] {
return this.eventUnmask.unmask(events);
}
public configure(session: WhatsappSession, webhooks: WebhookConfig[]) {
for (const webhookConfig of webhooks) {
this.configureSingleWebhook(session, webhookConfig);
}
}
private configureSingleWebhook(
session: WhatsappSession,
webhook: WebhookConfig,
) {
if (!webhook || !webhook.url || webhook.events.length === 0) {
return;
}
const url = webhook.url;
this.logger.info(`Configuring webhooks for ${url}...`);
const events = this.getSuitableEvents(webhook.events);
const sender = this.buildSender(webhook);
for (const event of events) {
const obs$ = session.getEventObservable(event);
obs$.subscribe((payload) => {
const data = populateSessionInfo(event, session)(payload);
sender.send(data);
});
this.logger.debug(`Event '${event}' is enabled for url: ${url}`);
}
this.logger.info(`Webhooks were configured for ${url}.`);
}
}
@@ -0,0 +1,203 @@
import { SECOND } from '@waha/structures/enums.dto';
import {
RetryPolicy,
WebhookConfig,
} from '@waha/structures/webhooks.config.dto';
import { LoggerBuilder } from '@waha/utils/logging';
import { VERSION } from '@waha/version';
import axios, { AxiosInstance } from 'axios';
import axiosRetry, { retryAfter } from 'axios-retry';
import * as crypto from 'crypto';
import { Logger } from 'pino';
import { ulid } from 'ulid';
// eslint-disable-next-line @typescript-eslint/no-var-requires
const HttpAgent = require('agentkeepalive');
// eslint-disable-next-line @typescript-eslint/no-var-requires
const HttpsAgent = require('agentkeepalive').HttpsAgent;
const DEFAULT_RETRY_DELAY_SECONDS = 2;
const DEFAULT_RETRY_ATTEMPTS = 15;
const DEFAULT_HMAC_ALGORITHM = 'sha512';
function noDelay(_retryNumber = 0, error: any) {
return Math.max(0, retryAfter(error));
}
function constantDelay(delayFactor: number) {
return (_retryNumber = 0, error = undefined) => {
return Math.max(delayFactor, retryAfter(error));
};
}
export function exponentialDelay(delayFactor: number) {
return (retryNumber = 0, error = undefined) => {
const calculatedDelay = 2 ** retryNumber * delayFactor;
const delay = Math.max(calculatedDelay, retryAfter(error));
const randomSum = delay * 0.2 * Math.random(); // 0-20% of the delay
return delay + randomSum;
};
}
export class WebhookSender {
protected static AGENTS = {
http: new HttpAgent({}),
https: new HttpsAgent({}),
};
protected url: string;
protected logger: Logger;
protected readonly config: WebhookConfig;
protected axios: AxiosInstance;
constructor(
loggerBuilder: LoggerBuilder,
protected webhookConfig: WebhookConfig,
) {
this.url = webhookConfig.url;
this.logger = loggerBuilder.child({ name: WebhookSender.name });
this.config = webhookConfig;
this.axios = this.buildAxiosInstance();
}
send(json: any) {
const body = JSON.stringify(json);
const headers = {
'content-type': 'application/json',
};
Object.assign(headers, this.getWebhookHeader());
Object.assign(headers, this.getHMACHeaders(body));
const ctx = {
id: headers['X-Webhook-Request-Id'],
['event.id']: json.id,
url: this.url,
};
this.logger.info(ctx, `Sending POST...`);
this.logger.debug(ctx, `POST DATA`);
this.axios
.post(this.url, body, { headers: headers })
.then((response) => {
this.logger.info(
ctx,
`POST request was sent with status code: ${response.status}`,
);
this.logger.debug(
{
...ctx,
body: response.data,
},
`Response`,
);
})
.catch((error) => {
this.logger.error(
{
...ctx,
error: error.message,
data: error.response?.data,
},
`POST request failed: ${error.message}`,
);
});
}
protected buildAxiosInstance(): AxiosInstance {
// configure headers
const customHeaders = this.config.customHeaders || [];
const headers = {
'content-type': 'application/json',
'User-Agent': `WAHA/${VERSION.version}`,
};
customHeaders.forEach((header) => {
headers[header.name] = header.value;
});
// configure retry
const attempts = this.config.retries?.attempts ?? DEFAULT_RETRY_ATTEMPTS;
const delaySeconds =
this.config.retries?.delaySeconds ?? DEFAULT_RETRY_DELAY_SECONDS;
const delayMs = delaySeconds * SECOND;
const policy = this.config.retries?.policy;
const retryDelay = this.buildRetryDelay(policy, delayMs);
const instance = axios.create({
headers: headers,
httpAgent: WebhookSender.AGENTS.http,
httpsAgent: WebhookSender.AGENTS.https,
});
axiosRetry(instance, {
retries: attempts,
retryDelay: retryDelay,
retryCondition: (error) => true,
onRetry: (retryCount, error, requestConfig) => {
this.logger.warn(
{ id: requestConfig.headers['X-Webhook-Request-Id'] },
`Error sending POST request: '${error.message}'. Retrying ${retryCount}/${attempts}...`,
);
},
});
return instance;
}
protected getHMACHeaders(body: string) {
// HMAC
const hmac = this.calculateHmac(body, DEFAULT_HMAC_ALGORITHM);
if (!hmac) {
return {};
}
return {
'X-Webhook-Hmac': hmac,
'X-Webhook-Hmac-Algorithm': DEFAULT_HMAC_ALGORITHM,
};
}
protected getWebhookHeader() {
return {
// UUID, no '-' in it
'X-Webhook-Request-Id': ulid(),
// unix timestamp with ms
'X-Webhook-Timestamp': Date.now().toString(),
};
}
private calculateHmac(body, algorithm) {
if (!this.config.hmac || !this.config.hmac.key) {
return undefined;
}
return crypto
.createHmac(algorithm, this.config.hmac.key)
.update(body)
.digest('hex');
}
private buildRetryDelay(
policy: RetryPolicy | null,
ms: number,
): (retryNumber: number, error: any) => number {
if (!ms) {
this.logger.debug(`Using no delay, because delaySeconds set to 0`);
return noDelay;
}
switch (policy) {
case RetryPolicy.CONSTANT:
this.logger.debug(`Using constant delay with '${ms}' ms factor`);
return constantDelay(ms);
case RetryPolicy.LINEAR:
this.logger.debug(`Using linear delay with '${ms}' ms factor`);
return axiosRetry.linearDelay(ms);
case RetryPolicy.EXPONENTIAL:
this.logger.debug(`Using exponential delay with '${ms}' ms factor`);
return exponentialDelay(ms);
default:
this.logger.debug('No delay policy specified, using constant delay');
return constantDelay(ms);
}
}
}
+86 -26
View File
@@ -1,13 +1,23 @@
import {
Injectable,
NotFoundException,
OnModuleInit,
UnprocessableEntityException,
} from '@nestjs/common';
import { EngineBootstrap } from '@waha/core/abc/EngineBootstrap';
import { GowsEngineConfigService } from '@waha/core/config/GowsEngineConfigService';
import { WebJSEngineConfigService } from '@waha/core/config/WebJSEngineConfigService';
import { WhatsappSessionGoWSCore } from '@waha/core/engines/gows/session.gows.core';
import { WebhookConductor } from '@waha/core/integrations/webhooks/WebhookConductor';
import { MediaStorageFactory } from '@waha/core/media/MediaStorageFactory';
import { DefaultMap } from '@waha/utils/DefaultMap';
import { getPinoLogLevel, LoggerBuilder } from '@waha/utils/logging';
import { promiseTimeout, sleep } from '@waha/utils/promiseTimeout';
import { EventEmitter } from 'events';
import { complete } from '@waha/utils/reactive/complete';
import { SwitchObservable } from '@waha/utils/reactive/SwitchObservable';
import { PinoLogger } from 'nestjs-pino';
import { Observable, retry, share } from 'rxjs';
import { map } from 'rxjs/operators';
import { WhatsappConfigService } from '../config.service';
import {
@@ -23,7 +33,7 @@ import {
SessionInfo,
} from '../structures/sessions.dto';
import { WebhookConfig } from '../structures/webhooks.config.dto';
import { SessionManager } from './abc/manager.abc';
import { populateSessionInfo, SessionManager } from './abc/manager.abc';
import { SessionParams, WhatsappSession } from './abc/session.abc';
import { EngineConfigService } from './config/EngineConfigService';
import { WhatsappSessionNoWebCore } from './engines/noweb/session.noweb.core';
@@ -33,7 +43,6 @@ import { getProxyConfig } from './helpers.proxy';
import { MediaManager } from './media/MediaManager';
import { LocalSessionAuthRepository } from './storage/LocalSessionAuthRepository';
import { LocalStoreCore } from './storage/LocalStoreCore';
import { WebhookConductorCore } from './webhooks.core';
export class OnlyDefaultSessionIsAllowed extends UnprocessableEntityException {
constructor() {
@@ -49,7 +58,7 @@ enum DefaultSessionStatus {
}
@Injectable()
export class SessionManagerCore extends SessionManager {
export class SessionManagerCore extends SessionManager implements OnModuleInit {
SESSION_STOP_TIMEOUT = 3000;
// session - exists and running (or failed or smth)
@@ -59,25 +68,34 @@ export class SessionManagerCore extends SessionManager {
private sessionConfig?: SessionConfig;
DEFAULT = 'default';
// @ts-ignore
protected WebhookConductorClass = WebhookConductorCore;
protected readonly EngineClass: typeof WhatsappSession;
protected events2: DefaultMap<WAHAEvents, SwitchObservable<any>>;
protected readonly engineBootstrap: EngineBootstrap;
constructor(
config: WhatsappConfigService,
private engineConfigService: EngineConfigService,
private webjsEngineConfigService: WebJSEngineConfigService,
gowsConfigService: GowsEngineConfigService,
log: PinoLogger,
private mediaStorageFactory: MediaStorageFactory,
) {
super(config, log);
this.events = new EventEmitter();
super(log, config, gowsConfigService);
this.session = DefaultSessionStatus.STOPPED;
this.sessionConfig = null;
const engineName = this.engineConfigService.getDefaultEngineName();
this.EngineClass = this.getEngine(engineName);
this.engineBootstrap = this.getEngineBootstrap(engineName);
this.events2 = new DefaultMap<WAHAEvents, SwitchObservable<any>>(
(key) =>
new SwitchObservable((obs$) => {
return obs$.pipe(retry(), share());
}),
);
this.store = new LocalStoreCore(engineName.toLowerCase());
this.sessionAuthRepository = new LocalSessionAuthRepository(this.store);
this.startPredefinedSessions();
this.clearStorage().catch((error) => {
this.log.error({ error }, 'Error while clearing storage');
});
@@ -88,6 +106,8 @@ export class SessionManagerCore extends SessionManager {
return WhatsappSessionWebJSCore;
} else if (engine === WAHAEngine.NOWEB) {
return WhatsappSessionNoWebCore;
} else if (engine === WAHAEngine.GOWS) {
return WhatsappSessionGoWSCore;
} else {
throw new NotFoundException(`Unknown whatsapp engine '${engine}'.`);
}
@@ -100,14 +120,21 @@ export class SessionManagerCore extends SessionManager {
}
async beforeApplicationShutdown(signal?: string) {
if (!this.session) {
return;
if (this.session) {
await this.stop(this.DEFAULT, true);
}
await this.stop(this.DEFAULT, true);
this.stopEvents();
await this.engineBootstrap.shutdown();
}
async onApplicationBootstrap() {
await this.engineBootstrap.bootstrap();
this.startPredefinedSessions();
}
private async clearStorage() {
const storage = this.mediaStorageFactory.build(
const storage = await this.mediaStorageFactory.build(
'all',
this.log.logger.child({ name: 'Storage' }),
);
await storage.purge();
@@ -138,12 +165,13 @@ export class SessionManagerCore extends SessionManager {
`Session '${this.DEFAULT}' is already started.`,
);
}
this.log.info(`'${name}' - starting session...`);
this.log.info({ session: name }, `Starting session...`);
const logger = this.log.logger.child({ session: name });
logger.level = getPinoLogLevel(this.sessionConfig?.debug);
const loggerBuilder: LoggerBuilder = logger;
const storage = this.mediaStorageFactory.build(
const storage = await this.mediaStorageFactory.build(
name,
loggerBuilder.child({ name: 'Storage' }),
);
await storage.init();
@@ -153,7 +181,7 @@ export class SessionManagerCore extends SessionManager {
loggerBuilder.child({ name: 'MediaManager' }),
);
const webhook = new this.WebhookConductorClass(loggerBuilder);
const webhook = new WebhookConductor(loggerBuilder);
const proxyConfig = this.getProxyConfig();
const sessionConfig: SessionParams = {
name,
@@ -164,21 +192,21 @@ export class SessionManagerCore extends SessionManager {
proxyConfig: proxyConfig,
sessionConfig: this.sessionConfig,
};
if (this.EngineClass === WhatsappSessionWebJSCore) {
sessionConfig.engineConfig = this.webjsEngineConfigService.getConfig();
} else if (this.EngineClass === WhatsappSessionGoWSCore) {
sessionConfig.engineConfig = this.gowsConfigService.getConfig();
}
await this.sessionAuthRepository.init(name);
// @ts-ignore
const session = new this.EngineClass(sessionConfig);
this.session = session;
this.updateSession();
// configure webhooks
const webhooks = this.getWebhooks();
webhook.configure(session, webhooks);
// configure events
session.events.on(
WAHAEvents.SESSION_STATUS,
this.handleSessionEvent(WAHAEvents.SESSION_STATUS, session),
);
// start session
await session.start();
logger.info('Session has been started.');
@@ -189,14 +217,32 @@ export class SessionManagerCore extends SessionManager {
};
}
private updateSession() {
if (!this.session) {
return;
}
const session: WhatsappSession = this.session as WhatsappSession;
for (const eventName in WAHAEvents) {
const event = WAHAEvents[eventName];
const stream$ = session
.getEventObservable(event)
.pipe(map(populateSessionInfo(event, session)));
this.events2.get(event).switch(stream$);
}
}
getSessionEvent(session: string, event: WAHAEvents): Observable<any> {
return this.events2.get(event);
}
async stop(name: string, silent: boolean): Promise<void> {
this.onlyDefault(name);
if (!this.isRunning(name)) {
this.log.debug(`Session is not running.`, { session: name });
this.log.debug({ session: name }, `Session is not running.`);
return;
}
this.log.info(`Stopping session...`, { session: name });
this.log.info({ session: name }, `Stopping session...`);
try {
const session = this.getSession(name);
await session.stop();
@@ -206,8 +252,9 @@ export class SessionManagerCore extends SessionManager {
throw err;
}
}
this.log.info(`Session has been stopped.`, { session: name });
this.log.info({ session: name }, `Session has been stopped.`);
this.session = DefaultSessionStatus.STOPPED;
this.updateSession();
await sleep(this.SESSION_STOP_TIMEOUT);
}
@@ -217,7 +264,7 @@ export class SessionManagerCore extends SessionManager {
}
const session = this.session as WhatsappSession;
this.log.info('Unpairing the device from account...', { session: name });
this.log.info({ session: name }, 'Unpairing the device from account...');
await session.unpair().catch((err) => {
this.log.warn(`Error while unpairing from device: ${err}`);
});
@@ -232,6 +279,7 @@ export class SessionManagerCore extends SessionManager {
async delete(name: string): Promise<void> {
this.onlyDefault(name);
this.session = DefaultSessionStatus.REMOVED;
this.updateSession();
this.sessionConfig = undefined;
}
@@ -339,4 +387,16 @@ export class SessionManagerCore extends SessionManager {
const engine = await this.fetchEngineInfo();
return { ...session, engine: engine };
}
protected stopEvents() {
complete(this.events2);
}
async onModuleInit() {
await this.init();
}
async init() {
await this.store.init();
}
}
+2
View File
@@ -11,6 +11,8 @@ interface IMediaEngineProcessor<Message> {
getMessageId(message: Message): string;
getChatId(message: Message): string;
getMediaBuffer(message: Message): Promise<Buffer | null>;
}
+1
View File
@@ -10,6 +10,7 @@ interface IMediaManager {
message: Message,
session: string,
): Promise<Message>;
close(): void;
}
export { IMediaManager };
+16
View File
@@ -6,6 +6,9 @@ import { S3MediaData } from '@waha/structures/media.s3.dto';
interface Message {
// false_111111111@c.us_AAAAAAAAAAAAAA
id: string;
// 1111111@c.us
chatId: string;
}
interface File {
@@ -37,6 +40,19 @@ abstract class IMediaStorage {
abstract getStorageData(data: MediaData): Promise<MediaStorageData>;
abstract purge(): Promise<void>;
abstract close(): Promise<void>;
}
export function getMetadata(data: MediaData): any {
const now = new Date();
return {
'waha-session': data.session,
'waha-chat-id': data.message.chatId,
'waha-message-id': data.message.id,
'waha-media-filename': data.file.filename,
'waha-created-at': now.getTime(),
};
}
export { IMediaStorage };
+8
View File
@@ -52,6 +52,7 @@ export class MediaManager implements IMediaManager {
session: string,
): Promise<WAMedia | null> {
const messageId = processor.getMessageId(message);
const chatId = processor.getChatId(message);
const mimetype = processor.getMimetype(message);
const filename = processor.getFilename(message);
if (!this.shouldProcessMimetype(mimetype)) {
@@ -66,6 +67,7 @@ export class MediaManager implements IMediaManager {
session: session,
message: {
id: messageId,
chatId: chatId,
},
file: {
extension: extension,
@@ -195,4 +197,10 @@ export class MediaManager implements IMediaManager {
throw error;
}
}
close() {
this.storage.close().catch((err) => {
this.log.error(`Failed to close media storage: ${err}`);
});
}
}
+1 -1
View File
@@ -2,5 +2,5 @@ import { IMediaStorage } from '@waha/core/media/IMediaStorage';
import { Logger } from 'pino';
export abstract class MediaStorageFactory {
abstract build(logger: Logger): IMediaStorage;
abstract build(name: string, logger: Logger): Promise<IMediaStorage>;
}
+7 -3
View File
@@ -3,12 +3,12 @@ import { SECOND } from '@waha/structures/enums.dto';
import * as fsp from 'fs/promises';
import * as path from 'path';
import { Logger } from 'pino';
import { promisify } from 'util';
import fs = require('fs');
import del = require('del');
import { fileExists } from '@waha/utils/files';
const writeFileAsync = promisify(fs.writeFile);
// eslint-disable-next-line @typescript-eslint/no-var-requires
const writeFileAtomic = require('write-file-atomic');
/**
* Save files locally using the filesystem
@@ -41,7 +41,7 @@ export class MediaLocalStorage implements IMediaStorage {
const filepath = this.getFullPath(data);
const folder = path.dirname(filepath);
await fsp.mkdir(folder, { recursive: true });
await writeFileAsync(filepath, buffer);
await writeFileAtomic(filepath, buffer);
this.postponeRemoval(filepath);
return true;
}
@@ -92,4 +92,8 @@ export class MediaLocalStorage implements IMediaStorage {
this.lifetimeMs,
);
}
async close() {
return;
}
}
@@ -11,7 +11,8 @@ export class MediaLocalStorageFactory extends MediaStorageFactory {
super();
}
build(logger: Logger): IMediaStorage {
async build(name: string, logger: Logger): Promise<IMediaStorage> {
// Local storage uses the same folder for all sessions
return new MediaLocalStorage(
logger,
this.config.filesFolder,
@@ -0,0 +1,22 @@
import { Injectable } from '@nestjs/common';
import { AvailableInPlusVersion } from '@waha/core/exceptions';
import {
ChannelCategory,
ChannelCountry,
ChannelView,
} from '@waha/structures/channels.dto';
@Injectable()
export class ChannelsInfoServiceCore {
async getCountries(): Promise<ChannelCountry[]> {
throw new AvailableInPlusVersion();
}
async getCategories(): Promise<ChannelCategory[]> {
throw new AvailableInPlusVersion();
}
async getViews(): Promise<ChannelView[]> {
throw new AvailableInPlusVersion();
}
}
+6 -4
View File
@@ -1,16 +1,18 @@
import { SessionConfig } from '../../structures/sessions.dto';
export abstract class ISessionConfigRepository {
abstract save(
abstract saveConfig(
sessionName: string,
config: SessionConfig | null,
): Promise<void>;
abstract get(sessionName: string): Promise<SessionConfig | null>;
abstract getConfig(sessionName: string): Promise<SessionConfig | null>;
abstract exists(sessionName: string): Promise<boolean>;
abstract delete(sessionName: string): Promise<void>;
abstract deleteConfig(sessionName: string): Promise<void>;
abstract getAll(): Promise<string[]>;
abstract getAllConfigs(): Promise<string[]>;
abstract init(): Promise<void>;
}
@@ -7,6 +7,9 @@ import { SessionConfig } from '../../structures/sessions.dto';
import { ISessionConfigRepository } from './ISessionConfigRepository';
import { LocalStore } from './LocalStore';
// eslint-disable-next-line @typescript-eslint/no-var-requires
const writeFileAtomic = require('write-file-atomic');
export class LocalSessionConfigRepository extends ISessionConfigRepository {
FILENAME = '.waha.session.config.json';
private store: LocalStore;
@@ -27,7 +30,7 @@ export class LocalSessionConfigRepository extends ISessionConfigRepository {
return true;
}
async get(sessionName: string): Promise<SessionConfig | null> {
async getConfig(sessionName: string): Promise<SessionConfig | null> {
const filepath = this.getFilePath(sessionName);
// Check file exists
if (!(await fileExists(filepath))) {
@@ -45,26 +48,26 @@ export class LocalSessionConfigRepository extends ISessionConfigRepository {
return JSON.parse(content);
}
async save(sessionName: string, config: SessionConfig | null) {
async saveConfig(sessionName: string, config: SessionConfig | null) {
// Create folder if not exist
const folder = this.store.getSessionDirectory(sessionName);
await fs.mkdir(folder, { recursive: true });
// Save config
const filepath = this.getFilePath(sessionName);
const content = JSON.stringify(config || null);
await fs.writeFile(filepath, content);
await writeFileAtomic(filepath, content);
}
private getFilePath(sessionName): string {
return this.store.getFilePath(sessionName, this.FILENAME);
}
async delete(sessionName: string): Promise<void> {
async deleteConfig(sessionName: string): Promise<void> {
const sessionDirectory = this.store.getSessionDirectory(sessionName);
await fs.remove(sessionDirectory);
}
async getAll(): Promise<string[]> {
async getAllConfigs(): Promise<string[]> {
await this.store.init();
const content = await fs.readdir(this.store.getEngineDirectory(), {
withFileTypes: true,
@@ -73,4 +76,8 @@ export class LocalSessionConfigRepository extends ISessionConfigRepository {
.filter((dirent) => dirent.isDirectory())
.map((dirent) => dirent.name);
}
async init() {
return;
}
}
@@ -1,74 +0,0 @@
import { Sqlite3SchemaValidation } from '@waha/core/engines/noweb/store/sqlite3/Sqlite3SchemaValidation';
import {
ISessionWorkerRepository,
SessionWorkerInfo,
} from '@waha/core/storage/ISessionWorkerRepository';
import { LocalStore } from '@waha/core/storage/LocalStore';
import { Field, Index, Schema } from '@waha/core/storage/sqlite3/Schema';
import { Sqlite3KVRepository } from '@waha/core/storage/sqlite3/Sqlite3KVRepository';
// eslint-disable-next-line @typescript-eslint/no-var-requires
const Database = require('better-sqlite3');
const SCHEMA = new Schema(
'session_worker',
[
new Field('id', 'TEXT'),
new Field('worker', 'TEXT'),
new Field('data', 'TEXT'),
],
[
new Index('session_worker_id_idx', ['id']),
new Index('session_worker_worker_idx', ['worker']),
],
);
export class LocalSessionWorkerRepository
extends Sqlite3KVRepository<SessionWorkerInfo>
implements ISessionWorkerRepository
{
constructor(store: LocalStore) {
const db = store.getWAHADatabase();
super(db, SCHEMA);
}
assign(session: string, worker: string): Promise<void> {
return this.upsertOne({ id: session, worker: worker });
}
unassign(session: string, worker: string): Promise<void> {
return this.deleteBy({ id: session, worker: worker });
}
remove(session: string) {
return this.deleteById(session);
}
async getSessionsByWorker(worker: string): Promise<string[]> {
const data = await this.getAllBy({ worker: worker });
return data.map((d) => d.id);
}
async init(): Promise<void> {
this.migrations();
this.validateSchema();
}
private migrations() {
this.db.exec(
'CREATE TABLE IF NOT EXISTS session_worker (id TEXT, worker TEXT, data TEXT)',
);
// Session can have only one record
this.db.exec(
'CREATE UNIQUE INDEX IF NOT EXISTS session_worker_id_idx ON session_worker (id)',
);
// Worker can have multiple records
this.db.exec(
'CREATE INDEX IF NOT EXISTS session_worker_worker_idx ON session_worker (worker)',
);
}
private validateSchema() {
new Sqlite3SchemaValidation(SCHEMA, this.db).validate();
}
}
+4 -6
View File
@@ -9,10 +9,8 @@ import { LocalStore } from './LocalStore';
const Database = require('better-sqlite3');
export class LocalStoreCore extends LocalStore {
protected readonly baseDirectory: string = path.join(
os.tmpdir(),
'waha-sessions',
);
protected readonly baseDirectory: string =
process.env.WAHA_LOCAL_STORE_BASE_DIR || './.sessions';
private readonly engine: string;
private db: any;
@@ -54,8 +52,7 @@ export class LocalStoreCore extends LocalStore {
}
protected getDirectoryPath(name: string): string {
const suffix = crypto.createHash('md5').update(name).digest('hex');
return path.join(this.getEngineDirectory(), `${name}-${suffix}`);
return path.join(this.getEngineDirectory(), name);
}
getWAHADatabase(): any {
@@ -63,6 +60,7 @@ export class LocalStoreCore extends LocalStore {
const engineDir = this.getEngineDirectory();
const database = path.join(engineDir, 'waha.sqlite3');
this.db = new Database(database);
this.db.pragma('journal_mode = WAL;');
}
return this.db;
}
File renamed without changes.
+4
View File
@@ -0,0 +1,4 @@
export interface IJsonQuery {
filter(field: string, key: string): string;
sortBy(field: string, sortBy: string, direction: string): string;
}
+28
View File
@@ -0,0 +1,28 @@
import Knex from 'knex';
export interface ISQLEngine {
/**
* Run a query that does not return any data
*/
run(query: Knex.QueryBuilder): Promise<void>;
/**
* Get a single row from the database
*/
get(query: Knex.QueryBuilder): Promise<any | null>;
/**
* Get all rows from the database
*/
all(query: Knex.QueryBuilder): Promise<any[]>;
/**
* Run a raw SQL query with bindings
*/
raw(sql: string, bindings: any[]): Promise<void>;
/**
* Execute a raw SQL query
*/
exec(sql: string): Promise<void>;
}
+217
View File
@@ -0,0 +1,217 @@
import { Field, Schema } from '@waha/core/storage/Schema';
import { IJsonQuery } from '@waha/core/storage/sql/IJsonQuery';
import { ISQLEngine } from '@waha/core/storage/sql/ISQLEngine';
import { PaginationParams } from '@waha/structures/pagination.dto';
import { KnexPaginator } from '@waha/utils/Paginator';
import Knex from 'knex';
export type Migration = string;
export class SqlKVRepository<Entity> {
protected UPSERT_BATCH_SIZE = 100;
protected Paginator: typeof KnexPaginator = KnexPaginator;
protected jsonQuery: IJsonQuery;
get schema(): Schema {
throw new Error('Not implemented');
}
get metadata(): Map<string, (entity: Entity) => any> {
return new Map();
}
get migrations(): Migration[] {
return [];
}
constructor(
private engine: ISQLEngine,
protected knex: Knex.Knex,
) {}
get columns(): Field[] {
return this.schema.columns;
}
get table(): string {
return this.schema.name;
}
/**
* Migrations
*/
async init(): Promise<void> {
await this.applyMigrations();
await this.validateSchema();
}
protected async applyMigrations() {
for (const migration of this.migrations) {
await this.engine.exec(migration);
}
}
protected async validateSchema() {
return;
}
/**
* Update methods
*/
save(entity: Entity) {
return this.upsertOne(entity);
}
async upsertOne(entity: Entity): Promise<void> {
await this.upsertMany([entity]);
}
async upsertMany(entities: Entity[]): Promise<void> {
if (entities.length === 0) {
return;
}
const batchSize = this.UPSERT_BATCH_SIZE;
for (let i = 0; i < entities.length; i += batchSize) {
const batch = entities.slice(i, i + batchSize);
await this.upsertBatch(batch);
}
}
private async upsertBatch(entities: Entity[]): Promise<void> {
const data = entities.map((entity) => this.dump(entity));
const columns = this.columns.map((c) => `"${c.fieldName}"`);
const values = data.map((d) => Object.values(d)).flat();
const sql = `INSERT INTO "${this.table}" (${columns.join(', ')})
VALUES ${data
.map(() => `(${columns.map(() => '?').join(', ')})`)
.join(', ')}
ON CONFLICT(id) DO UPDATE
SET ${columns
.map((column) => `${column} = excluded.${column}`)
.join(', ')}`;
try {
await this.raw(sql, values);
} catch (err) {
console.error(
`Error upserting data: ${err}, sql: ${sql}, values: ${values}`,
);
throw err;
}
}
/**
* Get methods
*/
getAll(pagination?: PaginationParams) {
let query = this.select();
query = this.pagination(query, pagination);
return this.all(query);
}
getAllByIds(ids: string[]) {
return this.all(this.select().whereIn('id', ids));
}
getById(id: string): Promise<Entity | null> {
return this.getBy({ id: id });
}
getAllBy(filters: any) {
const query = this.select().where(filters);
return this.all(query);
}
public async getBy(filters: any) {
const query = this.select().where(filters).limit(1);
return this.get(query);
}
/**
* Delete methods
*/
async deleteAll() {
const query = this.delete();
await this.run(query);
}
async deleteById(id: string) {
await this.deleteBy({ id: id });
}
public async deleteBy(filters: any) {
const query = this.delete().where(filters);
await this.run(query);
}
/**
* SQL Implementation details
*/
public async raw(sql: string, bindings: any[]): Promise<void> {
await this.engine.raw(sql, bindings);
}
protected async run(query: Knex.QueryBuilder): Promise<void> {
await this.engine.run(query);
}
protected async get(query: Knex.QueryBuilder): Promise<Entity | null> {
const row = await this.engine.get(query);
if (!row) {
return null;
}
return this.parse(row);
}
public async all(query: Knex.QueryBuilder): Promise<Entity[]> {
const rows = await this.engine.all(query);
return rows.map((row) => this.parse(row));
}
/**
* SQL helpers
*/
public select() {
return this.knex.select().from(this.table);
}
protected delete() {
return this.knex.delete().from(this.table);
}
public pagination(query: any, pagination?: PaginationParams) {
const paginator = new this.Paginator(pagination, this.jsonQuery);
return paginator.apply(query);
}
/**
* JSON helpers
*/
public filterJson(field: string, key: string): string {
return this.jsonQuery.filter(field, key);
}
protected stringify(data: any): string {
return JSON.stringify(data);
}
protected parse(row: any) {
return JSON.parse(row.data);
}
protected dump(entity: Entity) {
const data = {};
const raw = entity;
for (const field of this.columns) {
const fn = this.metadata.get(field.fieldName);
if (fn) {
data[field.fieldName] = fn(raw);
} else if (field.fieldName == 'data') {
data['data'] = this.stringify(raw);
} else {
data[field.fieldName] = raw[field.fieldName] ?? null;
}
}
return data;
}
}
+54
View File
@@ -0,0 +1,54 @@
import { Field, Index, Schema } from '@waha/core/storage/Schema';
import { Migration } from '@waha/core/storage/sql/SqlKVRepository';
/**
* Session Config
*/
export const SQLSessionConfigSchema = new Schema(
'session_config',
[new Field('id', 'TEXT'), new Field('data', 'TEXT')],
[new Index('session_config_id_index', ['id'])],
);
export const SQLSessionConfigMigrations: Migration[] = [
'CREATE TABLE IF NOT EXISTS session_config (id TEXT PRIMARY KEY, data TEXT)',
'CREATE UNIQUE INDEX IF NOT EXISTS session_config_id_index ON session_config (id)',
];
/**
* Me
*/
export const SQLMeSchema = new Schema(
'me',
[new Field('id', 'TEXT'), new Field('data', 'TEXT')],
[new Index('me_id_index', ['id'])],
);
export const SQLMeMigrations: Migration[] = [
'CREATE TABLE IF NOT EXISTS me (id TEXT PRIMARY KEY, data TEXT)',
'CREATE UNIQUE INDEX IF NOT EXISTS me_id_index ON me (id)',
];
/**
* Worker
*/
export const SQLSessionWorkerSchema = new Schema(
'session_worker',
[
new Field('id', 'TEXT'),
new Field('worker', 'TEXT'),
new Field('data', 'TEXT'),
],
[
new Index('session_worker_id_idx', ['id']),
new Index('session_worker_worker_idx', ['worker']),
],
);
export const SQLSessionWorkerMigrations: Migration[] = [
'CREATE TABLE IF NOT EXISTS session_worker (id TEXT, worker TEXT, data TEXT)',
// Session can have only one record
'CREATE UNIQUE INDEX IF NOT EXISTS session_worker_id_idx ON session_worker (id)',
// Worker can have multiple records
'CREATE INDEX IF NOT EXISTS session_worker_worker_idx ON session_worker (worker)',
];
+38
View File
@@ -0,0 +1,38 @@
import { ISQLEngine } from '@waha/core/storage/sql/ISQLEngine';
import { Database } from 'better-sqlite3';
import { Knex } from 'knex';
export class Sqlite3Engine implements ISQLEngine {
constructor(protected db: Database) {}
async all(query: Knex.QueryBuilder): Promise<any[]> {
const sql = query.toSQL().sql;
const bind = query.toSQL().bindings;
return this.db.prepare(sql).all(bind);
}
async get(query: Knex.QueryBuilder): Promise<any | null> {
const sql = query.toSQL().sql;
const bind = query.toSQL().bindings;
const row: any = this.db.prepare(sql).get(bind);
if (!row) {
return null;
}
return row;
}
async run(query: Knex.QueryBuilder): Promise<void> {
const sql = query.toSQL().sql;
const bind = query.toSQL().bindings;
this.db.prepare(sql).run(bind);
return;
}
async raw(sql: string, bindings: any[]) {
this.db.prepare(sql).run(bindings);
}
async exec(sql): Promise<void> {
this.db.exec(sql);
}
}
@@ -0,0 +1,13 @@
import { IJsonQuery } from '@waha/core/storage/sql/IJsonQuery';
export class Sqlite3JsonQuery implements IJsonQuery {
filter(field: string, key: string): string {
const jsonPath = `$.${key}`;
return `${field}->'${jsonPath}' = ?`;
}
sortBy(field: string, sortBy: string, direction: string): string {
const jsonPath = `$.${sortBy}`;
return `${field}->'${jsonPath}' ${direction}`;
}
}
+11 -167
View File
@@ -1,177 +1,21 @@
import { PaginationParams } from '@waha/structures/pagination.dto';
import { KnexPaginator } from '@waha/utils/Paginator';
import { SqlKVRepository } from '@waha/core/storage/sql/SqlKVRepository';
import { Sqlite3Engine } from '@waha/core/storage/sqlite3/Sqlite3Engine';
import { Sqlite3JsonQuery } from '@waha/core/storage/sqlite3/Sqlite3JsonQuery';
import { Database } from 'better-sqlite3';
import Knex, { QueryBuilder } from 'knex';
import { Field, Schema } from './Schema';
import Knex from 'knex';
/**
* Key value repository with extra metadata
*/
export class Sqlite3KVRepository<Entity> {
private UPSERT_BATCH_SIZE = 100;
export class Sqlite3KVRepository<Entity> extends SqlKVRepository<Entity> {
protected db: Database;
protected jsonQuery = new Sqlite3JsonQuery();
private readonly metadata: Map<string, (entity: Entity) => any>;
protected readonly table: string;
private readonly columns: Field[];
private knex: Knex.Knex;
protected Paginator = KnexPaginator;
constructor(
db: Database,
schema: Schema,
metadata: Map<string, (entity: Entity) => any> | null = null,
) {
this.db = db;
this.columns = schema.columns;
this.table = schema.name;
this.metadata = metadata || new Map();
constructor(db: Database) {
// sqlite does not support inserting default values. Set the `useNullAsDefault` flag to hide this warning. (see docs https://knexjs.org/guide/query-builder.html#insert).
this.knex = Knex({ client: 'better-sqlite3', useNullAsDefault: true });
}
getAll(pagination?: PaginationParams) {
let query = this.select();
query = this.pagination(query, pagination);
return this.all(query);
}
getAllByIds(ids: string[]) {
return this.all(this.select().whereIn('id', ids));
}
protected getAllBy(filters: any) {
const query = this.select().where(filters);
return this.all(query);
}
protected async getBy(filters: any) {
const query = this.select().where(filters).limit(1);
return this.get(query);
}
protected dump(entity: Entity) {
const data = {};
const raw = entity;
for (const field of this.columns) {
const fn = this.metadata.get(field.fieldName);
if (fn) {
data[field.fieldName] = fn(raw);
} else if (field.fieldName == 'data') {
data['data'] = this.stringify(raw);
} else {
data[field.fieldName] = raw[field.fieldName];
}
}
return data;
}
save(entity: Entity) {
return this.upsertOne(entity);
}
async getById(id: string): Promise<Entity | null> {
return this.getBy({ id: id });
}
async upsertOne(entity: Entity): Promise<void> {
await this.upsertMany([entity]);
}
async upsertMany(entities: Entity[]): Promise<void> {
if (entities.length === 0) {
return;
}
const batchSize = this.UPSERT_BATCH_SIZE;
for (let i = 0; i < entities.length; i += batchSize) {
const batch = entities.slice(i, i + batchSize);
await this.upsertBatch(batch);
}
}
private async upsertBatch(entities: Entity[]): Promise<void> {
const data = entities.map((entity) => this.dump(entity));
const keys = this.columns.map((c) => c.fieldName);
const values = data.map((d) => Object.values(d)).flat();
try {
this.db
.prepare(
`INSERT INTO ${this.table} (${keys.join(', ')})
VALUES ${data
.map(() => `(${keys.map(() => '?').join(', ')})`)
.join(', ')}
ON CONFLICT(id) DO UPDATE
SET ${keys.map((key) => `${key} = excluded.${key}`).join(', ')}`,
)
.run(values);
} catch (err) {
console.error(`Error upserting data: ${err}, values: ${values}`);
throw err;
}
}
protected async deleteBy(filters: any) {
const query = this.delete().where(filters);
await this.run(query);
}
async deleteAll() {
const query = this.delete();
await this.run(query);
}
async deleteById(id: string) {
await this.deleteBy({ id: id });
}
/**
* SQL helpers
*/
protected select() {
return this.knex.select().from(this.table);
}
protected delete() {
return this.knex.delete().from(this.table);
}
protected async all(query: any) {
const sql = query.toSQL().sql;
const bind = query.toSQL().bindings;
const rows: any[] = this.db.prepare(sql).all(bind);
return rows.map((row) => this.parse(row));
}
protected async get(query: any) {
const sql = query.toSQL().sql;
const bind = query.toSQL().bindings;
const row: any = this.db.prepare(sql).get(bind);
if (!row) {
return null;
}
return this.parse(row);
}
protected async run(query: any) {
const sql = query.toSQL().sql;
const bind = query.toSQL().bindings;
return this.db.prepare(sql).run(bind);
}
protected stringify(data: any): string {
return JSON.stringify(data);
}
protected parse(row: any) {
return JSON.parse(row.data);
}
protected pagination(query: any, pagination?: PaginationParams) {
const paginator = new this.Paginator(pagination);
return paginator.apply(query);
const knex = Knex({ client: 'better-sqlite3', useNullAsDefault: true });
const engine = new Sqlite3Engine(db);
super(engine, knex);
this.db = db;
}
}
@@ -1,32 +1,30 @@
import { Sqlite3SchemaValidation } from '@waha/core/engines/noweb/store/sqlite3/Sqlite3SchemaValidation';
import { ISessionMeRepository } from '@waha/core/storage/ISessionMeRepository';
import { LocalStore } from '@waha/core/storage/LocalStore';
import { Field, Index, Schema } from '@waha/core/storage/sqlite3/Schema';
import { SQLMeMigrations, SQLMeSchema } from '@waha/core/storage/sql/schemas';
import { Sqlite3KVRepository } from '@waha/core/storage/sqlite3/Sqlite3KVRepository';
import { MeInfo } from '@waha/structures/sessions.dto';
import * as path from 'path';
// eslint-disable-next-line @typescript-eslint/no-var-requires
const Database = require('better-sqlite3');
const SCHEMA = new Schema(
'me',
[new Field('id', 'TEXT'), new Field('data', 'TEXT')],
[new Index('me_id_index', ['id'])],
);
class SessionMeInfo {
id: string;
me?: MeInfo;
}
export class LocalSessionMeRepository
export class Sqlite3SessionMeRepository
extends Sqlite3KVRepository<SessionMeInfo>
implements ISessionMeRepository
{
get schema() {
return SQLMeSchema;
}
get migrations() {
return SQLMeMigrations;
}
constructor(store: LocalStore) {
const db = store.getWAHADatabase();
super(db, SCHEMA);
super(db);
}
upsertMe(sessionName: string, me: MeInfo): Promise<void> {
@@ -42,19 +40,8 @@ export class LocalSessionMeRepository
return this.deleteById(sessionName);
}
async init(): Promise<void> {
this.migrations();
this.validateSchema();
}
private migrations() {
this.db.exec(
'CREATE TABLE IF NOT EXISTS me (id TEXT PRIMARY KEY, data TEXT)',
);
this.db.exec('CREATE UNIQUE INDEX IF NOT EXISTS me_id_index ON me (id)');
}
private validateSchema() {
new Sqlite3SchemaValidation(SCHEMA, this.db).validate();
protected async validateSchema() {
const validation = new Sqlite3SchemaValidation(this.schema, this.db);
validation.validate();
}
}
Loaded 100 of 133 files, more files were not shown because too many files have changed in this diff. Show more