Compare commits

..
149 Commits
Author SHA1 Message Date
devlikepro 8302530954 [core] 2025.5.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 / 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-05-23 14:47:37 +07:00
devlikepro 6c271829aa [core] Up Dashboard 2025-05-23 14:47:37 +07:00
devlikepro 3b30d339ee [core] Up GOWS 2025-05-23 14:47:37 +07:00
devlikepro c5a268bdbd [core] Up NOWEB - lid support
fix #957
2025-05-23 14:47:36 +07:00
devlikepro 8457ea8e47 [core] Find a phone number by lid - do not require @lid 2025-05-23 14:47:36 +07:00
devlikepro 72d69eeab5 [core] Add lid to phone number mapping - all engines
fix #957
2025-05-23 14:47:35 +07:00
devlikepro 31885e41af [core] Use maxExecutionTime instead of timeout in async-lock to avoid "async-lock timed out in queue" error
fix #831
2025-05-23 14:47:34 +07:00
devlikepro e63726d18b [core] GOWS - WhatsApp Event Message 2025-05-23 14:47:33 +07:00
devlikepro 3d2ab45d25 [core] GOWS - Labels support
fix #922
2025-05-23 14:47:33 +07:00
devlikepro 246478e1d8 [core] GOWS - add send vcard
fix #902
2025-05-23 14:47:32 +07:00
devlikepro 9418de3b23 [core] add reply_to to sendContactVcard 2025-05-23 14:47:32 +07:00
devlikepro c6b68cad37 [core] add types for toVcard 2025-05-23 14:47:32 +07:00
devlikepro ef36e4d3ac [core] 2025.5.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-05-16 18:33:10 +07:00
devlikepro ba2ddd62ad [core] NOWEB - fix poll.vote event
fix #960
2025-05-16 18:33:10 +07:00
devlikepro 6a03e1488c [core] 2025.5.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-05-16 17:42:09 +07:00
devlikepro cabd2ed562 [core] GOWS - pairing with official account
fix #864
2025-05-16 17:42:09 +07:00
devlikepro 4a9296276d [core] NOWEB - fix pairing code issue
fix #961
2025-05-16 17:42:09 +07:00
devlikepro 1fd84d824c [core] senderKeyDistributionMessage - ignore undefined messages 2025-05-16 17:42:08 +07:00
devlikepro faa55ffe2d [core] 2025.5.1
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-05-10 15:55:42 +07:00
devlikepro d45af4191f [core] WEBJS - properly close remote auth storage 2025-05-10 15:55:41 +07:00
devlikepro 9f8dead8b8 [core] NOWEB + SQL - speedup Contacts upsert
fix #952
2025-05-10 15:55:41 +07:00
devlikepro 564037cd53 [core] NOWEB + SQL - speedup Chats upsert
fix #952
2025-05-10 15:55:40 +07:00
devlikepro 434de6292c [core] SQL - Fix inserting duplicate entities in one batch 2025-05-10 15:55:40 +07:00
devlikepro a12e6990e5 [core] WEBJS - fix sorting groups in /chats and /chats/overview
fix #915
2025-05-10 15:55:40 +07:00
devlikepro ce0c4a094b [core] WEBJS - fix groups participant management (add/remove)
fix #944
2025-05-10 15:55:39 +07:00
devlikepro e36728ec08 [core] NOWEB - use ILogger 2025-05-10 15:55:39 +07:00
devlikepro 2c1f3090a0 [core] NOWEB - fix participant for message.ack in groups 2025-05-10 15:55:39 +07:00
devlikepro 0c6d4b3581 [core] NOWEB - update status only if not old 2025-05-10 15:55:39 +07:00
devlikepro 1d5b9eb241 [core] NOWEB - DistinctAck
fix #948
2025-05-10 15:55:38 +07:00
devlikepro b0567dafe3 [core] DistinctAck function 2025-05-10 15:55:37 +07:00
devlikepro 518f78f067 [core] Add StatusToAck and AckToStatus methods 2025-05-10 15:55:37 +07:00
devlikepro 47a2c21824 [core] Up GOWS
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-05-05 13:08:45 +07:00
devlikepro aca5547965 [core] 2025.4.2 2025-05-05 13:08:45 +07:00
devlikepro 0e07876630 [core] Use POST in /api/chats/messages/read 2025-05-05 13:08:45 +07:00
devlikepro a43901b4a4 [core] /api/sendSeen - If no message id provided - switch to readChatMessages 2025-05-05 13:08:44 +07:00
devlikepro 6515fac16d [core] WEBJS - add message._data.receipts in group/status messages 2025-05-05 13:08:44 +07:00
devlikepro fdaa7e9f48 [core] Up Dashboard 2025-05-05 13:08:44 +07:00
devlikepro cd779850de [core] GOWS - fix missing DEVICE ack for groups 2025-05-05 13:08:44 +07:00
devlikepro ef939f4357 [core] Add "message.ack" event for groups and status
fix #495
fix #900
2025-05-05 13:08:41 +07:00
devlikepro bfb9dd7009 [core] WEBJS - Update chrome 2025-05-05 13:08:39 +07:00
devlikepro f282f14eff [core] WEBJS - Update puppeter 2025-05-05 13:08:38 +07:00
devlikepro 992b289500 [core] WEBJS - catch puppeter ProtocolError and "Promise was collected" and restart the browser
fix #244 - Promise was collected
fix #714 - Promise was collected
fix #685 - ProtocolError
fix #936 - ProtocolError
2025-05-05 13:08:37 +07:00
devlikepro 9dcdebcab2 [core] Add /api/chats/messages/read
fix #783
2025-05-05 13:08:36 +07:00
devlikepro c0b8bacab0 [core] Fix missing first message - GOWS, NOWEB 2025-05-05 13:08:35 +07:00
devlikepro f96e6420fc [core] Get message by id - filter by 'all' 2025-05-05 13:08:35 +07:00
devlikepro d7f97480cc [core] Do not send seen on empty keys 2025-05-05 13:08:35 +07:00
devlikepro 26df41b571 [core] Add isJidCus method 2025-05-05 13:08:34 +07:00
devlikepro 4ca336017d [core] GOWS - /api/sendSeen messageIds 2025-05-05 13:08:33 +07:00
devlikepro 94252ea54c [core] NOWEB - /api/sendSeen messageIds 2025-05-05 13:08:32 +07:00
devlikepro f8220fec1f [core] GOWS - send seen use messageIds 2025-05-05 13:08:32 +07:00
devlikepro a5ded396c9 [core] Add "audio-decode" to dependencies 2025-05-05 13:08:31 +07:00
devlikepro c457cf8064 [core] Add "node-cache" to dependencies 2025-05-05 13:08:30 +07:00
devlikepro 3079f90118 [core] Up NOWEB 2025-05-05 13:08:29 +07:00
devlikepro 4fbb331015 [core] Copy-paste in-memory store
Got removed from the upstream
2025-05-05 13:08:28 +07:00
devlikepro 640c0d305c [core] - Add 'filter.ack' to /messages 2025-05-05 13:08:26 +07:00
devlikepro 0b044c67bb [core] NOWEB fix ListMessages "Cannot read properties of undefined (reading 'protocolMessage')"
fix #932
2025-05-05 13:08:26 +07:00
devlikepro 7a37ba8145 [core] WEBJS - stop processing if we reach out the earliest message we need 2025-05-05 13:08:25 +07:00
devlikepro b447863d9f [core] NOWEB - update message status on /api/sendSeen API call
fix #635
2025-05-05 13:08:25 +07:00
devlikepro 6dd7e14f6f [core] NOWEB - set DEVICE message status for incoming messages if no status found 2025-05-05 13:08:24 +07:00
devlikepro b09ef98dce [core] default limit to 10 in messages 2025-05-05 13:08:24 +07:00
devlikepro ff1cf8a288 [core] gpt 2025-05-05 13:08:24 +07:00
devlikepro 603793662a [core] Wait 60 times in STARTING, approximately 2 minutes
fix #904
2025-05-05 13:08:24 +07:00
devlikepro 4cdab0dbc7 [core] admin/admin 2025-04-16 09:26:32 +07:00
devlikepro d745279fec [core] 2025.4.1
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-04-09 16:05:03 +07:00
devlikepro df34c9f2fd [core] Up GOWS 2025-04-09 16:05:03 +07:00
devlikepro 37da9aabe3 [core] Add POST /api/send/link-review
fix #880
fix #596
2025-04-09 16:05:02 +07:00
devlikepro 3a2205147c [core] API - deprecate /sendLinkPreview 2025-04-09 16:05:02 +07:00
devlikepro 5c6acaf10a [core] NOWEB - filter out sender key distribution msg
fix #891
2025-04-09 16:05:02 +07:00
devlikepro d916f2dea5 [core] NOWEB - return error in case of malformed xml 2025-04-09 16:05:01 +07:00
devlikepro 254a1aedcb [core] NOWEB - send reaction to channel
fix #889
2025-04-09 16:05:01 +07:00
devlikepro dce3175388 [core] GOWS - fix sending reaction to DM/Groups for other participants 2025-04-09 16:05:01 +07:00
devlikepro e2b6b45418 [core] GOWS - send reaction to channel
fix #889
2025-04-09 16:05:01 +07:00
devlikepro e210d1e9fc [core] 2025.3.3
Release / WEBJS - chrome - amd64 - chrome (push) Waiting to run
Release / NOWEB - none - linux/arm64 - noweb-arm (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
2025-03-31 15:39:56 +07:00
devlikepro df0f449e13 [core] use isJidBroadcast 2025-03-31 15:39:56 +07:00
devlikepro 895105c900 [core] Ignore @broadcast for picture
fix #878
fix #881
2025-03-31 15:39:56 +07:00
devlikepro 209538709b [core] Up Dashboard 2025-03-31 15:39:56 +07:00
devlikepro ef7c35286b [core] NOWEB - revert presence optimizations 2025-03-31 15:39:56 +07:00
devlikepro 70af0828cb [core] NOWEB - cache presence for 1 minute 2025-03-31 15:39:56 +07:00
devlikepro ef18e311b1 [core] 2025.3.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-03-27 14:36:38 +07:00
devlikepro 68de84973a [core] GOWS - extract replyTo
fix #876
2025-03-27 14:36:38 +07:00
devlikepro ef6d4f9998 [core] Add _data (original message) to replyTo field 2025-03-27 14:36:37 +07:00
devlikepro 9f5413963b [core] NOWEB - log errors on profile picture change
fix #873
2025-03-27 14:36:36 +07:00
devlikepro ec7c0a5cb7 [core] check session name in waitUntilStatus 2025-03-27 14:36:36 +07:00
devlikepro f82bd9eadf [core] no browser for non webjs based engines 2025-03-27 14:36:35 +07:00
devlikepro 843c064ebe [core] Up GOWS 2025-03-27 14:36:35 +07:00
devlikepro 8a52e1d1d0 [core] Up Dashboard 2025-03-27 14:36:34 +07:00
devlikepro 8ec220dc9a [core] Status to 50K and more contacts
fix #858
2025-03-27 14:36:33 +07:00
devlikepro 9c4b9f4433 [core] NOWEB - fix scan qr issue 2025-03-27 14:36:33 +07:00
devlikepro 6059a29660 [core] fix message reactions empty for channels preview 2025-03-27 14:36:32 +07:00
devlikepro e65e6764fa [core] NOWEB - presence optimizations
Apply 1 hour ttl for presence information
2025-03-27 14:36:32 +07:00
devlikepro 1e77cb234a [core] NOWEB - use {} for presence 2025-03-27 14:36:31 +07:00
devlikepro 4a357087d1 [core] NOWEB - do not restart after SCAN_QR_CODE expires 2025-03-27 14:36:31 +07:00
devlikepro 6a3823cdff [core] NOWEB - fix the case when STARTING session fails the container
fix #821
fix #860
fix #869
2025-03-27 14:36:30 +07:00
devlikepro c2372ce96e [core] GOWS - fix get contact issue
fix #866
2025-03-27 14:36:30 +07:00
devlikepro 0f5e79b163 [core] Enable all features in Global Webhook
fix #855
2025-03-27 14:36:30 +07:00
devlikepro fa4026c83f [core] Allow only http/https for webhooks 2025-03-27 14:36:30 +07:00
devlikepro 3e4d66ca27 [core] Reply to button
fix #848
2025-03-27 14:36:30 +07:00
devlikepro 2ec94dc13c [core] 2025.3.1
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-03-16 14:05:33 +07:00
devlikepro 6b11e08f3d [core] UP GOWS - v1.0.1 2025-03-16 14:05:33 +07:00
devlikepro e884e56b47 [core] Up Dashboard 2025-03-16 14:05:32 +07:00
devlikepro 2d74affed4 [core] NOWEB - send message save ids 2025-03-16 14:05:31 +07:00
devlikepro 3572b17049 [core] NOWEB up engine 2025-03-16 14:05:30 +07:00
devlikepro 4ee7091cd1 [core] Add /api/groups/count
fix #752
2025-03-16 14:05:29 +07:00
devlikepro ff5e89d97c [core] GOWS - no sender key dis message 2025-03-16 14:05:29 +07:00
devlikepro c4e82afc0c [core] log as debug engine info errors (not working perfectly in WEBJS)
fix #850
2025-03-16 14:05:29 +07:00
devlikepro 52c5d85487 [core] source: api|app for messages.
fix #787
2025-03-16 14:05:28 +07:00
devlikepro fd0438f80b [core] GOWS send _data with full message 2025-03-16 14:05:26 +07:00
devlikepro cb5781ca65 [core] NOWEB disappearing messages
fix #736
2025-03-16 14:05:25 +07:00
devlikepro 4d8aad9dfe [core] Remove calls from message events
fix #840
2025-03-16 14:05:24 +07:00
devlikepro 39429a2e44 [core] Add event.timestamp
fix #755
2025-03-16 14:05:23 +07:00
devlikepro 0a3514d81e [core] GOWS reply on message
fix #799
2025-03-16 14:05:22 +07:00
devlikepro 4e5e0a7c34 [core] GOWS fix filename in sendFile
fix #853
2025-03-16 14:05:22 +07:00
devlikepro f6cc6a9bef [core] GOWS edit message
fix #797
2025-03-16 14:05:20 +07:00
devlikepro 2e545e9fb9 [core] GOWS delete status message
fix #754
2025-03-16 14:05:19 +07:00
devlikepro d49ef64e7e [core] API - description for message id 2025-03-16 14:05:18 +07:00
devlikepro b1e6eaef90 [core] GOWS - delete message
fix #796
2025-03-16 14:05:16 +07:00
devlikepro 3420d0199b [core] NOWEB - fix getting message by id 2025-03-16 14:05:16 +07:00
devlikepro 671db35571 [core] NOWEB - link preview HQ 2025-03-16 14:05:16 +07:00
devlikepro d724f87726 [core] GOWS - link preview 2025-03-16 14:05:14 +07:00
devlikepro 5ab8565205 [core] NOWEB - link preview in status 2025-03-16 14:05:14 +07:00
devlikepro 660c567d1b [core] GOWS - link preview
fix #763
2025-03-16 14:05:14 +07:00
devlikepro 2c68d13c56 [core] Send session name back
fix #835
2025-03-16 14:05:14 +07:00
devlikeapro b9fc155b4f Update feature_request.md 2025-02-28 10:55:19 +07:00
devlikeapro 8dd5aefc81 Update feature_request.md 2025-02-28 10:55:05 +07:00
devlikeapro f9aaf5dcef Update bug_report.md 2025-02-28 10:53:52 +07:00
devlikepro 34a6e773df [core] 2025.2.8
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-28 10:22:33 +07:00
devlikepro f0beb03344 [core] Link preview on edit
fix #833
2025-02-28 10:22:33 +07:00
devlikepro 1ff9a81ab3 [core] Fix channels API
fix #832
2025-02-28 10:22:33 +07:00
devlikepro 9b1771c693 [core] 2025.2.7
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-27 14:34:23 +07:00
devlikepro 823e604737 [core] GOWS v1.0.0 2025-02-27 14:34:23 +07:00
devlikepro 97a31bdf17 [core] NOWEB - ignore EPHEMERAL_SYNC_RESPONSE 2025-02-27 14:34:22 +07:00
devlikepro ee7a198ae6 [core] GOWS - startTyping/stopTyping 2025-02-27 14:34:22 +07:00
devlikepro f143a51010 [core] group id in picture 2025-02-27 14:34:21 +07:00
devlikepro e483185b2c [core] fields.exclude optional 2025-02-27 14:34:21 +07:00
devlikepro 40e1c75746 [core] GOWS - v1.0.0-dev.1 2025-02-27 14:34:21 +07:00
devlikepro 456bd4693c [core] GOWS - fetch proto from ref with no v 2025-02-27 14:34:21 +07:00
devlikepro 5ac9c0f760 [core] NOWEB - no auto restart 2025-02-27 14:34:20 +07:00
devlikepro f621d84b83 [core] Exclude groups participants
fix #242
2025-02-27 14:34:20 +07:00
devlikepro 0dd001e95a [core] GOWS Groups API 2025-02-27 14:34:19 +07:00
devlikepro 69b5235db9 [core] Groups API - return 200 on POST in not create API 2025-02-27 14:34:19 +07:00
devlikepro a673d63b33 [core] Groups Picture API - Get, Update, Remove
fix #802
2025-02-27 14:34:18 +07:00
devlikepro 36485e10f2 [core] NOWEB add admin only / messages admin settings API
fix #695
fix #808
2025-02-27 14:34:18 +07:00
devlikepro 8a1a062127 [core] Place Channels Search API at the end 2025-02-27 14:34:18 +07:00
devlikepro 5961e0de08 [core] Fix group.participants undefined
fix #810
2025-02-27 14:34:18 +07:00
devlikepro 60bda521a0 [core] Wait until session is running and check if exist after 2025-02-27 14:34:18 +07:00
devlikepro 49f63c0f0d [core] 2025.2.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
fix #820
2025-02-21 10:06:27 +07:00
devlikepro 33910aedb8 [core] 2025.2.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-02-19 09:20:27 +07:00
devlikepro 3359a81a14 [core] NOWEB add message.revoked event
fix #813
2025-02-19 09:20:26 +07:00
devlikepro e27f850e68 [core] NOWEB Fix body extract from messages
fix #813
2025-02-19 09:20:26 +07:00
89 changed files with 15361 additions and 1007 deletions

No files matched your search

+5 -5
View File
@@ -23,9 +23,9 @@ WAHA_BASE_URL=http://localhost:3000
#
# https://waha.devlike.pro/docs/how-to/security/
#
WHATSAPP_API_KEY=321
WAHA_DASHBOARD_USERNAME=waha
WAHA_DASHBOARD_PASSWORD=waha
WHATSAPP_API_KEY=admin
WAHA_DASHBOARD_USERNAME=admin
WAHA_DASHBOARD_PASSWORD=admin
#WHATSAPP_SWAGGER_USERNAME=admin
#WHATSAPP_SWAGGER_PASSWORD=admin
@@ -35,8 +35,8 @@ WAHA_DASHBOARD_PASSWORD=waha
# https://waha.devlike.pro/docs/how-to/waha-dashboard/#configuration
# Enabled by default
#WAHA_DASHBOARD_ENABLED=true
#WAHA_DASHBOARD_USERNAME=waha
#WAHA_DASHBOARD_PASSWORD=waha
#WAHA_DASHBOARD_USERNAME=admin
#WAHA_DASHBOARD_PASSWORD=admin
#
# Logging
+2 -1
View File
@@ -1,9 +1,10 @@
---
name: Bug report
about: Create a report to help us improve
title: '[BUG][WEBJS] - '
title: '[WEBJS] - '
labels: bug
assignees: ''
type: 'BUG'
---
### Describe the bug
+2 -1
View File
@@ -1,9 +1,10 @@
---
name: Feature request
about: Suggest an idea for this project
title: '[Feature Request]'
title: ''
labels: ''
assignees: ''
type: 'Feature'
---
**Is your feature request related to a problem? Please describe.** I'm always
+2 -2
View File
@@ -66,7 +66,7 @@ RUN \
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} && \
wget -O /go/gows/bin/gows https://github.com/${GOWS_GITHUB_REPO}/releases/download/${GOWS_SHA}/gows-${ARCH} && \
chmod +x /go/gows/bin/gows
@@ -131,7 +131,7 @@ RUN if [ "$USE_BROWSER" = "chromium" ]; then \
# Install Chrome
# Available versions:
# https://www.ubuntuupdates.org/package/google_chrome/stable/main/base/google-chrome-stable
ARG CHROME_VERSION="130.0.6723.69-1"
ARG CHROME_VERSION="135.0.7049.114-1"
RUN if [ "$USE_BROWSER" = "chrome" ]; then \
wget --no-verbose -O /tmp/chrome.deb https://dl.google.com/linux/chrome/deb/pool/main/g/google-chrome-stable/google-chrome-stable_${CHROME_VERSION}_amd64.deb \
&& apt-get update \
+1 -1
View File
@@ -35,7 +35,7 @@ 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-6712
. ${NVM_DIR}/nvm.sh && nvm exec yarn up @adiwajshing/baileys@github:devlikeapro/Baileys#fork-master-2025-05-01
up-noweb-libsignal:
. ${NVM_DIR}/nvm.sh && nvm exec yarn up libsignal@github:devlikeapro/libsignal-node#fork-master
+2 -2
View File
@@ -11,8 +11,8 @@ services:
- './.media:/app/.media'
environment:
- WHATSAPP_API_KEY=321
- WAHA_DASHBOARD_USERNAME=waha
- WAHA_DASHBOARD_PASSWORD=waha
- WAHA_DASHBOARD_USERNAME=admin
- WAHA_DASHBOARD_PASSWORD=admin
- WHATSAPP_DEFAULT_ENGINE=WEBJS
- WAHA_PRINT_QR=False
- WAHA_MEDIA_STORAGE=LOCAL
+2 -2
View File
@@ -36,8 +36,8 @@ services:
# Username and password for WAHA Dashboard
# you can use the same username and password for both Swagger and Dashboard
# Default - waha / waha
- WAHA_DASHBOARD_USERNAME=waha
- WAHA_DASHBOARD_PASSWORD=waha
- WAHA_DASHBOARD_USERNAME=admin
- WAHA_DASHBOARD_PASSWORD=admin
# Engine specific environment variables
# - WAHA_WEBJS_WEB_VERSION=2.2412.54-videofix
restart: always
+4 -2
View File
@@ -26,7 +26,7 @@
"gows:proto": "yarn gows:proto:fetch && yarn gows:proto:build"
},
"dependencies": {
"@adiwajshing/baileys": "github:devlikeapro/Baileys#fork-master-channels-6712",
"@adiwajshing/baileys": "github:devlikeapro/Baileys#fork-master-2025-05-01",
"@adiwajshing/keyed-db": "^0.2.4",
"@aws-sdk/client-s3": "^3.633.0",
"@aws-sdk/s3-request-presigner": "^3.633.0",
@@ -47,6 +47,7 @@
"adm-zip": "0.5.10",
"agentkeepalive": "^4.5.0",
"async-lock": "^1.4.1",
"audio-decode": "^2.2.2",
"axios": "^1.7.7",
"axios-retry": "^4.5.0",
"better-sqlite3": "11.3.0",
@@ -69,6 +70,7 @@
"mime-types": "^2.1.27",
"mongodb": "6.9.0",
"nestjs-pino": "^4.1.0",
"node-cache": "5.1.2",
"passport": "^0.6.0",
"passport-headerapikey": "^1.2.2",
"pg": "^8.13.1",
@@ -94,7 +96,7 @@
"resolutions": {
"bufferutil": "^4.0.8",
"ws": "^8.18.0",
"puppeteer": "^23.6.0",
"puppeteer": "^24.7.2",
"whatwg-url": "13.0.0",
"libsignal": "github:devlikeapro/libsignal-node#fork-master"
},
+22
View File
@@ -0,0 +1,22 @@
#!/bin/bash
# Set source and output file
SOURCE_DIR="src"
OUTPUT_DIR="$HOME/Downloads/chatgpt"
OUTPUT_FILE="$OUTPUT_DIR/waha.ts"
# Create or clear the output file
rm -f "$OUTPUT_FILE"
touch "$OUTPUT_FILE"
# Find all .md files in the source directory and combine them
find "$SOURCE_DIR" -type f -name "*.ts" | while read -r file; do
# Add file name to OUTPUT_FILE
echo -e "\n-------" >> "$OUTPUT_FILE"
echo "File: $file" >> "$OUTPUT_FILE"
echo -e "-------\n" >> "$OUTPUT_FILE"
# Append the content of the file to the output file
cat "$file" >> "$OUTPUT_FILE"
done
echo "All .ts files combined into $OUTPUT_FILE with separators."
+1 -1
View File
@@ -53,7 +53,7 @@ function cleanDirectory(directory, suffix) {
// 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 url = `https://github.com/${repo}/releases/download/${ref}/${file}`;
const filePath = path.join(directory, file);
try {
const response = await axios.get(url, { responseType: 'arraybuffer' });
+80 -80
View File
@@ -12,6 +12,7 @@ import {
} from '@nestjs/common';
import { ApiOperation, ApiParam, ApiSecurity, ApiTags } from '@nestjs/swagger';
import { ChannelsInfoServiceCore } from '@waha/core/services/ChannelsInfoServiceCore';
import { isJidNewsletter } from '@waha/core/utils/jids';
import {
SessionApiParam,
WorkingSessionParam,
@@ -35,7 +36,6 @@ import {
import { SessionManager } from '../core/abc/manager.abc';
import {
isNewsletter,
parseChannelInviteLink,
WhatsappSession,
} from '../core/abc/session.abc';
@@ -49,84 +49,6 @@ export class ChannelsController {
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
@ApiOperation({ summary: 'Get list of know channels' })
@@ -171,7 +93,7 @@ export class ChannelsController {
@WorkingSessionParam session: WhatsappSession,
@Param('id') id: string,
): Promise<Channel> {
if (isNewsletter(id)) {
if (isJidNewsletter(id)) {
return session.channelsGetChannel(id);
} else {
const inviteCode = parseChannelInviteLink(id);
@@ -179,6 +101,39 @@ export class ChannelsController {
}
}
@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 (isJidNewsletter(code)) {
const channel = await session.channelsGetChannel(code);
code = parseChannelInviteLink(channel.invite);
}
const inviteCode = parseChannelInviteLink(code);
return session.previewChannelMessages(inviteCode, query);
}
@Post(':id/follow')
@SessionApiParam
@NewsletterIdApiParam
@@ -222,4 +177,49 @@ export class ChannelsController {
): Promise<void> {
return session.channelsUnmuteChannel(id);
}
@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();
}
}
+18
View File
@@ -1,3 +1,4 @@
import { isJidGroup } from '@adiwajshing/baileys';
import {
Body,
Controller,
@@ -31,6 +32,9 @@ import {
GetChatMessagesQuery,
OverviewPaginationParams,
PinMessageRequest,
ReadChatMessagesQuery,
ReadChatMessagesResponse,
transformAck,
} from '../structures/chats.dto';
import { EditMessageRequest } from '../structures/chatting.dto';
@@ -100,9 +104,23 @@ class ChatsController {
@WorkingSessionParam session: WhatsappSession,
@Param('chatId') chatId: string,
) {
filter = transformAck(filter);
return session.getChatMessages(chatId, query, filter);
}
@Post(':chatId/messages/read')
@SessionApiParam
@ApiOperation({ summary: 'Read unread messages in the chat' })
@ChatIdApiParam
@UsePipes(new ValidationPipe({ transform: true, whitelist: true }))
readChatMessages(
@Query() query: ReadChatMessagesQuery,
@WorkingSessionParam session: WhatsappSession,
@Param('chatId') chatId: string,
): Promise<ReadChatMessagesResponse> {
return session.readChatMessages(chatId, query);
}
@Get(':chatId/messages/:messageId')
@SessionApiParam
@ApiOperation({ summary: 'Gets message by id' })
+53 -7
View File
@@ -10,7 +10,11 @@ import {
} from '@nestjs/common';
import { ApiOperation, ApiSecurity, ApiTags } from '@nestjs/swagger';
import { WAHAValidationPipe } from '@waha/nestjs/pipes/WAHAValidationPipe';
import { GetChatMessagesFilter } from '@waha/structures/chats.dto';
import {
GetChatMessagesFilter,
ReadChatMessagesQuery,
transformAck,
} from '@waha/structures/chats.dto';
import { SendButtonsRequest } from '@waha/structures/chatting.buttons.dto';
import { SessionManager } from '../core/abc/manager.abc';
@@ -18,10 +22,12 @@ import {
ChatRequest,
CheckNumberStatusQuery,
GetMessageQuery,
MessageButtonReply,
MessageContactVcardRequest,
MessageFileRequest,
MessageForwardRequest,
MessageImageRequest,
MessageLinkCustomPreviewRequest,
MessageLinkPreviewRequest,
MessageLocationRequest,
MessagePollRequest,
@@ -94,6 +100,25 @@ export class ChattingController {
return whatsapp.sendVideo(request);
}
@Post('/send/link-custom-preview')
@ApiOperation({
summary: 'Send a text message with a CUSTOM link preview.',
description:
'You can use regular /api/sendText if you wanna send auto-generated link preview.',
})
@UsePipes(new WAHAValidationPipe())
async sendLinkCustomPreview(
@Body() request: MessageLinkCustomPreviewRequest,
): Promise<any> {
const whatsapp = await this.manager.getWorkingSession(request.session);
if (!request.text.includes(request.preview.url)) {
throw new Error(
'"text" must include the URL provided in the "preview.url"',
);
}
return whatsapp.sendLinkCustomPreview(request);
}
@Post('/sendButtons')
@ApiOperation({
summary: 'Send buttons (interactive message)',
@@ -115,6 +140,15 @@ export class ChattingController {
@Post('/sendSeen')
async sendSeen(@Body() chat: SendSeenRequest) {
const hasMessageId = chat.messageIds?.length > 0 || Boolean(chat.messageId);
if (!hasMessageId) {
const whatsapp = await this.manager.getWorkingSession(chat.session);
const query: ReadChatMessagesQuery = {
messages: null,
days: 7,
};
return whatsapp.readChatMessages(chat.chatId, query);
}
const whatsapp = await this.manager.getWorkingSession(chat.session);
return whatsapp.sendSeen(chat);
}
@@ -165,18 +199,22 @@ export class ChattingController {
return whatsapp.sendLocation(request);
}
@Post('/sendLinkPreview')
async sendLinkPreview(@Body() request: MessageLinkPreviewRequest) {
const whatsapp = await this.manager.getWorkingSession(request.session);
return whatsapp.sendLinkPreview(request);
}
@Post('/sendContactVcard')
async sendContactVcard(@Body() request: MessageContactVcardRequest) {
const whatsapp = await this.manager.getWorkingSession(request.session);
return whatsapp.sendContactVCard(request);
}
@Post('/send/buttons/reply')
@ApiOperation({
summary: 'Reply on a button message',
})
@UsePipes(new ValidationPipe({ transform: true, whitelist: true }))
async sendButtonsReply(@Body() request: MessageButtonReply) {
const whatsapp = await this.manager.getWorkingSession(request.session);
return whatsapp.sendButtonsReply(request);
}
@Get('/sendText')
@ApiOperation({ summary: 'Send a text message', deprecated: true })
async sendTextGet(@Query() query: MessageTextQuery) {
@@ -198,6 +236,7 @@ export class ChattingController {
@Query() query: GetMessageQuery,
@Query() filter: GetChatMessagesFilter,
) {
filter = transformAck(filter);
const whatsapp = await this.manager.getWorkingSession(query.session);
return whatsapp.getChatMessages(query.chatId, query, filter);
}
@@ -225,4 +264,11 @@ export class ChattingController {
const whatsapp = await this.manager.getWorkingSession(request.session);
return whatsapp.reply(request);
}
@Post('/sendLinkPreview')
@ApiOperation({ deprecated: true })
async sendLinkPreview_DEPRECATED(@Body() request: MessageLinkPreviewRequest) {
const whatsapp = await this.manager.getWorkingSession(request.session);
return whatsapp.sendLinkPreview(request);
}
}
+50
View File
@@ -0,0 +1,50 @@
import {
Body,
Controller,
Param,
Post,
UsePipes,
ValidationPipe,
} from '@nestjs/common';
import { ApiOperation, ApiSecurity, ApiTags } from '@nestjs/swagger';
import { WhatsappSession } from '@waha/core/abc/session.abc';
import {
SessionApiParam,
WorkingSessionParam,
} from '@waha/nestjs/params/SessionApiParam';
import { SessionManager } from '../core/abc/manager.abc';
import {
EventCancelRequest,
EventMessageRequest,
} from '../structures/events.dto';
import { WAMessage } from '../structures/responses.dto';
@ApiSecurity('api_key')
@Controller('api/:session/events')
@ApiTags('📅 Events')
export class EventsController {
constructor(private manager: SessionManager) {}
@Post()
@ApiOperation({ summary: 'Send an event message' })
@UsePipes(new ValidationPipe())
@SessionApiParam
async sendEvent(
@WorkingSessionParam session: WhatsappSession,
@Body() request: EventMessageRequest,
): Promise<WAMessage> {
return session.sendEvent(request);
}
// @Post(':id/cancel')
// @ApiOperation({ summary: 'Cancel an event by ID' })
// @UsePipes(new ValidationPipe())
// @SessionApiParam
// async cancelEvent(
// @WorkingSessionParam session: WhatsappSession,
// @Param('id') id: string,
// ): Promise<WAMessage> {
// return session.cancelEvent(id);
// }
}
+130 -56
View File
@@ -3,6 +3,8 @@ import {
Controller,
Delete,
Get,
HttpCode,
HttpStatus,
Param,
Post,
Put,
@@ -16,12 +18,20 @@ import {
SessionApiParam,
WorkingSessionParam,
} from '@waha/nestjs/params/SessionApiParam';
import { CountResponse, Result } from '@waha/structures/base.dto';
import {
ChatPictureQuery,
ChatPictureResponse,
} from '@waha/structures/chats.dto';
import { ProfilePictureRequest } from '@waha/structures/profile.dto';
import { SessionManager } from '../core/abc/manager.abc';
import { parseGroupInviteLink, WhatsappSession } from '../core/abc/session.abc';
import {
CreateGroupRequest,
DescriptionRequest,
GroupField,
GroupsListFields,
GroupsPaginationParams,
JoinGroupRequest,
JoinGroupResponse,
@@ -58,6 +68,7 @@ export class GroupsController {
}
@Post('join')
@HttpCode(HttpStatus.OK)
@SessionApiParam
@ApiOperation({ summary: 'Join group via code' })
async joinGroup(
@@ -73,14 +84,32 @@ export class GroupsController {
@SessionApiParam
@ApiOperation({ summary: 'Get all groups.' })
@UsePipes(new ValidationPipe({ transform: true, whitelist: true }))
getGroups(
async getGroups(
@WorkingSessionParam session: WhatsappSession,
@Query() pagination: GroupsPaginationParams,
@Query() fields: GroupsListFields,
) {
return session.getGroups(pagination);
let groups: any = await session.getGroups(pagination);
groups = session.filterGroupsFields(groups, fields);
return groups;
}
@Get('/count')
@SessionApiParam
@ApiOperation({ summary: 'Get the number of groups.' })
@UsePipes(new ValidationPipe({ transform: true, whitelist: true }))
async getGroupsCount(
@WorkingSessionParam session: WhatsappSession,
): Promise<CountResponse> {
const data: any = await session.getGroups({});
const groups: any[] = Array.isArray(data) ? data : Object.values(data);
return {
count: groups.length,
};
}
@Post('refresh')
@HttpCode(HttpStatus.OK)
@SessionApiParam
@ApiOperation({ summary: 'Refresh groups from the server.' })
@UsePipes(new ValidationPipe({ transform: true, whitelist: true }))
@@ -99,6 +128,100 @@ export class GroupsController {
return session.getGroup(id);
}
@Delete(':id')
@SessionApiParam
@GroupIdApiParam
@ApiOperation({ summary: 'Delete the group.' })
deleteGroup(
@WorkingSessionParam session: WhatsappSession,
@Param('id') id: string,
) {
return session.deleteGroup(id);
}
@Post(':id/leave')
@HttpCode(HttpStatus.OK)
@SessionApiParam
@GroupIdApiParam
@ApiOperation({ summary: 'Leave the group.' })
leaveGroup(
@WorkingSessionParam session: WhatsappSession,
@Param('id') id: string,
) {
return session.leaveGroup(id);
}
@Get(':id/picture')
@SessionApiParam
@GroupIdApiParam
@ApiOperation({ summary: 'Get group picture' })
@UsePipes(new ValidationPipe({ transform: true, whitelist: true }))
async getChatPicture(
@WorkingSessionParam session: WhatsappSession,
@Param('id') id: string,
@Query() query: ChatPictureQuery,
): Promise<ChatPictureResponse> {
const url = await session.getContactProfilePicture(id, query.refresh);
return { url: url };
}
@Put(':id/picture')
@SessionApiParam
@GroupIdApiParam
@ApiOperation({ summary: 'Set group picture' })
async setPicture(
@Param('id') id: string,
@WorkingSessionParam session: WhatsappSession,
@Body() request: ProfilePictureRequest,
): Promise<Result> {
const success = await session.updateGroupPicture(id, request.file);
return { success: success ?? true };
}
@Delete(':id/picture')
@SessionApiParam
@GroupIdApiParam
@ApiOperation({ summary: 'Delete group picture' })
async deletePicture(
@Param('id') id: string,
@WorkingSessionParam session: WhatsappSession,
): Promise<Result> {
const success = await session.updateGroupPicture(id, null);
return { success: success || true };
}
@Put(':id/description')
@ApiOperation({
summary: 'Updates the group description.',
description:
'Returns "true" if the subject was properly updated. This can return "false" if the user does not have the necessary permissions.',
})
@SessionApiParam
@GroupIdApiParam
setDescription(
@WorkingSessionParam session: WhatsappSession,
@Param('id') id: string,
@Body() request: DescriptionRequest,
) {
return session.setDescription(id, request.description);
}
@Put(':id/subject')
@SessionApiParam
@GroupIdApiParam
@ApiOperation({
summary: 'Updates the group subject',
description:
'Returns "true" if the subject was properly updated. This can return "false" if the user does not have the necessary permissions.',
})
setSubject(
@WorkingSessionParam session: WhatsappSession,
@Param('id') id: string,
@Body() request: SubjectRequest,
) {
return session.setSubject(id, request.subject);
}
@Put(':id/settings/security/info-admin-only')
@SessionApiParam
@GroupIdApiParam
@@ -160,60 +283,6 @@ export class GroupsController {
return session.getMessagesAdminsOnly(id);
}
@Delete(':id')
@SessionApiParam
@GroupIdApiParam
@ApiOperation({ summary: 'Delete the group.' })
deleteGroup(
@WorkingSessionParam session: WhatsappSession,
@Param('id') id: string,
) {
return session.deleteGroup(id);
}
@Post(':id/leave')
@SessionApiParam
@GroupIdApiParam
@ApiOperation({ summary: 'Leave the group.' })
leaveGroup(
@WorkingSessionParam session: WhatsappSession,
@Param('id') id: string,
) {
return session.leaveGroup(id);
}
@Put(':id/description')
@ApiOperation({
summary: 'Updates the group description.',
description:
'Returns "true" if the subject was properly updated. This can return "false" if the user does not have the necessary permissions.',
})
@SessionApiParam
@GroupIdApiParam
setDescription(
@WorkingSessionParam session: WhatsappSession,
@Param('id') id: string,
@Body() request: DescriptionRequest,
) {
return session.setDescription(id, request.description);
}
@Put(':id/subject')
@SessionApiParam
@GroupIdApiParam
@ApiOperation({
summary: 'Updates the group subject',
description:
'Returns "true" if the subject was properly updated. This can return "false" if the user does not have the necessary permissions.',
})
setSubject(
@WorkingSessionParam session: WhatsappSession,
@Param('id') id: string,
@Body() request: SubjectRequest,
) {
return session.setSubject(id, request.subject);
}
@Get(':id/invite-code')
@SessionApiParam
@GroupIdApiParam
@@ -226,6 +295,7 @@ export class GroupsController {
}
@Post(':id/invite-code/revoke')
@HttpCode(HttpStatus.OK)
@SessionApiParam
@GroupIdApiParam
@ApiOperation({
@@ -251,6 +321,7 @@ export class GroupsController {
}
@Post(':id/participants/add')
@HttpCode(HttpStatus.OK)
@SessionApiParam
@GroupIdApiParam
@ApiOperation({ summary: 'Add participants' })
@@ -263,6 +334,7 @@ export class GroupsController {
}
@Post(':id/participants/remove')
@HttpCode(HttpStatus.OK)
@SessionApiParam
@GroupIdApiParam
@ApiOperation({
@@ -277,6 +349,7 @@ export class GroupsController {
}
@Post(':id/admin/promote')
@HttpCode(HttpStatus.OK)
@SessionApiParam
@GroupIdApiParam
@ApiOperation({ summary: 'Promote participants to admin users.' })
@@ -289,6 +362,7 @@ export class GroupsController {
}
@Post(':id/admin/demote')
@HttpCode(HttpStatus.OK)
@SessionApiParam
@GroupIdApiParam
@ApiOperation({ summary: 'Demotes participants to regular users.' })
+102
View File
@@ -0,0 +1,102 @@
import { isLidUser } from '@adiwajshing/baileys/lib/WABinary/jid-utils';
import {
Controller,
Get,
Param,
Query,
UnprocessableEntityException,
UsePipes,
ValidationPipe,
} from '@nestjs/common';
import { ApiOperation, ApiSecurity, ApiTags } from '@nestjs/swagger';
import { WhatsappSession } from '@waha/core/abc/session.abc';
import {
SessionApiParam,
WorkingSessionParam,
} from '@waha/nestjs/params/SessionApiParam';
import { CountResponse } from '@waha/structures/base.dto';
import {
LidsListQueryParams,
LidToPhoneNumber,
} from '@waha/structures/lids.dto';
import { PaginationParams, SortOrder } from '@waha/structures/pagination.dto';
import { SessionManager } from '../core/abc/manager.abc';
@ApiSecurity('api_key')
@Controller('api/:session/lids')
@ApiTags('👤 Contacts')
export class LidsController {
constructor(private manager: SessionManager) {}
@Get('/')
@SessionApiParam
@ApiOperation({ summary: 'Get all known lids to phone number mapping' })
@UsePipes(new ValidationPipe({ transform: true, whitelist: true }))
async getAll(
@WorkingSessionParam session: WhatsappSession,
@Query() params: LidsListQueryParams,
): Promise<Array<LidToPhoneNumber>> {
// Always lid
const pagination: PaginationParams = params;
pagination.sortBy = 'lid';
pagination.sortOrder = SortOrder.ASC;
const lids = await session.getAllLids(pagination);
return lids;
}
@Get('/count')
@SessionApiParam
@ApiOperation({ summary: 'Get the number of known lids' })
@UsePipes(new ValidationPipe({ transform: true, whitelist: true }))
async getLidsCount(
@WorkingSessionParam session: WhatsappSession,
): Promise<CountResponse> {
const count = await session.getLidsCount();
return {
count: count,
};
}
@Get('/:lid')
@SessionApiParam
@ApiOperation({ summary: 'Get phone number by lid' })
@UsePipes(new ValidationPipe({ transform: true, whitelist: true }))
async findPNByLid(
@WorkingSessionParam session: WhatsappSession,
@Param('lid') lid: string,
): Promise<LidToPhoneNumber> {
if (!lid.includes('@')) {
lid = lid + '@lid';
}
if (!isLidUser(lid)) {
throw new UnprocessableEntityException(
'Invalid LID - it must end with @lid',
);
}
const result = await session.findPNByLid(lid);
result.pn = result.pn || null;
return result;
}
@Get('/pn/:phoneNumber')
@SessionApiParam
@ApiOperation({ summary: 'Get lid by phone number (chat id)' })
@UsePipes(new ValidationPipe({ transform: true, whitelist: true }))
async findLIDByPhoneNumber(
@WorkingSessionParam session: WhatsappSession,
@Param('phoneNumber') phoneNumber: string,
): Promise<LidToPhoneNumber> {
if (isLidUser(phoneNumber)) {
return {
lid: phoneNumber,
pn: null,
};
}
const result = await session.findLIDByPhoneNumber(phoneNumber);
result.lid = result.lid || null;
return result;
}
}
+14 -1
View File
@@ -1,9 +1,10 @@
import { Body, Controller, Post } from '@nestjs/common';
import { Body, Controller, Get, Post } from '@nestjs/common';
import { ApiOperation, ApiSecurity, ApiTags } from '@nestjs/swagger';
import {
SessionApiParam,
WorkingSessionParam,
} from '@waha/nestjs/params/SessionApiParam';
import { NewMessageIDResponse } from '@waha/structures/chatting.dto';
import { SessionManager } from '../core/abc/manager.abc';
import { WhatsappSession } from '../core/abc/session.abc';
@@ -70,6 +71,18 @@ class StatusController {
) {
return session.deleteStatus(status);
}
@Get('new-message-id')
@SessionApiParam
@ApiOperation({
summary: 'Generate message ID you can use to batch contacts',
})
async getNewMessageId(
@WorkingSessionParam session: WhatsappSession,
): Promise<NewMessageIDResponse> {
const id = await session.generateNewMessageId();
return { id: id };
}
}
export { StatusController };
+18 -19
View File
@@ -1,13 +1,19 @@
import { Injectable } from '@nestjs/common';
import { Injectable, Logger, OnApplicationBootstrap } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { WAHAEvents } from '@waha/structures/enums.dto';
import { GlobalWebhookConfigConfig } from '@waha/core/config/GlobalWebhookConfig';
import { parseBool } from './helpers';
import { WebhookConfig } from './structures/webhooks.config.dto';
@Injectable()
export class WhatsappConfigService {
constructor(private configService: ConfigService) {}
export class WhatsappConfigService implements OnApplicationBootstrap {
private logger: Logger;
private webhookConfig: GlobalWebhookConfigConfig;
constructor(private configService: ConfigService) {
this.logger = new Logger('WhatsappConfigService');
this.webhookConfig = new GlobalWebhookConfigConfig(configService);
}
get schema() {
return this.configService.get('WHATSAPP_API_SCHEMA', 'http');
@@ -113,21 +119,7 @@ export class WhatsappConfigService {
}
getWebhookConfig(): WebhookConfig | undefined {
const url = this.getWebhookUrl();
const events = this.getWebhookEvents();
if (!url || events.length === 0) {
return undefined;
}
return { url: url, events: events };
}
private getWebhookUrl(): string | undefined {
return this.get('WHATSAPP_HOOK_URL');
}
private getWebhookEvents(): WAHAEvents[] {
const value = this.get('WHATSAPP_HOOK_EVENTS', '');
return value ? value.split(',') : [];
return this.webhookConfig.config;
}
getSessionMongoUrl(): string | undefined {
@@ -182,4 +174,11 @@ export class WhatsappConfigService {
const value = this.configService.get('WAHA_DEBUG_MODE', 'false');
return parseBool(value);
}
onApplicationBootstrap() {
const error = this.webhookConfig.validateConfig();
if (error) {
throw new Error(`Invalid global webhook config:\n${error}\n`);
}
}
}
+1
View File
@@ -71,6 +71,7 @@ export class SwaggerConfiguratorCore {
)
.addTag('👥 Groups', `Groups methods.<br>`)
.addTag('✅ Presence', `Presence information`)
.addTag('📅 Events', `Event Message`)
.addTag(
'🏷️ Labels',
'Labels - available only for WhatsApp Business accounts',
+38 -3
View File
@@ -46,9 +46,10 @@ export abstract class SessionManager
protected sessionWorkerRepository: ISessionWorkerRepository;
private lock: any;
WAIT_SESSION_RUNNING_INTERVAL = 500;
WAIT_SESSION_RUNNING_TIMEOUT = 5_000;
WAIT_STATUS_INTERVAL = 500;
WAIT_STATUS_TIMEOUT = 5_000;
LOCK_TIMEOUT = 10_000;
WAIT_STATUS_TIMEOUT = 10_000;
protected constructor(
protected log: PinoLogger,
@@ -57,7 +58,7 @@ export abstract class SessionManager
) {
this.lock = new AsyncLock({
maxPending: Infinity,
timeout: this.LOCK_TIMEOUT,
maxExecutionTime: 30_000,
});
this.log.setContext(SessionManager.name);
}
@@ -148,6 +149,37 @@ export abstract class SessionManager
sessionName: string,
expected: WAHASessionStatus[],
): Promise<WhatsappSession> {
if (!sessionName) {
throw new UnprocessableEntityException({
error: `Session name is required`,
session: sessionName,
});
}
const running = await waitUntil(
async () => this.isRunning(sessionName),
this.WAIT_SESSION_RUNNING_INTERVAL,
this.WAIT_SESSION_RUNNING_TIMEOUT,
);
if (!running) {
const exists = await this.exists(sessionName);
if (!exists) {
throw new UnprocessableEntityException({
error: `Session "${sessionName}" does not exist`,
session: sessionName,
});
}
const msg = {
error:
'Session status is not as expected. Try again later or restart the session',
session: sessionName,
status: 'STOPPED',
expected: expected,
};
throw new UnprocessableEntityException(msg);
}
const session = this.getSession(sessionName);
const valid = await waitUntil(
async () => expected.includes(session.status),
@@ -191,11 +223,14 @@ export function populateSessionInfo(
) {
return (payload: any): WAHAWebhook => {
const id = payload._eventId;
const timestampMs = payload._timestampMs;
const data = { ...payload };
delete data._eventId;
delete data._timestampMs;
const me = session.getSessionMeInfo();
return {
id: id,
timestamp: timestampMs,
event: event,
session: session.name,
metadata: session.sessionConfig?.metadata,
+182 -12
View File
@@ -1,3 +1,6 @@
import { isJidBroadcast } from '@adiwajshing/baileys/lib/WABinary/jid-utils';
import { MessagesForRead } from '@waha/core/utils/convertors';
import { isJidNewsletter } from '@waha/core/utils/jids';
import {
Channel,
ChannelListResult,
@@ -13,12 +16,18 @@ import {
GetChatMessageQuery,
GetChatMessagesFilter,
GetChatMessagesQuery,
ReadChatMessagesQuery,
ReadChatMessagesResponse,
} 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 { LidToPhoneNumber } from '@waha/structures/lids.dto';
import {
LimitOffsetParams,
PaginationParams,
} from '@waha/structures/pagination.dto';
import { MessageSource, WAMessage } from '@waha/structures/responses.dto';
import { DefaultMap } from '@waha/utils/DefaultMap';
import { generatePrefixedId } from '@waha/utils/ids';
import { LoggerBuilder } from '@waha/utils/logging';
@@ -26,7 +35,6 @@ 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 {
@@ -47,10 +55,12 @@ import {
ChatRequest,
CheckNumberStatusQuery,
EditMessageRequest,
MessageButtonReply,
MessageContactVcardRequest,
MessageFileRequest,
MessageForwardRequest,
MessageImageRequest,
MessageLinkCustomPreviewRequest,
MessageLinkPreviewRequest,
MessageLocationRequest,
MessagePollRequest,
@@ -69,9 +79,14 @@ import {
WAHAPresenceStatus,
WAHASessionStatus,
} from '../../structures/enums.dto';
import {
EventCancelRequest,
EventMessageRequest,
} from '../../structures/events.dto';
import {
CreateGroupRequest,
GroupsPaginationParams,
GroupField,
GroupsListFields,
ParticipantsRequest,
SettingsSecurityChangeInfo,
} from '../../structures/groups.dto';
@@ -146,6 +161,12 @@ export abstract class WhatsappSession {
stdTTL: 24 * 60 * 60, // 1 day
});
// Save sent messages ids in cache so we can determine if a message was sent
// via API or APP
private sentMessageIds: NodeCache = new NodeCache({
stdTTL: 10 * 60, // 10 minutes
});
public constructor({
name,
printQR,
@@ -176,6 +197,7 @@ export abstract class WhatsappSession {
filter(Boolean),
map((data) => {
data._eventId = generatePrefixedId('evt');
data._timestampMs = Date.now();
return data;
}),
retry(),
@@ -360,10 +382,10 @@ export abstract class WhatsappSession {
// Refresh profile picture after update
setTimeout(() => {
this.logger.info('Refreshing my profile picture after update...');
this.logger.debug('Refreshing my profile picture after update...');
this.refreshMyProfilePicture()
.then(() => {
this.logger.info('Refreshed my profile picture after update');
this.logger.debug('Refreshed my profile picture after update');
})
.catch((err) => {
this.logger.error('Error refreshing my profile picture after update');
@@ -390,6 +412,10 @@ export abstract class WhatsappSession {
/**
* Other methods
*/
generateNewMessageId(): Promise<string> {
throw new NotImplementedByEngineError();
}
abstract checkNumberStatus(request: CheckNumberStatusQuery);
abstract sendText(request: MessageTextRequest);
@@ -408,6 +434,12 @@ export abstract class WhatsappSession {
throw new NotImplementedByEngineError();
}
sendLinkCustomPreview(
request: MessageLinkCustomPreviewRequest,
): Promise<any> {
throw new NotImplementedByEngineError();
}
abstract forwardMessage(request: MessageForwardRequest): Promise<WAMessage>;
abstract sendImage(request: MessageImageRequest);
@@ -424,6 +456,10 @@ export abstract class WhatsappSession {
throw new NotImplementedByEngineError();
}
sendButtonsReply(request: MessageButtonReply) {
throw new NotImplementedByEngineError();
}
abstract reply(request: MessageReplyRequest);
abstract sendSeen(chat: SendSeenRequest);
@@ -438,6 +474,14 @@ export abstract class WhatsappSession {
throw new NotImplementedByEngineError();
}
sendEvent(request: EventMessageRequest): Promise<WAMessage> {
throw new NotImplementedByEngineError();
}
cancelEvent(eventId: string): Promise<WAMessage> {
throw new NotImplementedByEngineError();
}
/**
* Chats methods
*/
@@ -459,10 +503,35 @@ export abstract class WhatsappSession {
chatId: string,
query: GetChatMessagesQuery,
filter: GetChatMessagesFilter,
) {
): Promise<WAMessage[]> {
throw new NotImplementedByEngineError();
}
abstract readChatMessages(
chatId: string,
request: ReadChatMessagesQuery,
): Promise<ReadChatMessagesResponse>;
protected async readChatMessagesWSImpl(
chatId: string,
request: ReadChatMessagesQuery,
): Promise<ReadChatMessagesResponse> {
const { query, filter } = MessagesForRead(chatId, request);
const messages = await this.getChatMessages(chatId, query, filter);
this.logger.debug(`Found ${messages.length} messages to read`);
if (messages.length === 0) {
return { ids: [] };
}
const ids = messages.map((m) => m.id);
const seen: SendSeenRequest = {
chatId: chatId,
messageIds: ids,
session: '',
};
await this.sendSeen(seen);
return { ids: ids };
}
public getChatMessage(
chatId: string,
messageId: string,
@@ -562,6 +631,29 @@ export abstract class WhatsappSession {
throw new NotImplementedByEngineError();
}
/**
* Lid to Phone Number methods
*/
public async getAllLids(
pagination: PaginationParams,
): Promise<Array<LidToPhoneNumber>> {
throw new NotImplementedByEngineError();
}
public async getLidsCount(): Promise<number> {
throw new NotImplementedByEngineError();
}
public async findPNByLid(lid: string): Promise<LidToPhoneNumber> {
throw new NotImplementedByEngineError();
}
public async findLIDByPhoneNumber(
phoneNumber: string,
): Promise<LidToPhoneNumber> {
throw new NotImplementedByEngineError();
}
/**
* Fetch the latest profile picture of the contact (group, newsletter, etc.)
* @param id
@@ -581,7 +673,9 @@ export abstract class WhatsappSession {
protected async refreshProfilePicture(id: string) {
let fn: Promise<string>;
if (isNewsletter(id)) {
if (isJidBroadcast(id)) {
return null;
} else if (isJidNewsletter(id)) {
fn = this.channelsGetChannel(id).then(
(channel: Channel) => channel.picture || channel.preview,
);
@@ -621,10 +715,22 @@ export abstract class WhatsappSession {
throw new NotImplementedByEngineError();
}
public getGroups(pagination: PaginationParams) {
public getGroups(pagination: PaginationParams): Promise<any> {
throw new NotImplementedByEngineError();
}
public filterGroupsFields(data: any, fields: GroupsListFields) {
const groups: any[] = Array.isArray(data) ? data : Object.values(data);
if (fields.exclude?.includes(GroupField.PARTICIPANTS)) {
groups.forEach((group) => this.removeGroupsFieldParticipant(group));
}
return data;
}
protected removeGroupsFieldParticipant(group: any) {
return;
}
public refreshGroups(): Promise<boolean> {
throw new NotImplementedByEngineError();
}
@@ -661,6 +767,43 @@ export abstract class WhatsappSession {
throw new NotImplementedByEngineError();
}
public async updateGroupPicture(
id: string,
file: BinaryFile | RemoteFile | null,
): Promise<boolean> {
if (file) {
await this.setGroupPicture(id, file);
} else {
await this.deleteGroupPicture(id);
}
// Refresh picture after update
setTimeout(() => {
this.logger.debug('Refreshing group profile picture after update...');
this.refreshProfilePicture(id)
.then(() => {
this.logger.debug('Refreshed group 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 setGroupPicture(
id: string,
file: BinaryFile | RemoteFile,
): Promise<boolean> {
throw new NotImplementedByEngineError();
}
protected deleteGroupPicture(id: string): Promise<boolean> {
throw new NotImplementedByEngineError();
}
public setSubject(id, description) {
throw new NotImplementedByEngineError();
}
@@ -792,6 +935,9 @@ export abstract class WhatsappSession {
throw new NotImplementedByEngineError();
}
/**
* Engine methods
*/
public async getEngineInfo(): Promise<any> {
return {};
}
@@ -833,10 +979,18 @@ export abstract class WhatsappSession {
);
qrcode.generate(qr.raw, { small: true });
}
}
export function isNewsletter(jid: string) {
return jid.endsWith('@newsletter');
protected saveSentMessageId(id: string) {
this.sentMessageIds.set(id, true);
}
protected getMessageSource(id: string): MessageSource {
if (!id) {
return MessageSource.APP;
}
const api = this.sentMessageIds.has(id);
return api ? MessageSource.API : MessageSource.APP;
}
}
export function getGroupInviteLink(code: string) {
@@ -864,3 +1018,19 @@ export function parseChannelInviteLink(link: string): string {
export function getPublicUrlFromDirectPath(directPath: string) {
return `https://pps.whatsapp.net${directPath}`;
}
const deviceRegexp = /^.*:(\d+)@.*$/;
/**
* Extracts the device ID from a JID string.
*
* @param jid - The JID string (e.g., "123123:12@c.us")
* @return The extracted device ID (e.g., "12") or null if the format is invalid.
*/
export function extractDeviceId(jid: string): string | null {
if (!jid) {
return null;
}
const match = jid.match(deviceRegexp);
return match ? match[1] : null;
}
+4
View File
@@ -5,6 +5,7 @@ import { PassportModule } from '@nestjs/passport';
import { ServeStaticModule } from '@nestjs/serve-static';
import { TerminusModule } from '@nestjs/terminus';
import { ChannelsController } from '@waha/api/channels.controller';
import { LidsController } from '@waha/api/lids.controller';
import { ProfileController } from '@waha/api/profile.controller';
import {
ServerController,
@@ -31,6 +32,7 @@ import { AuthController } from '../api/auth.controller';
import { ChatsController } from '../api/chats.controller';
import { ChattingController } from '../api/chatting.controller';
import { ContactsController } from '../api/contacts.controller';
import { EventsController } from '../api/events.controller';
import { GroupsController } from '../api/groups.controller';
import { HealthController } from '../api/health.controller';
import { LabelsController } from '../api/labels.controller';
@@ -129,9 +131,11 @@ export const CONTROLLERS = [
StatusController,
LabelsController,
ContactsController,
LidsController,
GroupsController,
PresenceController,
ScreenshotController,
EventsController,
PingController,
HealthController,
ServerController,
+167
View File
@@ -0,0 +1,167 @@
import { ConfigService } from '@nestjs/config';
import { WAHAEvents } from '@waha/structures/enums.dto';
import {
CustomHeader,
HmacConfiguration,
RetriesConfiguration,
WebhookConfig,
} from '@waha/structures/webhooks.config.dto';
import { plainToInstance } from 'class-transformer';
import { validateSync, ValidationError } from 'class-validator';
enum Env {
WHATSAPP_HOOK_URL = 'WHATSAPP_HOOK_URL',
WHATSAPP_HOOK_EVENTS = 'WHATSAPP_HOOK_EVENTS',
WHATSAPP_HOOK_RETRIES_POLICY = 'WHATSAPP_HOOK_RETRIES_POLICY',
WHATSAPP_HOOK_RETRIES_DELAY_SECONDS = 'WHATSAPP_HOOK_RETRIES_DELAY_SECONDS',
WHATSAPP_HOOK_RETRIES_ATTEMPTS = 'WHATSAPP_HOOK_RETRIES_ATTEMPTS',
WHATSAPP_HOOK_HMAC_KEY = 'WHATSAPP_HOOK_HMAC_KEY',
WHATSAPP_HOOK_CUSTOM_HEADERS = 'WHATSAPP_HOOK_CUSTOM_HEADERS',
}
export class GlobalWebhookConfigConfig {
protected _config: WebhookConfig | null = null;
constructor(private configService: ConfigService) {}
private getUrl(): string | undefined {
return this.configService.get(Env.WHATSAPP_HOOK_URL);
}
private getEvents(): WAHAEvents[] {
const value = this.configService.get(Env.WHATSAPP_HOOK_EVENTS, '');
return value ? value.split(',') : [];
}
private getHmac(): HmacConfiguration | null {
const key = this.configService.get(Env.WHATSAPP_HOOK_HMAC_KEY, '');
if (!key) {
return null;
}
return {
key: key,
};
}
private getRetries(): RetriesConfiguration | null {
const policy: any = this.configService.get(
Env.WHATSAPP_HOOK_RETRIES_POLICY,
null,
);
const delaySeconds: any = this.configService.get(
Env.WHATSAPP_HOOK_RETRIES_DELAY_SECONDS,
null,
);
const attempts: any = this.configService.get(
Env.WHATSAPP_HOOK_RETRIES_ATTEMPTS,
null,
);
if (!policy && !delaySeconds && !attempts) {
return null;
}
return {
policy: policy,
delaySeconds: delaySeconds,
attempts: attempts,
};
}
private getCustomHeaders(): CustomHeader[] {
// key:value;key2:value
const value = this.configService.get(Env.WHATSAPP_HOOK_CUSTOM_HEADERS, '');
if (!value) {
return [];
}
const headers = value.split(';');
return headers.map((header: string) => {
const parts = header.split(':');
if (parts.length !== 2) {
throw new Error(
`${Env.WHATSAPP_HOOK_CUSTOM_HEADERS} - Invalid custom header, no ':' found in '${header}'`,
);
}
return {
name: parts[0],
value: parts[1],
};
});
}
get config() {
if (!this._config) {
this._config = this.parseWebhookConfig();
}
return this._config;
}
validateConfig(): string | null {
const config = this.parseWebhookConfig();
if (!config) {
return null;
}
const errors = validateSync(config);
if (errors.length > 0) {
return this.formatErrors([], errors, []).join('\n');
}
return;
}
private getEnv(key: string): string | null {
const keys = {
url: Env.WHATSAPP_HOOK_URL,
events: Env.WHATSAPP_HOOK_EVENTS,
'retries.policy': Env.WHATSAPP_HOOK_RETRIES_POLICY,
'retries.delaySeconds': Env.WHATSAPP_HOOK_RETRIES_DELAY_SECONDS,
'retries.attempts': Env.WHATSAPP_HOOK_RETRIES_ATTEMPTS,
'hmac.key': Env.WHATSAPP_HOOK_HMAC_KEY,
};
return keys[key] || null;
}
private formatErrors(
lines: string[],
errors: ValidationError[],
path: string[],
) {
for (const err of errors) {
path = [...path, err.property];
for (const key in err.constraints) {
if (!err.constraints.hasOwnProperty(key)) {
continue;
}
const property = path.join('.');
const env = this.getEnv(property);
if (env) {
const value = this.configService.get(env, '');
const line = `- ${env} (${property}): '${value}' => '${err.value}' - ${err.constraints[key]}`;
lines.push(line);
} else {
const line = `- ${property}: '${err.value}' - ${err.constraints[key]}`;
lines.push(line);
}
}
if (err.children) {
this.formatErrors(lines, err.children, path);
}
}
return lines;
}
private parseWebhookConfig(): WebhookConfig | null {
const url = this.getUrl();
const events = this.getEvents();
if (!url || events.length === 0) {
return null;
}
const data: WebhookConfig = {
url: url,
events: events,
hmac: this.getHmac(),
retries: this.getRetries(),
customHeaders: this.getCustomHeaders(),
};
return plainToInstance(WebhookConfig, data, {
enableImplicitConversion: true,
});
}
}
+3
View File
@@ -0,0 +1,3 @@
export function isFromFullSync(event: any): boolean {
return event.FromFullSync;
}
+1 -1
View File
@@ -4,7 +4,7 @@ import {
GroupInfoFull,
JoinedGroupEvent,
} from '@waha/core/engines/gows/types.group';
import { toJID } from '@waha/core/engines/noweb/session.noweb.core';
import { toJID } from '@waha/core/utils/jids';
import {
GroupId,
GroupInfo,
File diff suppressed because it is too large. Load diff
+598 -31
View File
@@ -4,6 +4,39 @@
var grpc = require('@grpc/grpc-js');
var gows_pb = require('./gows_pb.js');
function serialize_messages_ButtonReplyRequest(arg) {
if (!(arg instanceof gows_pb.ButtonReplyRequest)) {
throw new Error('Expected argument of type messages.ButtonReplyRequest');
}
return Buffer.from(arg.serializeBinary());
}
function deserialize_messages_ButtonReplyRequest(buffer_arg) {
return gows_pb.ButtonReplyRequest.deserializeBinary(new Uint8Array(buffer_arg));
}
function serialize_messages_CancelEventMessageRequest(arg) {
if (!(arg instanceof gows_pb.CancelEventMessageRequest)) {
throw new Error('Expected argument of type messages.CancelEventMessageRequest');
}
return Buffer.from(arg.serializeBinary());
}
function deserialize_messages_CancelEventMessageRequest(buffer_arg) {
return gows_pb.CancelEventMessageRequest.deserializeBinary(new Uint8Array(buffer_arg));
}
function serialize_messages_ChatLabelRequest(arg) {
if (!(arg instanceof gows_pb.ChatLabelRequest)) {
throw new Error('Expected argument of type messages.ChatLabelRequest');
}
return Buffer.from(arg.serializeBinary());
}
function deserialize_messages_ChatLabelRequest(buffer_arg) {
return gows_pb.ChatLabelRequest.deserializeBinary(new Uint8Array(buffer_arg));
}
function serialize_messages_ChatPresenceRequest(arg) {
if (!(arg instanceof gows_pb.ChatPresenceRequest)) {
throw new Error('Expected argument of type messages.ChatPresenceRequest');
@@ -37,6 +70,17 @@ function deserialize_messages_CheckPhonesResponse(buffer_arg) {
return gows_pb.CheckPhonesResponse.deserializeBinary(new Uint8Array(buffer_arg));
}
function serialize_messages_CreateGroupRequest(arg) {
if (!(arg instanceof gows_pb.CreateGroupRequest)) {
throw new Error('Expected argument of type messages.CreateGroupRequest');
}
return Buffer.from(arg.serializeBinary());
}
function deserialize_messages_CreateGroupRequest(buffer_arg) {
return gows_pb.CreateGroupRequest.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');
@@ -48,6 +92,17 @@ function deserialize_messages_CreateNewsletterRequest(buffer_arg) {
return gows_pb.CreateNewsletterRequest.deserializeBinary(new Uint8Array(buffer_arg));
}
function serialize_messages_DeleteLabelRequest(arg) {
if (!(arg instanceof gows_pb.DeleteLabelRequest)) {
throw new Error('Expected argument of type messages.DeleteLabelRequest');
}
return Buffer.from(arg.serializeBinary());
}
function deserialize_messages_DeleteLabelRequest(buffer_arg) {
return gows_pb.DeleteLabelRequest.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');
@@ -70,6 +125,17 @@ function deserialize_messages_DownloadMediaResponse(buffer_arg) {
return gows_pb.DownloadMediaResponse.deserializeBinary(new Uint8Array(buffer_arg));
}
function serialize_messages_EditMessageRequest(arg) {
if (!(arg instanceof gows_pb.EditMessageRequest)) {
throw new Error('Expected argument of type messages.EditMessageRequest');
}
return Buffer.from(arg.serializeBinary());
}
function deserialize_messages_EditMessageRequest(buffer_arg) {
return gows_pb.EditMessageRequest.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');
@@ -125,6 +191,28 @@ function deserialize_messages_GetContactsRequest(buffer_arg) {
return gows_pb.GetContactsRequest.deserializeBinary(new Uint8Array(buffer_arg));
}
function serialize_messages_GetLabelsRequest(arg) {
if (!(arg instanceof gows_pb.GetLabelsRequest)) {
throw new Error('Expected argument of type messages.GetLabelsRequest');
}
return Buffer.from(arg.serializeBinary());
}
function deserialize_messages_GetLabelsRequest(buffer_arg) {
return gows_pb.GetLabelsRequest.deserializeBinary(new Uint8Array(buffer_arg));
}
function serialize_messages_GetLidsRequest(arg) {
if (!(arg instanceof gows_pb.GetLidsRequest)) {
throw new Error('Expected argument of type messages.GetLidsRequest');
}
return Buffer.from(arg.serializeBinary());
}
function deserialize_messages_GetLidsRequest(buffer_arg) {
return gows_pb.GetLidsRequest.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');
@@ -147,6 +235,50 @@ function deserialize_messages_GetNewsletterMessagesByInviteRequest(buffer_arg) {
return gows_pb.GetNewsletterMessagesByInviteRequest.deserializeBinary(new Uint8Array(buffer_arg));
}
function serialize_messages_GroupCodeRequest(arg) {
if (!(arg instanceof gows_pb.GroupCodeRequest)) {
throw new Error('Expected argument of type messages.GroupCodeRequest');
}
return Buffer.from(arg.serializeBinary());
}
function deserialize_messages_GroupCodeRequest(buffer_arg) {
return gows_pb.GroupCodeRequest.deserializeBinary(new Uint8Array(buffer_arg));
}
function serialize_messages_JidBoolRequest(arg) {
if (!(arg instanceof gows_pb.JidBoolRequest)) {
throw new Error('Expected argument of type messages.JidBoolRequest');
}
return Buffer.from(arg.serializeBinary());
}
function deserialize_messages_JidBoolRequest(buffer_arg) {
return gows_pb.JidBoolRequest.deserializeBinary(new Uint8Array(buffer_arg));
}
function serialize_messages_JidRequest(arg) {
if (!(arg instanceof gows_pb.JidRequest)) {
throw new Error('Expected argument of type messages.JidRequest');
}
return Buffer.from(arg.serializeBinary());
}
function deserialize_messages_JidRequest(buffer_arg) {
return gows_pb.JidRequest.deserializeBinary(new Uint8Array(buffer_arg));
}
function serialize_messages_JidStringRequest(arg) {
if (!(arg instanceof gows_pb.JidStringRequest)) {
throw new Error('Expected argument of type messages.JidStringRequest');
}
return Buffer.from(arg.serializeBinary());
}
function deserialize_messages_JidStringRequest(buffer_arg) {
return gows_pb.JidStringRequest.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');
@@ -213,6 +345,17 @@ function deserialize_messages_MessageResponse(buffer_arg) {
return gows_pb.MessageResponse.deserializeBinary(new Uint8Array(buffer_arg));
}
function serialize_messages_NewMessageIDResponse(arg) {
if (!(arg instanceof gows_pb.NewMessageIDResponse)) {
throw new Error('Expected argument of type messages.NewMessageIDResponse');
}
return Buffer.from(arg.serializeBinary());
}
function deserialize_messages_NewMessageIDResponse(buffer_arg) {
return gows_pb.NewMessageIDResponse.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');
@@ -290,6 +433,28 @@ function deserialize_messages_NewsletterToggleMuteRequest(buffer_arg) {
return gows_pb.NewsletterToggleMuteRequest.deserializeBinary(new Uint8Array(buffer_arg));
}
function serialize_messages_OptionalString(arg) {
if (!(arg instanceof gows_pb.OptionalString)) {
throw new Error('Expected argument of type messages.OptionalString');
}
return Buffer.from(arg.serializeBinary());
}
function deserialize_messages_OptionalString(buffer_arg) {
return gows_pb.OptionalString.deserializeBinary(new Uint8Array(buffer_arg));
}
function serialize_messages_OptionalUInt64(arg) {
if (!(arg instanceof gows_pb.OptionalUInt64)) {
throw new Error('Expected argument of type messages.OptionalUInt64');
}
return Buffer.from(arg.serializeBinary());
}
function deserialize_messages_OptionalUInt64(buffer_arg) {
return gows_pb.OptionalUInt64.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');
@@ -367,6 +532,17 @@ function deserialize_messages_ProfileStatusRequest(buffer_arg) {
return gows_pb.ProfileStatusRequest.deserializeBinary(new Uint8Array(buffer_arg));
}
function serialize_messages_RevokeMessageRequest(arg) {
if (!(arg instanceof gows_pb.RevokeMessageRequest)) {
throw new Error('Expected argument of type messages.RevokeMessageRequest');
}
return Buffer.from(arg.serializeBinary());
}
function deserialize_messages_RevokeMessageRequest(buffer_arg) {
return gows_pb.RevokeMessageRequest.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');
@@ -411,6 +587,17 @@ function deserialize_messages_SessionStateResponse(buffer_arg) {
return gows_pb.SessionStateResponse.deserializeBinary(new Uint8Array(buffer_arg));
}
function serialize_messages_SetPictureRequest(arg) {
if (!(arg instanceof gows_pb.SetPictureRequest)) {
throw new Error('Expected argument of type messages.SetPictureRequest');
}
return Buffer.from(arg.serializeBinary());
}
function deserialize_messages_SetPictureRequest(buffer_arg) {
return gows_pb.SetPictureRequest.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');
@@ -444,6 +631,28 @@ function deserialize_messages_SubscribePresenceRequest(buffer_arg) {
return gows_pb.SubscribePresenceRequest.deserializeBinary(new Uint8Array(buffer_arg));
}
function serialize_messages_UpdateParticipantsRequest(arg) {
if (!(arg instanceof gows_pb.UpdateParticipantsRequest)) {
throw new Error('Expected argument of type messages.UpdateParticipantsRequest');
}
return Buffer.from(arg.serializeBinary());
}
function deserialize_messages_UpdateParticipantsRequest(buffer_arg) {
return gows_pb.UpdateParticipantsRequest.deserializeBinary(new Uint8Array(buffer_arg));
}
function serialize_messages_UpsertLabelRequest(arg) {
if (!(arg instanceof gows_pb.UpsertLabelRequest)) {
throw new Error('Expected argument of type messages.UpsertLabelRequest');
}
return Buffer.from(arg.serializeBinary());
}
function deserialize_messages_UpsertLabelRequest(buffer_arg) {
return gows_pb.UpsertLabelRequest.deserializeBinary(new Uint8Array(buffer_arg));
}
//
// Events
@@ -559,31 +768,226 @@ setProfileName: {
responseDeserialize: deserialize_messages_Empty,
},
//
// Lids
//
getAllLids: {
path: '/messages.MessageService/GetAllLids',
requestStream: false,
responseStream: false,
requestType: gows_pb.GetLidsRequest,
responseType: gows_pb.JsonList,
requestSerialize: serialize_messages_GetLidsRequest,
requestDeserialize: deserialize_messages_GetLidsRequest,
responseSerialize: serialize_messages_JsonList,
responseDeserialize: deserialize_messages_JsonList,
},
getLidsCount: {
path: '/messages.MessageService/GetLidsCount',
requestStream: false,
responseStream: false,
requestType: gows_pb.Session,
responseType: gows_pb.OptionalUInt64,
requestSerialize: serialize_messages_Session,
requestDeserialize: deserialize_messages_Session,
responseSerialize: serialize_messages_OptionalUInt64,
responseDeserialize: deserialize_messages_OptionalUInt64,
},
findPNByLid: {
path: '/messages.MessageService/FindPNByLid',
requestStream: false,
responseStream: false,
requestType: gows_pb.EntityByIdRequest,
responseType: gows_pb.OptionalString,
requestSerialize: serialize_messages_EntityByIdRequest,
requestDeserialize: deserialize_messages_EntityByIdRequest,
responseSerialize: serialize_messages_OptionalString,
responseDeserialize: deserialize_messages_OptionalString,
},
findLIDByPhoneNumber: {
path: '/messages.MessageService/FindLIDByPhoneNumber',
requestStream: false,
responseStream: false,
requestType: gows_pb.EntityByIdRequest,
responseType: gows_pb.OptionalString,
requestSerialize: serialize_messages_EntityByIdRequest,
requestDeserialize: deserialize_messages_EntityByIdRequest,
responseSerialize: serialize_messages_OptionalString,
responseDeserialize: deserialize_messages_OptionalString,
},
//
// Groups
//
fetchGroups: {
path: '/messages.MessageService/FetchGroups',
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,
},
getGroups: {
path: '/messages.MessageService/GetGroups',
requestStream: false,
responseStream: false,
requestType: gows_pb.Session,
responseType: gows_pb.JsonList,
requestSerialize: serialize_messages_Session,
requestDeserialize: deserialize_messages_Session,
responseSerialize: serialize_messages_JsonList,
responseDeserialize: deserialize_messages_JsonList,
},
getGroupInfo: {
path: '/messages.MessageService/GetGroupInfo',
requestStream: false,
responseStream: false,
requestType: gows_pb.JidRequest,
responseType: gows_pb.Json,
requestSerialize: serialize_messages_JidRequest,
requestDeserialize: deserialize_messages_JidRequest,
responseSerialize: serialize_messages_Json,
responseDeserialize: deserialize_messages_Json,
},
createGroup: {
path: '/messages.MessageService/CreateGroup',
requestStream: false,
responseStream: false,
requestType: gows_pb.CreateGroupRequest,
responseType: gows_pb.Json,
requestSerialize: serialize_messages_CreateGroupRequest,
requestDeserialize: deserialize_messages_CreateGroupRequest,
responseSerialize: serialize_messages_Json,
responseDeserialize: deserialize_messages_Json,
},
leaveGroup: {
path: '/messages.MessageService/LeaveGroup',
requestStream: false,
responseStream: false,
requestType: gows_pb.JidRequest,
responseType: gows_pb.Empty,
requestSerialize: serialize_messages_JidRequest,
requestDeserialize: deserialize_messages_JidRequest,
responseSerialize: serialize_messages_Empty,
responseDeserialize: deserialize_messages_Empty,
},
getGroupInviteLink: {
path: '/messages.MessageService/GetGroupInviteLink',
requestStream: false,
responseStream: false,
requestType: gows_pb.JidRequest,
responseType: gows_pb.OptionalString,
requestSerialize: serialize_messages_JidRequest,
requestDeserialize: deserialize_messages_JidRequest,
responseSerialize: serialize_messages_OptionalString,
responseDeserialize: deserialize_messages_OptionalString,
},
revokeGroupInviteLink: {
path: '/messages.MessageService/RevokeGroupInviteLink',
requestStream: false,
responseStream: false,
requestType: gows_pb.JidRequest,
responseType: gows_pb.OptionalString,
requestSerialize: serialize_messages_JidRequest,
requestDeserialize: deserialize_messages_JidRequest,
responseSerialize: serialize_messages_OptionalString,
responseDeserialize: deserialize_messages_OptionalString,
},
getGroupInfoFromLink: {
path: '/messages.MessageService/GetGroupInfoFromLink',
requestStream: false,
responseStream: false,
requestType: gows_pb.GroupCodeRequest,
responseType: gows_pb.Json,
requestSerialize: serialize_messages_GroupCodeRequest,
requestDeserialize: deserialize_messages_GroupCodeRequest,
responseSerialize: serialize_messages_Json,
responseDeserialize: deserialize_messages_Json,
},
joinGroupWithLink: {
path: '/messages.MessageService/JoinGroupWithLink',
requestStream: false,
responseStream: false,
requestType: gows_pb.GroupCodeRequest,
responseType: gows_pb.Json,
requestSerialize: serialize_messages_GroupCodeRequest,
requestDeserialize: deserialize_messages_GroupCodeRequest,
responseSerialize: serialize_messages_Json,
responseDeserialize: deserialize_messages_Json,
},
setGroupName: {
path: '/messages.MessageService/SetGroupName',
requestStream: false,
responseStream: false,
requestType: gows_pb.JidStringRequest,
responseType: gows_pb.Empty,
requestSerialize: serialize_messages_JidStringRequest,
requestDeserialize: deserialize_messages_JidStringRequest,
responseSerialize: serialize_messages_Empty,
responseDeserialize: deserialize_messages_Empty,
},
setGroupDescription: {
path: '/messages.MessageService/SetGroupDescription',
requestStream: false,
responseStream: false,
requestType: gows_pb.JidStringRequest,
responseType: gows_pb.Empty,
requestSerialize: serialize_messages_JidStringRequest,
requestDeserialize: deserialize_messages_JidStringRequest,
responseSerialize: serialize_messages_Empty,
responseDeserialize: deserialize_messages_Empty,
},
setGroupPicture: {
path: '/messages.MessageService/SetGroupPicture',
requestStream: false,
responseStream: false,
requestType: gows_pb.SetPictureRequest,
responseType: gows_pb.Empty,
requestSerialize: serialize_messages_SetPictureRequest,
requestDeserialize: deserialize_messages_SetPictureRequest,
responseSerialize: serialize_messages_Empty,
responseDeserialize: deserialize_messages_Empty,
},
setGroupLocked: {
path: '/messages.MessageService/SetGroupLocked',
requestStream: false,
responseStream: false,
requestType: gows_pb.JidBoolRequest,
responseType: gows_pb.Empty,
requestSerialize: serialize_messages_JidBoolRequest,
requestDeserialize: deserialize_messages_JidBoolRequest,
responseSerialize: serialize_messages_Empty,
responseDeserialize: deserialize_messages_Empty,
},
// change info only by admins
setGroupAnnounce: {
path: '/messages.MessageService/SetGroupAnnounce',
requestStream: false,
responseStream: false,
requestType: gows_pb.JidBoolRequest,
responseType: gows_pb.Empty,
requestSerialize: serialize_messages_JidBoolRequest,
requestDeserialize: deserialize_messages_JidBoolRequest,
responseSerialize: serialize_messages_Empty,
responseDeserialize: deserialize_messages_Empty,
},
// send messages only by admins
updateGroupParticipants: {
path: '/messages.MessageService/UpdateGroupParticipants',
requestStream: false,
responseStream: false,
requestType: gows_pb.UpdateParticipantsRequest,
responseType: gows_pb.JsonList,
requestSerialize: serialize_messages_UpdateParticipantsRequest,
requestDeserialize: deserialize_messages_UpdateParticipantsRequest,
responseSerialize: serialize_messages_JsonList,
responseDeserialize: deserialize_messages_JsonList,
},
//
// 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: {
getProfilePicture: {
path: '/messages.MessageService/GetProfilePicture',
requestStream: false,
responseStream: false,
@@ -627,6 +1031,53 @@ sendMessage: {
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,
},
//
// Message
//
generateNewMessageID: {
path: '/messages.MessageService/GenerateNewMessageID',
requestStream: false,
responseStream: false,
requestType: gows_pb.Session,
responseType: gows_pb.NewMessageIDResponse,
requestSerialize: serialize_messages_Session,
requestDeserialize: deserialize_messages_Session,
responseSerialize: serialize_messages_NewMessageIDResponse,
responseDeserialize: deserialize_messages_NewMessageIDResponse,
},
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,
},
markRead: {
path: '/messages.MessageService/MarkRead',
requestStream: false,
@@ -638,16 +1089,38 @@ sendMessage: {
responseSerialize: serialize_messages_Empty,
responseDeserialize: deserialize_messages_Empty,
},
checkPhones: {
path: '/messages.MessageService/CheckPhones',
editMessage: {
path: '/messages.MessageService/EditMessage',
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,
requestType: gows_pb.EditMessageRequest,
responseType: gows_pb.MessageResponse,
requestSerialize: serialize_messages_EditMessageRequest,
requestDeserialize: deserialize_messages_EditMessageRequest,
responseSerialize: serialize_messages_MessageResponse,
responseDeserialize: deserialize_messages_MessageResponse,
},
revokeMessage: {
path: '/messages.MessageService/RevokeMessage',
requestStream: false,
responseStream: false,
requestType: gows_pb.RevokeMessageRequest,
responseType: gows_pb.MessageResponse,
requestSerialize: serialize_messages_RevokeMessageRequest,
requestDeserialize: deserialize_messages_RevokeMessageRequest,
responseSerialize: serialize_messages_MessageResponse,
responseDeserialize: deserialize_messages_MessageResponse,
},
sendButtonReply: {
path: '/messages.MessageService/SendButtonReply',
requestStream: false,
responseStream: false,
requestType: gows_pb.ButtonReplyRequest,
responseType: gows_pb.MessageResponse,
requestSerialize: serialize_messages_ButtonReplyRequest,
requestDeserialize: deserialize_messages_ButtonReplyRequest,
responseSerialize: serialize_messages_MessageResponse,
responseDeserialize: deserialize_messages_MessageResponse,
},
//
// Newsletters
@@ -741,6 +1214,100 @@ getSubscribedNewsletters: {
responseDeserialize: deserialize_messages_Empty,
},
//
// Labels
//
getLabels: {
path: '/messages.MessageService/GetLabels',
requestStream: false,
responseStream: false,
requestType: gows_pb.GetLabelsRequest,
responseType: gows_pb.JsonList,
requestSerialize: serialize_messages_GetLabelsRequest,
requestDeserialize: deserialize_messages_GetLabelsRequest,
responseSerialize: serialize_messages_JsonList,
responseDeserialize: deserialize_messages_JsonList,
},
upsertLabel: {
path: '/messages.MessageService/UpsertLabel',
requestStream: false,
responseStream: false,
requestType: gows_pb.UpsertLabelRequest,
responseType: gows_pb.Empty,
requestSerialize: serialize_messages_UpsertLabelRequest,
requestDeserialize: deserialize_messages_UpsertLabelRequest,
responseSerialize: serialize_messages_Empty,
responseDeserialize: deserialize_messages_Empty,
},
deleteLabel: {
path: '/messages.MessageService/DeleteLabel',
requestStream: false,
responseStream: false,
requestType: gows_pb.DeleteLabelRequest,
responseType: gows_pb.Empty,
requestSerialize: serialize_messages_DeleteLabelRequest,
requestDeserialize: deserialize_messages_DeleteLabelRequest,
responseSerialize: serialize_messages_Empty,
responseDeserialize: deserialize_messages_Empty,
},
addChatLabel: {
path: '/messages.MessageService/AddChatLabel',
requestStream: false,
responseStream: false,
requestType: gows_pb.ChatLabelRequest,
responseType: gows_pb.Empty,
requestSerialize: serialize_messages_ChatLabelRequest,
requestDeserialize: deserialize_messages_ChatLabelRequest,
responseSerialize: serialize_messages_Empty,
responseDeserialize: deserialize_messages_Empty,
},
removeChatLabel: {
path: '/messages.MessageService/RemoveChatLabel',
requestStream: false,
responseStream: false,
requestType: gows_pb.ChatLabelRequest,
responseType: gows_pb.Empty,
requestSerialize: serialize_messages_ChatLabelRequest,
requestDeserialize: deserialize_messages_ChatLabelRequest,
responseSerialize: serialize_messages_Empty,
responseDeserialize: deserialize_messages_Empty,
},
getLabelsByJid: {
path: '/messages.MessageService/GetLabelsByJid',
requestStream: false,
responseStream: false,
requestType: gows_pb.EntityByIdRequest,
responseType: gows_pb.JsonList,
requestSerialize: serialize_messages_EntityByIdRequest,
requestDeserialize: deserialize_messages_EntityByIdRequest,
responseSerialize: serialize_messages_JsonList,
responseDeserialize: deserialize_messages_JsonList,
},
getChatsByLabelId: {
path: '/messages.MessageService/GetChatsByLabelId',
requestStream: false,
responseStream: false,
requestType: gows_pb.EntityByIdRequest,
responseType: gows_pb.JsonList,
requestSerialize: serialize_messages_EntityByIdRequest,
requestDeserialize: deserialize_messages_EntityByIdRequest,
responseSerialize: serialize_messages_JsonList,
responseDeserialize: deserialize_messages_JsonList,
},
//
// Events
//
cancelEventMessage: {
path: '/messages.MessageService/CancelEventMessage',
requestStream: false,
responseStream: false,
requestType: gows_pb.CancelEventMessageRequest,
responseType: gows_pb.MessageResponse,
requestSerialize: serialize_messages_CancelEventMessageRequest,
requestDeserialize: deserialize_messages_CancelEventMessageRequest,
responseSerialize: serialize_messages_MessageResponse,
responseDeserialize: deserialize_messages_MessageResponse,
},
//
// Media
//
downloadMedia: {
File diff suppressed because it is too large. Load diff
+45
View File
@@ -0,0 +1,45 @@
import { toCusFormat } from '@waha/core/engines/noweb/session.noweb.core';
import { Label, LabelChatAssociation } from '@waha/structures/labels.dto';
import * as gows from './types';
/**
* Converts a GOWS LabelEdit event to a Label DTO
*/
export function eventToLabelDTO(labelEdit: gows.LabelEdit): Label {
return {
id: labelEdit.LabelID,
name: labelEdit.Action.name,
color: labelEdit.Action.color,
colorHex: Label.toHex(labelEdit.Action.color),
} as Label;
}
/**
* Converts a GOWS LabelAssociationChat event to a LabelChatAssociation DTO
*/
export function eventToLabelChatAssociationDTO(
labelAssoc: gows.LabelAssociationChat,
): LabelChatAssociation {
return {
labelId: labelAssoc.LabelID,
label: null, // We don't have the label info here
chatId: toCusFormat(labelAssoc.JID),
};
}
/**
* Checks if a LabelEdit event is for a label creation or update (not deletion)
*/
export function isLabelUpsertEvent(labelEdit: gows.LabelEdit): boolean {
return !labelEdit.Action.deleted;
}
/**
* Checks if a LabelAssociationChat event is for adding a label to a chat
*/
export function isLabelChatAddedEvent(
labelAssoc: gows.LabelAssociationChat,
): boolean {
return labelAssoc.Action.labeled === true;
}
File diff suppressed because it is too large. Load diff
+28
View File
@@ -39,3 +39,31 @@ export enum MessageStatus {
Read = 4,
Played = 5,
}
export interface LabelEditAction {
name: string;
color: number;
deleted: boolean;
orderIndex: number;
isActive: boolean;
type: number;
}
export interface LabelEdit {
Timestamp: string;
LabelID: string;
Action: LabelEditAction;
FromFullSync: boolean;
}
export interface LabelAssociationChatAction {
labeled: boolean;
}
export interface LabelAssociationChat {
JID: string;
Timestamp: string;
LabelID: string;
Action: LabelAssociationChatAction;
FromFullSync: boolean;
}
+51
View File
@@ -0,0 +1,51 @@
import * as NodeCache from 'node-cache';
export class PresenceProxy {
private cache: NodeCache;
public proxy: Record<string, any>;
constructor() {
this.cache = new NodeCache({
stdTTL: 60, // 1 minute
checkperiod: 60, // 1 minute
useClones: false,
});
this.proxy = new Proxy(
{},
{
get: (_, prop: string) => {
return this.cache.get(prop);
},
set: (_, prop: string, value) => {
this.cache.set(prop, value);
return true;
},
deleteProperty: (_, prop: string) => {
return this.cache.del(prop) > 0;
},
has: (_, prop: string) => {
return this.cache.has(prop);
},
ownKeys: () => {
return this.cache.keys();
},
getOwnPropertyDescriptor: (_, prop: string) => {
if (this.cache.has(prop)) {
return {
enumerable: true,
configurable: true,
};
}
return undefined;
},
},
);
}
}
File diff suppressed because it is too large. Load diff
@@ -18,4 +18,6 @@ export interface IChatRepository {
deleteById(id: string): Promise<void>;
save(chat: Chat): Promise<void>;
upsertMany(chats: Chat[]): Promise<void>;
}
@@ -11,4 +11,8 @@ export interface IContactRepository {
deleteById(id: string): Promise<void>;
save(contact: Contact): Promise<void>;
upsertMany(contacts: Contact[]): Promise<void>;
getEntitiesByIds(ids: string[]): Promise<Map<string, Contact | null>>;
}
@@ -8,6 +8,8 @@ export interface IMessagesRepository {
upsertOne(message: any): Promise<void>;
getById(id: string): Promise<any | null>;
getAllByJid(
jid: string,
filter: GetChatMessagesFilter,
@@ -0,0 +1,33 @@
import { LimitOffsetParams } from '@waha/structures/pagination.dto';
export class LidToPN {
id: string;
pn: string;
}
export interface INowebLidPNRepository {
/**
* Save lid to phone number mapping
*/
saveLids(lids: LidToPN[]): Promise<void>;
/**
* Get all lids with pagination
*/
getAllLids(pagination?: LimitOffsetParams): Promise<LidToPN[]>;
/**
* Get a total count of lids
*/
getLidsCount(): Promise<number>;
/**
* Find a phone number by lid
*/
findPNByLid(lid: string): Promise<string | null>;
/**
* Find lid by phone number
*/
findLidByPN(pn: string): Promise<string | null>;
}
@@ -5,6 +5,7 @@ import { ILabelsRepository } from '@waha/core/engines/noweb/store/ILabelsReposit
import { IChatRepository } from './IChatRepository';
import { IContactRepository } from './IContactRepository';
import { IMessagesRepository } from './IMessagesRepository';
import { INowebLidPNRepository } from './INowebLidPNRepository';
export abstract class INowebStorage {
abstract init(): Promise<void>;
@@ -22,4 +23,6 @@ export abstract class INowebStorage {
abstract getLabelsRepository(): ILabelsRepository;
abstract getLabelAssociationRepository(): ILabelAssociationRepository;
abstract getLidPNRepository(): INowebLidPNRepository;
}
+16 -1
View File
@@ -7,7 +7,11 @@ import {
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';
import { LidToPhoneNumber } from '@waha/structures/lids.dto';
import {
LimitOffsetParams,
PaginationParams,
} from '@waha/structures/pagination.dto';
export interface INowebStore {
presences: any;
@@ -30,6 +34,8 @@ export interface INowebStore {
getChats(pagination: PaginationParams, broadcast: boolean): Promise<Chat[]>;
getChat(jid: string): Promise<Chat | null>;
getContacts(pagination: PaginationParams): Promise<Contact[]>;
getContactById(jid: string): Promise<Contact>;
@@ -45,4 +51,13 @@ export interface INowebStore {
getGroups(pagination: PaginationParams): Promise<GroupMetadata[]>;
resetGroupsCache(): void;
// Lid Repository methods
getAllLids(pagination?: LimitOffsetParams): Promise<LidToPhoneNumber[]>;
getLidsCount(): Promise<number>;
findPNByLid(lid: string): Promise<string | null>;
findLidByPN(pn: string): Promise<string | null>;
}
@@ -2,16 +2,20 @@ 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 { LidToPhoneNumber } from '@waha/structures/lids.dto';
import {
LimitOffsetParams,
PaginationParams,
} from '@waha/structures/pagination.dto';
import { PaginatorInMemory } from '@waha/utils/Paginator';
import { INowebStore } from './INowebStore';
import makeInMemoryStore from './memory/make-in-memory-store';
// eslint-disable-next-line @typescript-eslint/no-var-requires
const logger = require('pino')();
@@ -26,6 +30,10 @@ export class NowebInMemoryStore implements INowebStore {
constructor() {
this.store = makeInMemoryStore({ logger: logger });
const presences = {};
this.store.presences = presences;
// Adjust inline even handler
this.store.setPresences(presences);
}
init(): Promise<void> {
@@ -65,6 +73,10 @@ export class NowebInMemoryStore implements INowebStore {
throw new BadRequestException(this.errorMessage);
}
getChat(jid: string): Promise<Chat | null> {
return null;
}
getContacts(pagination: PaginationParams): Promise<Contact[]> {
throw new BadRequestException(this.errorMessage);
}
@@ -91,7 +103,7 @@ export class NowebInMemoryStore implements INowebStore {
async getGroups(pagination: PaginationParams): Promise<GroupMetadata[]> {
const response = await this.socket?.groupFetchAllParticipating();
const groups = Object.values(response);
const groups: any[] = Object.values(response);
const paginator = new PaginatorInMemory(pagination);
return paginator.apply(groups);
}
@@ -99,4 +111,23 @@ export class NowebInMemoryStore implements INowebStore {
resetGroupsCache() {
return;
}
//
// Lids methods
//
getAllLids(pagination?: LimitOffsetParams): Promise<LidToPhoneNumber[]> {
throw new BadRequestException(this.errorMessage);
}
findLidByPN(pn: string): Promise<string | null> {
throw new BadRequestException(this.errorMessage);
}
findPNByLid(lid: string): Promise<string | null> {
throw new BadRequestException(this.errorMessage);
}
getLidsCount(): Promise<number> {
throw new BadRequestException(this.errorMessage);
}
}
@@ -5,6 +5,7 @@ import makeWASocket, {
ChatUpdate,
Contact,
GroupParticipant,
isJidUser,
isRealMessage,
jidNormalizedUser,
ParticipantAction,
@@ -18,13 +19,19 @@ import {
LabelAssociation,
LabelAssociationType,
} from '@adiwajshing/baileys/lib/Types/LabelAssociation';
import { isLidUser } from '@adiwajshing/baileys/lib/WABinary/jid-utils';
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 { LidToPhoneNumber } from '@waha/structures/lids.dto';
import {
LimitOffsetParams,
PaginationParams,
SortOrder,
} from '@waha/structures/pagination.dto';
import { DefaultMap } from '@waha/utils/DefaultMap';
import { sleep, waitUntil } from '@waha/utils/promiseTimeout';
import { waitUntil } from '@waha/utils/promiseTimeout';
import * as lodash from 'lodash';
import { toNumber } from 'lodash';
import { Logger } from 'pino';
@@ -32,6 +39,7 @@ import { Logger } from 'pino';
import { IChatRepository } from './IChatRepository';
import { IContactRepository } from './IContactRepository';
import { IMessagesRepository } from './IMessagesRepository';
import { INowebLidPNRepository, LidToPN } from './INowebLidPNRepository';
import { INowebStorage } from './INowebStorage';
import { INowebStore } from './INowebStore';
@@ -49,8 +57,14 @@ export class NowebPersistentStore implements INowebStore {
private messagesRepo: IMessagesRepository;
private labelsRepo: ILabelsRepository;
private labelAssociationsRepo: ILabelAssociationRepository;
private lidRepo: INowebLidPNRepository;
public presences: any;
private lock: any;
private lock: any = new AsyncLock({
maxPending: Infinity,
maxExecutionTime: 60_000,
});
private groupsFetchLock: any = new AsyncLock({
maxPending: Infinity,
maxExecutionTime: 60_000,
@@ -71,8 +85,8 @@ export class NowebPersistentStore implements INowebStore {
this.messagesRepo = storage.getMessagesRepository();
this.labelsRepo = storage.getLabelsRepository();
this.labelAssociationsRepo = storage.getLabelAssociationRepository();
this.lidRepo = storage.getLidPNRepository();
this.presences = {};
this.lock = new AsyncLock({ maxPending: Infinity });
}
init(): Promise<void> {
@@ -112,9 +126,16 @@ export class NowebPersistentStore implements INowebStore {
ev.on('groups.upsert', (data) =>
this.withLock('groups', () => this.onGroupUpsert(data)),
);
ev.on('groups.update', (data) =>
this.withLock('groups', () => this.onGroupUpdate(data)),
);
ev.on('groups.update', (data) => {
this.withLock('groups', () => this.onGroupUpdate(data));
this.withLock('lids', async () => {
const participants = lodash.flatMap(data, (g) => g?.participants || []);
const lids = await this.handleLidPNUpdates(participants);
this.logger.debug(
`groups.update - '${lids.length}' synced lid to pn mapping`,
);
});
});
ev.on('group-participants.update', (data) =>
this.withLock(`group-${data.id}`, () =>
this.onGroupParticipantsUpdate(data),
@@ -122,12 +143,24 @@ export class NowebPersistentStore implements INowebStore {
);
// Contacts
ev.on('contacts.upsert', (data) =>
this.withLock('contacts', () => this.onContactsUpsert(data)),
);
ev.on('contacts.update', (data) =>
this.withLock('contacts', () => this.onContactUpdate(data)),
);
ev.on('contacts.upsert', (data) => {
this.withLock('contacts', () => this.onContactsUpsert(data));
this.withLock('lids', async () => {
const lids = await this.handleLidPNUpdates(data);
this.logger.debug(
`contacts.upsert - '${lids.length}' synced lid to pn mapping`,
);
});
});
ev.on('contacts.update', (data) => {
this.withLock('contacts', () => this.onContactUpdate(data));
this.withLock('lids', async () => {
const lids = await this.handleLidPNUpdates(data);
this.logger.debug(
`contacts.update - '${lids.length}' synced lid to pn mapping`,
);
});
});
ev.on('labels.edit', (data) => this.onLabelsEdit(data));
ev.on('labels.association', ({ association, type }) =>
this.onLabelsAssociation(association, type),
@@ -162,6 +195,12 @@ export class NowebPersistentStore implements INowebStore {
await this.onContactsUpsert(contacts);
this.logger.info(`history sync - '${contacts.length}' synced contacts`);
}),
this.withLock('lids', async () => {
const lids = await this.handleLidPNUpdates(contacts);
this.logger.info(
`history sync - '${lids.length}' synced lid to pn mapping`,
);
}),
this.withLock('chats', () => this.onChatUpsert(chats)),
this.withLock('messages', () => this.syncMessagesHistory(messages)),
]);
@@ -194,9 +233,26 @@ export class NowebPersistentStore implements INowebStore {
const jid = jidNormalizedUser(update.key.remoteJid!);
const message = await this.messagesRepo.getByJidById(jid, update.key.id);
if (!message) {
this.logger.warn(
`got update for non-existent message. update: '${JSON.stringify(
update,
)}'`,
);
continue;
}
const fields = { ...update.update };
// check if fields has only "status" field
const onlyStatusField =
Object.keys(fields).length === 1 &&
'status' in fields &&
fields.status !== null;
if (onlyStatusField) {
// if so, check the message don't have a newer status
if (message.status >= fields.status) {
continue;
}
}
// It can overwrite the key, so we need to delete it
delete fields['key'];
Object.assign(message, fields);
@@ -226,8 +282,8 @@ export class NowebPersistentStore implements INowebStore {
for (const chat of chats) {
delete chat['messages'];
chat.conversationTimestamp = toNumber(chat.conversationTimestamp) || null;
await this.chatRepo.save(chat);
}
await this.chatRepo.upsertMany(chats);
this.logger.info(`store sync - '${chats.length}' synced chats`);
}
@@ -269,6 +325,9 @@ export class NowebPersistentStore implements INowebStore {
if (!group) {
group = { id: id, participants: [] } as GroupMetadata;
}
if (!group.participants) {
group.participants = [];
}
const participantsById = new DefaultMap<string, GroupParticipant>((key) => {
return { id: key, admin: null } as GroupParticipant;
@@ -330,15 +389,19 @@ export class NowebPersistentStore implements INowebStore {
}
private async onContactsUpsert(contacts: Contact[]) {
const upserts = [];
const ids = contacts.map((c) => c.id);
const contactById = await this.contactRepo.getEntitiesByIds(ids);
for (const update of contacts) {
const contact = await this.contactRepo.getById(update.id);
const contact = contactById.get(update.id) || {};
// remove undefined from data
Object.keys(update).forEach(
(key) => update[key] === undefined && delete update[key],
);
const result = { ...(contact || {}), ...update };
await this.contactRepo.save(result);
const result = { ...contact, ...update };
upserts.push(result);
}
await this.contactRepo.upsertMany(upserts);
}
private async onContactUpdate(updates: Partial<Contact>[]) {
@@ -360,7 +423,12 @@ export class NowebPersistentStore implements INowebStore {
if (update.imgUrl === 'changed') {
contact.imgUrl = this.socket
? await this.socket?.profilePictureUrl(contact.id)
? await this.socket?.profilePictureUrl(contact.id).catch((error) => {
this.logger.warn(
`failed to get profile picture for contact '${contact.id}': ${error}`,
);
return undefined;
})
: undefined;
} else if (update.imgUrl === 'removed') {
delete contact.imgUrl;
@@ -427,7 +495,12 @@ export class NowebPersistentStore implements INowebStore {
}
async loadMessage(jid: string, id: string) {
const data = await this.messagesRepo.getByJidById(jid, id);
let data;
if (!jid) {
data = await this.messagesRepo.getById(id);
} else {
data = await this.messagesRepo.getByJidById(jid, id);
}
if (!data) {
return null;
}
@@ -454,6 +527,10 @@ export class NowebPersistentStore implements INowebStore {
return this.chatRepo.getAllWithMessages(pagination, broadcast);
}
async getChat(jid: string): Promise<Chat | null> {
return await this.chatRepo.getById(jid);
}
private shouldFetchGroup(): boolean {
const timePassed = new Date().getTime() - this.lastTimeGroupFetch.getTime();
return timePassed > this.GROUP_METADATA_CACHE_TIME;
@@ -521,4 +598,64 @@ export class NowebPersistentStore implements INowebStore {
const ids = associations.map((association) => association.labelId);
return await this.labelsRepo.getAllByIds(ids);
}
//
// Lid methods
//
private async handleLidPNUpdates(contacts: Array<Partial<Contact>>) {
let lids: LidToPN[] = [];
for (const contact of contacts) {
// contact.id = pn, contact.lid = lid
if (isJidUser(contact.id) && isLidUser(contact.lid)) {
lids.push({
pn: contact.id,
id: contact.lid,
});
}
// contact.pn = pn, contact.lid = lid
else if (isJidUser(contact.pn) && isLidUser(contact.lid)) {
lids.push({
pn: contact.pn,
id: contact.lid,
});
}
// contact.pn = pn, contact.id = lid
else if (isJidUser(contact.pn) && isLidUser(contact.id)) {
lids.push({
pn: contact.pn,
id: contact.id,
});
}
}
// make lids unique by id
lids = lodash.uniqBy(lids, 'id');
if (lids.length > 0) {
await this.lidRepo.saveLids(lids);
}
return lids;
}
async getAllLids(
pagination?: LimitOffsetParams,
): Promise<LidToPhoneNumber[]> {
const lids = await this.lidRepo.getAllLids(pagination);
return lids.map((value) => {
return {
lid: value.id,
pn: value.pn,
};
});
}
getLidsCount(): Promise<number> {
return this.lidRepo.getLidsCount();
}
findPNByLid(lid: string): Promise<string | null> {
return this.lidRepo.findPNByLid(lid);
}
findLidByPN(pn: string): Promise<string | null> {
return this.lidRepo.findLidByPN(pn);
}
}
@@ -0,0 +1,564 @@
/* eslint-disable @typescript-eslint/no-non-null-assertion */
import type {
BaileysEventEmitter,
Chat,
ConnectionState,
Contact,
GroupMetadata,
PresenceData,
WAMessage,
WAMessageCursor,
WAMessageKey,
} from '@adiwajshing/baileys/lib/Types';
import { Label } from '@adiwajshing/baileys/lib/Types/Label';
import {
LabelAssociation,
LabelAssociationType,
MessageLabelAssociation,
} from '@adiwajshing/baileys/lib/Types/LabelAssociation';
import {
md5,
toNumber,
updateMessageWithReaction,
updateMessageWithReceipt,
} from '@adiwajshing/baileys/lib/Utils';
import {
jidDecode,
jidNormalizedUser,
} from '@adiwajshing/baileys/lib/WABinary';
import { proto } from '@adiwajshing/baileys/WAProto';
import type { Comparable } from '@adiwajshing/keyed-db/lib/Types';
import type { Logger } from 'pino';
import makeOrderedDictionary from './make-ordered-dictionary';
import { ObjectRepository } from './object-repository';
// eslint-disable-next-line @typescript-eslint/no-var-requires
const KeyedDB = require('@adiwajshing/keyed-db').default;
// eslint-disable-next-line @typescript-eslint/no-var-requires
const { writeFileSync } = require('fs');
// eslint-disable-next-line @typescript-eslint/no-var-requires
const { readFileSync, existsSync } = require('fs');
type WASocket = ReturnType<any>;
export const waChatKey = (pin: boolean) => ({
key: (c: Chat) =>
(pin ? (c.pinned ? '1' : '0') : '') +
(c.archived ? '0' : '1') +
(c.conversationTimestamp
? c.conversationTimestamp.toString(16).padStart(8, '0')
: '') +
c.id,
compare: (k1: string, k2: string) => k2.localeCompare(k1),
});
export const waMessageID = (m: WAMessage) => m.key.id || '';
export const waLabelAssociationKey: Comparable<LabelAssociation, string> = {
key: (la: LabelAssociation) =>
la.type === LabelAssociationType.Chat
? la.chatId + la.labelId
: la.chatId + la.messageId + la.labelId,
compare: (k1: string, k2: string) => k2.localeCompare(k1),
};
export type BaileysInMemoryStoreConfig = {
chatKey?: Comparable<Chat, string>;
labelAssociationKey?: Comparable<LabelAssociation, string>;
logger: Logger;
socket?: WASocket;
};
const makeMessagesDictionary = () => makeOrderedDictionary(waMessageID);
export default (config: BaileysInMemoryStoreConfig) => {
const socket = config.socket;
const chatKey = config.chatKey || waChatKey(true);
const labelAssociationKey =
config.labelAssociationKey || waLabelAssociationKey;
const logger: Logger = config.logger;
const chats = new KeyedDB(chatKey, (c) => c.id) as any;
const messages: { [_: string]: ReturnType<typeof makeMessagesDictionary> } =
{};
const contacts: { [_: string]: Contact } = {};
const groupMetadata: { [_: string]: GroupMetadata } = {};
let presences: { [id: string]: { [participant: string]: PresenceData } } = {};
const state: ConnectionState = { connection: 'close' };
const labels = new ObjectRepository<Label>();
const labelAssociations = new KeyedDB(
labelAssociationKey,
labelAssociationKey.key,
) as any;
const assertMessageList = (jid: string) => {
if (!messages[jid]) {
messages[jid] = makeMessagesDictionary();
}
return messages[jid];
};
const contactsUpsert = (newContacts: Contact[]) => {
const oldContacts = new Set(Object.keys(contacts));
for (const contact of newContacts) {
oldContacts.delete(contact.id);
contacts[contact.id] = Object.assign(contacts[contact.id] || {}, contact);
}
return oldContacts;
};
const labelsUpsert = (newLabels: Label[]) => {
for (const label of newLabels) {
labels.upsertById(label.id, label);
}
};
/**
* binds to a BaileysEventEmitter.
* It listens to all events and constructs a state that you can query accurate data from.
* Eg. can use the store to fetch chats, contacts, messages etc.
* @param ev typically the event emitter from the socket connection
*/
const bind = (ev: BaileysEventEmitter) => {
ev.on('connection.update', (update) => {
Object.assign(state, update);
});
ev.on(
'messaging-history.set',
({
chats: newChats,
contacts: newContacts,
messages: newMessages,
isLatest,
syncType,
}) => {
if (syncType === proto.HistorySync.HistorySyncType.ON_DEMAND) {
return; // FOR NOW,
//TODO: HANDLE
}
if (isLatest) {
chats.clear();
for (const id in messages) {
delete messages[id];
}
}
const chatsAdded = chats.insertIfAbsent(...newChats).length;
logger.debug({ chatsAdded }, 'synced chats');
const oldContacts = contactsUpsert(newContacts);
if (isLatest) {
for (const jid of oldContacts) {
delete contacts[jid];
}
}
logger.debug(
{ deletedContacts: isLatest ? oldContacts.size : 0, newContacts },
'synced contacts',
);
for (const msg of newMessages) {
const jid = msg.key.remoteJid!;
const list = assertMessageList(jid);
list.upsert(msg, 'prepend');
}
logger.debug({ messages: newMessages.length }, 'synced messages');
},
);
ev.on('contacts.upsert', (contacts) => {
contactsUpsert(contacts);
});
ev.on('contacts.update', async (updates) => {
for (const update of updates) {
let contact: Contact;
if (contacts[update.id!]) {
contact = contacts[update.id!];
} else {
const contactHashes = await Promise.all(
Object.keys(contacts).map(async (contactId) => {
const { user } = jidDecode(contactId)!;
return [
contactId,
(await md5(Buffer.from(user + 'WA_ADD_NOTIF', 'utf8')))
.toString('base64')
.slice(0, 3),
];
}),
);
contact =
contacts[contactHashes.find(([, b]) => b === update.id)?.[0] || '']; // find contact by attrs.hash, when user is not saved as a contact
}
if (contact) {
if (update.imgUrl === 'changed') {
contact.imgUrl = socket
? await socket?.profilePictureUrl(contact.id)
: undefined;
} else if (update.imgUrl === 'removed') {
delete contact.imgUrl;
}
} else {
return logger.debug(
{ update },
'got update for non-existant contact',
);
}
Object.assign(contacts[contact.id], contact);
}
});
ev.on('chats.upsert', (newChats) => {
chats.upsert(...newChats);
});
ev.on('chats.update', (updates) => {
for (let update of updates) {
const result = chats.update(update.id!, (chat) => {
if (update.unreadCount! > 0) {
update = { ...update };
update.unreadCount = (chat.unreadCount || 0) + update.unreadCount!;
}
Object.assign(chat, update);
});
if (!result) {
logger.debug({ update }, 'got update for non-existant chat');
}
}
});
ev.on('labels.edit', (label: Label) => {
if (label.deleted) {
return labels.deleteById(label.id);
}
// WhatsApp can store only up to 20 labels
if (labels.count() < 20) {
return labels.upsertById(label.id, label);
}
logger.error('Labels count exceed');
});
ev.on('labels.association', ({ type, association }) => {
switch (type) {
case 'add':
labelAssociations.upsert(association);
break;
case 'remove':
labelAssociations.delete(association);
break;
default:
console.error(`unknown operation type [${type}]`);
}
});
ev.on('presence.update', ({ id, presences: update }) => {
presences[id] = presences[id] || {};
Object.assign(presences[id], update);
});
ev.on('chats.delete', (deletions) => {
for (const item of deletions) {
if (chats.get(item)) {
chats.deleteById(item);
}
}
});
ev.on('messages.upsert', ({ messages: newMessages, type }) => {
switch (type) {
case 'append':
case 'notify':
for (const msg of newMessages) {
const jid = jidNormalizedUser(msg.key.remoteJid!);
const list = assertMessageList(jid);
list.upsert(msg, 'append');
if (type === 'notify' && !chats.get(jid)) {
ev.emit('chats.upsert', [
{
id: jid,
conversationTimestamp: toNumber(msg.messageTimestamp),
unreadCount: 1,
},
]);
}
}
break;
}
});
ev.on('messages.update', (updates) => {
for (const { update, key } of updates) {
const list = assertMessageList(jidNormalizedUser(key.remoteJid!));
if (update?.status) {
const listStatus = list.get(key.id!)?.status;
if (listStatus && update?.status <= listStatus) {
logger.debug(
{ update, storedStatus: listStatus },
'status stored newer then update',
);
delete update.status;
logger.debug({ update }, 'new update object');
}
}
const result = list.updateAssign(key.id!, update);
if (!result) {
logger.debug({ update }, 'got update for non-existent message');
}
}
});
ev.on('messages.delete', (item) => {
if ('all' in item) {
const list = messages[item.jid];
list?.clear();
} else {
const jid = item.keys[0].remoteJid!;
const list = messages[jid];
if (list) {
const idSet = new Set(item.keys.map((k) => k.id));
list.filter((m) => !idSet.has(m.key.id));
}
}
});
ev.on('groups.update', (updates) => {
for (const update of updates) {
const id = update.id!;
if (groupMetadata[id]) {
Object.assign(groupMetadata[id], update);
} else {
logger.debug(
{ update },
'got update for non-existant group metadata',
);
}
}
});
ev.on('group-participants.update', ({ id, participants, action }) => {
const metadata = groupMetadata[id];
if (metadata) {
switch (action) {
case 'add':
metadata.participants.push(
...participants.map((id) => ({
id,
isAdmin: false,
isSuperAdmin: false,
})),
);
break;
case 'demote':
case 'promote':
for (const participant of metadata.participants) {
if (participants.includes(participant.id)) {
participant.isAdmin = action === 'promote';
}
}
break;
case 'remove':
metadata.participants = metadata.participants.filter(
(p) => !participants.includes(p.id),
);
break;
}
}
});
ev.on('message-receipt.update', (updates) => {
for (const { key, receipt } of updates) {
const obj = messages[key.remoteJid!];
const msg = obj?.get(key.id!);
if (msg) {
updateMessageWithReceipt(msg, receipt);
}
}
});
ev.on('messages.reaction', (reactions) => {
for (const { key, reaction } of reactions) {
const obj = messages[key.remoteJid!];
const msg = obj?.get(key.id!);
if (msg) {
updateMessageWithReaction(msg, reaction);
}
}
});
};
const toJSON = () => ({
chats,
contacts,
messages,
labels,
labelAssociations,
});
const fromJSON = (json: {
chats: Chat[];
contacts: { [id: string]: Contact };
messages: { [id: string]: WAMessage[] };
labels: { [labelId: string]: Label };
labelAssociations: LabelAssociation[];
}) => {
chats.upsert(...json.chats);
labelAssociations.upsert(...(json.labelAssociations || []));
contactsUpsert(Object.values(json.contacts));
labelsUpsert(Object.values(json.labels || {}));
for (const jid in json.messages) {
const list = assertMessageList(jid);
for (const msg of json.messages[jid]) {
list.upsert(proto.WebMessageInfo.fromObject(msg), 'append');
}
}
};
return {
chats,
contacts,
messages,
groupMetadata,
state,
presences,
setPresences: (obj: any) => {
presences = obj;
},
labels,
labelAssociations,
bind,
/** loads messages from the store, if not found -- uses the legacy connection */
loadMessages: async (
jid: string,
count: number,
cursor: WAMessageCursor,
) => {
const list = assertMessageList(jid);
const mode = !cursor || 'before' in cursor ? 'before' : 'after';
const cursorKey = !!cursor
? 'before' in cursor
? cursor.before
: cursor.after
: undefined;
const cursorValue = cursorKey ? list.get(cursorKey.id!) : undefined;
let messages: WAMessage[];
if (list && mode === 'before' && (!cursorKey || cursorValue)) {
if (cursorValue) {
const msgIdx = list.array.findIndex(
(m) => m.key.id === cursorKey?.id,
);
messages = list.array.slice(0, msgIdx);
} else {
messages = list.array;
}
const diff = count - messages.length;
if (diff < 0) {
messages = messages.slice(-count); // get the last X messages
}
} else {
messages = [];
}
return messages;
},
/**
* Get all available labels for profile
*
* Keep in mind that the list is formed from predefined tags and tags
* that were "caught" during their editing.
*/
getLabels: () => {
return labels;
},
/**
* Get labels for chat
*
* @returns Label IDs
**/
getChatLabels: (chatId: string) => {
return labelAssociations.filter((la) => la.chatId === chatId).all();
},
/**
* Get labels for message
*
* @returns Label IDs
**/
getMessageLabels: (messageId: string) => {
const associations = labelAssociations
.filter((la: MessageLabelAssociation) => la.messageId === messageId)
.all();
return associations.map(({ labelId }) => labelId);
},
loadMessage: async (jid: string, id: string) => messages[jid]?.get(id),
mostRecentMessage: async (jid: string) => {
const message: WAMessage | undefined = messages[jid]?.array.slice(-1)[0];
return message;
},
fetchImageUrl: async (jid: string, sock: WASocket | undefined) => {
const contact = contacts[jid];
if (!contact) {
return sock?.profilePictureUrl(jid);
}
if (typeof contact.imgUrl === 'undefined') {
contact.imgUrl = await sock?.profilePictureUrl(jid);
}
return contact.imgUrl;
},
fetchGroupMetadata: async (jid: string, sock: WASocket | undefined) => {
if (!groupMetadata[jid]) {
const metadata = await sock?.groupMetadata(jid);
if (metadata) {
groupMetadata[jid] = metadata;
}
}
return groupMetadata[jid];
},
// fetchBroadcastListInfo: async(jid: string, sock: WASocket | undefined) => {
// if(!groupMetadata[jid]) {
// const metadata = await sock?.getBroadcastListInfo(jid)
// if(metadata) {
// groupMetadata[jid] = metadata
// }
// }
// return groupMetadata[jid]
// },
fetchMessageReceipts: async ({ remoteJid, id }: WAMessageKey) => {
const list = messages[remoteJid!];
const msg = list?.get(id!);
return msg?.userReceipt;
},
toJSON,
fromJSON,
writeToFile: (path: string) => {
writeFileSync(path, JSON.stringify(toJSON()));
},
readFromFile: (path: string) => {
if (existsSync(path)) {
logger.debug({ path }, 'reading from file');
const jsonStr = readFileSync(path, { encoding: 'utf-8' });
const json = JSON.parse(jsonStr);
fromJSON(json);
}
},
};
};
@@ -0,0 +1,86 @@
function makeOrderedDictionary<T>(idGetter: (item: T) => string) {
const array: T[] = [];
const dict: { [_: string]: T } = {};
const get = (id: string): T | undefined => dict[id];
const update = (item: T) => {
const id = idGetter(item);
const idx = array.findIndex((i) => idGetter(i) === id);
if (idx >= 0) {
array[idx] = item;
dict[id] = item;
}
return false;
};
const upsert = (item: T, mode: 'append' | 'prepend') => {
const id = idGetter(item);
if (get(id)) {
update(item);
} else {
if (mode === 'append') {
array.push(item);
} else {
array.splice(0, 0, item);
}
dict[id] = item;
}
};
const remove = (item: T) => {
const id = idGetter(item);
const idx = array.findIndex((i) => idGetter(i) === id);
if (idx >= 0) {
array.splice(idx, 1);
delete dict[id];
return true;
}
return false;
};
return {
array,
get,
upsert,
update,
remove,
updateAssign: (id: string, update: Partial<T>) => {
const item = get(id);
if (item) {
Object.assign(item, update);
delete dict[id];
dict[idGetter(item)] = item;
return true;
}
return false;
},
clear: () => {
array.splice(0, array.length);
for (const key of Object.keys(dict)) {
delete dict[key];
}
},
filter: (contain: (item: T) => boolean) => {
let i = 0;
while (i < array.length) {
if (!contain(array[i])) {
delete dict[idGetter(array[i])];
array.splice(i, 1);
} else {
i += 1;
}
}
},
toJSON: () => array,
fromJSON: (newItems: T[]) => {
array.splice(0, array.length, ...newItems);
},
};
}
export default makeOrderedDictionary;
@@ -0,0 +1,32 @@
export class ObjectRepository<T extends any> {
readonly entityMap: Map<string, T>;
constructor(entities: Record<string, T> = {}) {
this.entityMap = new Map(Object.entries(entities));
}
findById(id: string) {
return this.entityMap.get(id);
}
findAll() {
return Array.from(this.entityMap.values());
}
upsertById(id: string, entity: T) {
// @ts-ignore
return this.entityMap.set(id, { ...entity });
}
deleteById(id: string) {
return this.entityMap.delete(id);
}
count() {
return this.entityMap.size;
}
toJSON() {
return this.findAll();
}
}
+14
View File
@@ -65,6 +65,15 @@ export const NowebLabelAssociationsSchema = new Schema(
],
);
export const NowebLidMapSchema = new Schema(
'lid_map',
[new Field('id', 'TEXT'), new Field('pn', 'TEXT'), new Field('data', 'TEXT')],
[
new Index('lid_map_id_index', ['id']),
new Index('lid_map_pn_index', ['pn']),
],
);
export const NOWEB_STORE_SCHEMA = [
NowebContactSchema,
NowebChatSchema,
@@ -72,6 +81,7 @@ export const NOWEB_STORE_SCHEMA = [
NowebMessagesSchema,
NowebLabelsSchema,
NowebLabelAssociationsSchema,
NowebLidMapSchema,
];
export const Migrations: string[] = [
@@ -100,4 +110,8 @@ export const Migrations: string[] = [
'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")',
// Lid Map
'CREATE TABLE IF NOT EXISTS lid_map (id TEXT PRIMARY KEY, pn TEXT, data TEXT)',
'CREATE UNIQUE INDEX IF NOT EXISTS lid_map_id_index ON lid_map (id)',
'CREATE INDEX IF NOT EXISTS lid_map_pn_index ON lid_map (pn)',
];
@@ -1,5 +1,6 @@
import { ALL_JID } from '@waha/core/engines/noweb/session.noweb.core';
import { SqlKVRepository } from '@waha/core/storage/sql/SqlKVRepository';
import { AckToStatus } from '@waha/core/utils/acks';
import { GetChatMessagesFilter } from '@waha/structures/chats.dto';
import { PaginationParams } from '@waha/structures/pagination.dto';
@@ -35,14 +36,26 @@ export class SqlMessagesMethods {
}
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']);
const [sql, value] = this.repository.filterJson(
'data',
'key.fromMe',
filter['filter.fromMe'],
);
query = query.whereRaw(sql, [value]);
}
if (filter['filter.ack'] != null) {
const status = AckToStatus(filter['filter.ack']);
const [sql, value] = this.repository.filterJson('data', 'status', status);
query = query.whereRaw(sql, [value]);
}
query = this.repository.pagination(query, pagination);
return this.repository.all(query);
}
async getByJidById(jid: string, id: string): Promise<any> {
if (jid === ALL_JID) {
return this.repository.getBy({ id: id });
}
return this.repository.getBy({ jid: jid, id: id });
}
@@ -0,0 +1,41 @@
import { NowebLidMapSchema } from '@waha/core/engines/noweb/store/schemas';
import { LimitOffsetParams } from '@waha/structures/pagination.dto';
import { KnexPaginator } from '@waha/utils/Paginator';
import { INowebLidPNRepository, LidToPN } from '../INowebLidPNRepository';
import { NOWEBSqlite3KVRepository } from './NOWEBSqlite3KVRepository';
export class LidPaginator extends KnexPaginator {
indexes = ['id', 'pn'];
}
export class Sqlite3LidPNRepository
extends NOWEBSqlite3KVRepository<LidToPN>
implements INowebLidPNRepository
{
get schema() {
return NowebLidMapSchema;
}
saveLids(lids: LidToPN[]): Promise<void> {
return this.upsertMany(lids);
}
getAllLids(pagination?: LimitOffsetParams): Promise<LidToPN[]> {
return this.getAll(pagination);
}
getLidsCount(): Promise<number> {
return this.getCount();
}
async findLidByPN(pn: string): Promise<string | null> {
const value = await this.getBy({ pn: pn });
return value?.id || null;
}
async findPNByLid(lid: string): Promise<string | null> {
const value = await this.getBy({ id: lid });
return value?.pn || null;
}
}
@@ -1,8 +1,10 @@
import { ILabelAssociationRepository } from '@waha/core/engines/noweb/store/ILabelAssociationsRepository';
import { ILabelsRepository } from '@waha/core/engines/noweb/store/ILabelsRepository';
import { INowebLidPNRepository } from '@waha/core/engines/noweb/store/INowebLidPNRepository';
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 { Sqlite3LidPNRepository } from '@waha/core/engines/noweb/store/sqlite3/Sqlite3LidPNRepository';
import { Schema } from '@waha/core/storage/Schema';
import { INowebStorage } from '../INowebStorage';
@@ -74,4 +76,8 @@ export class Sqlite3Storage extends INowebStorage {
getMessagesRepository() {
return new Sqlite3MessagesRepository(this.db);
}
getLidPNRepository(): INowebLidPNRepository {
return new Sqlite3LidPNRepository(this.db);
}
}
@@ -18,7 +18,10 @@ const AsyncLock = require('async-lock');
// https://github.com/nodejs/node/issues/26338
// Default pending is 1000, set it to infinity
// https://github.com/rogierschouten/async-lock/issues/63
const fileLock = new AsyncLock({ maxPending: Infinity });
const fileLock = new AsyncLock({
maxPending: Infinity,
maxExecutionTime: 30_000,
});
/**
* stores the full authentication state in a single folder.
+110
View File
@@ -0,0 +1,110 @@
import { sleep } from '@waha/utils/promiseTimeout';
import { EventEmitter } from 'events';
import { Key } from 'node-cache';
import { Page } from 'puppeteer';
export interface CallErrorEvent {
method: string | symbol;
error: unknown;
}
export const PAGE_CALL_ERROR_EVENT = 'page.call.error';
/**
* WPage is a puppeter.Page wrapper that use ALL original methods from Page,
* but in case if there were any errors in the call (original) -
* it calls the errorCallback with method name, error and arguments
*
* Find in webjs used functions like "pupPage.{function}"
* >>> cd src
* >>> grep -rho 'page\.[a-zA-Z0-9_]\+(' . --include="*.js" | sed 's/($//' | sort -u
*
* Find in webjs used functions like "page.{function}"
* >>> cd src
* >>> grep -rho 'page\.[a-zA-Z0-9_]\+(' . --include="*.js" | sed 's/($//' | sort -u
*
*/
export class WPage extends EventEmitter {
constructor(private page: Page) {
super();
}
async evaluate(fn, ...args: any[]) {
try {
return await this.page.evaluate(fn, ...args);
} catch (err) {
this.emit(PAGE_CALL_ERROR_EVENT, { method: 'evaluate', error: err });
throw err;
}
}
async screenshot(options) {
try {
return await this.page.screenshot(options);
} catch (err) {
this.emit(PAGE_CALL_ERROR_EVENT, { method: 'screenshot', error: err });
throw err;
}
}
// waitForNavigation
async waitForNavigation(options: any) {
try {
return await this.page.waitForNavigation(options);
} catch (err) {
this.emit(PAGE_CALL_ERROR_EVENT, {
method: 'waitForNavigation',
error: err,
});
throw err;
}
}
// waitForFunction
async waitForFunction(fn, options, ...args) {
try {
return await this.page.waitForFunction(fn, options, ...args);
} catch (err) {
this.emit(PAGE_CALL_ERROR_EVENT, {
method: 'waitForFunction',
error: err,
});
throw err;
}
}
// setRequestInterception
async setRequestInterception(value: boolean) {
try {
return await this.page.setRequestInterception(value);
} catch (err) {
this.emit(PAGE_CALL_ERROR_EVENT, {
method: 'setRequestInterception',
error: err,
});
throw err;
}
}
// exposeFunction
async exposeFunction(name: string, fn: (...args: any[]) => any) {
try {
return await this.page.exposeFunction(name, fn);
} catch (err) {
this.emit(PAGE_CALL_ERROR_EVENT, {
method: 'exposeFunction',
error: err,
});
throw err;
}
}
on(event: string | symbol, listener: (...args: any[]) => void): this {
if (event === PAGE_CALL_ERROR_EVENT) {
super.on(event, listener);
return this;
}
this.page.on(event, listener);
return this.page as any;
}
}
+127 -3
View File
@@ -1,10 +1,16 @@
import { GetChatMessagesFilter } from '@waha/structures/chats.dto';
import { Label } from '@waha/structures/labels.dto';
import { LidToPhoneNumber } from '@waha/structures/lids.dto';
import { PaginationParams } from '@waha/structures/pagination.dto';
import { TextStatus } from '@waha/structures/status.dto';
import { EventEmitter } from 'events';
import * as lodash from 'lodash';
import { Page } from 'puppeteer';
import { Client, Events } from 'whatsapp-web.js';
import { Message } from 'whatsapp-web.js/src/structures';
import { exposeFunctionIfAbsent } from 'whatsapp-web.js/src/util/Puppeter';
import { CallErrorEvent, PAGE_CALL_ERROR_EVENT, WPage } from './WPage';
// eslint-disable-next-line @typescript-eslint/no-var-requires
const { LoadWAHA } = require('./_WAHA.js');
@@ -19,20 +25,53 @@ const { LoadPaginator } = require('./_Paginator.js');
const ChatFactory = require('whatsapp-web.js/src/factories/ChatFactory');
export class WebjsClientCore extends Client {
public events = new EventEmitter();
private wpage: WPage = null;
constructor(options) {
super(options);
// Wait until it's READY and inject more utils
this.on(Events.READY, async () => {
await this.attachCustomEventListeners();
await this.injectWaha();
});
}
async initialize() {
const result = await super.initialize();
if (this.pupPage && !(this.pupPage instanceof WPage)) {
this.wpage = new WPage(this.pupPage);
this.wpage.on(PAGE_CALL_ERROR_EVENT as any, (event: CallErrorEvent) => {
this.events.emit(PAGE_CALL_ERROR_EVENT as any, event);
});
this.pupPage = this.wpage as any as Page;
}
return result;
}
async injectWaha() {
await this.pupPage.evaluate(LoadLodash);
await this.pupPage.evaluate(LoadPaginator);
await this.pupPage.evaluate(LoadWAHA);
}
async attachCustomEventListeners() {
await exposeFunctionIfAbsent(
this.pupPage,
'onNewMessageId',
(messageId: string) => {
this.events.emit('message.id', { id: messageId });
return;
},
);
}
async destroy() {
this.events.removeAllListeners();
this.wpage?.removeAllListeners();
await super.destroy();
}
async setPushName(name: string) {
await this.pupPage.evaluate(async (pushName) => {
return await window['WAHA'].WAWebSetPushnameConnAction.setPushname(
@@ -61,7 +100,7 @@ export class WebjsClientCore extends Client {
}
async createLabel(name: string, color: number): Promise<number> {
const labelId: number = await this.pupPage.evaluate(
const labelId: number = (await this.pupPage.evaluate(
async (name, color) => {
// @ts-ignore
return await window.WAHA.WAWebBizLabelEditingAction.labelAddAction(
@@ -71,7 +110,7 @@ export class WebjsClientCore extends Client {
},
name,
color,
);
)) as any;
return labelId;
}
@@ -178,6 +217,9 @@ export class WebjsClientCore extends Client {
) {
return false;
}
if (filter['filter.ack'] != null && m.ack !== filter['filter.ack']) {
return false;
}
return true;
};
@@ -189,8 +231,17 @@ export class WebjsClientCore extends Client {
const loadedMessages =
// @ts-ignore
await window.Store.ConversationMsgs.loadEarlierMsgs(chat);
if (!loadedMessages || !loadedMessages.length) break;
if (!loadedMessages || loadedMessages.length == 0) break;
msgs = [...loadedMessages.filter(msgFilter), ...msgs];
msgs = msgs.sort((a, b) => b.t - a.t);
// Check if the earliest message is already outside the timerange filter
const earliest = msgs[msgs.length - 1];
if (earliest.t < (filter['filter.timestamp.gte'] || Infinity)) {
// Add only messages that pass the filter and stop loading more
break;
}
}
if (msgs.length > pagination.limit + pagination.offset) {
@@ -212,4 +263,77 @@ export class WebjsClientCore extends Client {
return messages.map((m) => new Message(this, m));
}
public async getAllLids(
pagination: PaginationParams,
): Promise<Array<LidToPhoneNumber>> {
const lids: Array<LidToPhoneNumber> = (await this.pupPage.evaluate(
async (pagination) => {
pagination.limit ||= Infinity;
pagination.offset ||= 0;
pagination.sortBy ||= 'lid';
// @ts-ignore
const WAWebApiContact = window.Store.LidUtils;
await WAWebApiContact.warmUpAllLidPnMappings();
const lidMap = WAWebApiContact.lidPnCache['$1'];
const values = Array.from(lidMap.values());
const result = values.map((map) => {
return {
// @ts-ignore
lid: map.lid._serialized,
// @ts-ignore
pn: map.phoneNumber._serialized,
};
});
// @ts-ignore
const paginator = new window.Paginator(pagination);
const page = paginator.apply(result);
return page;
},
pagination,
)) as any;
return lids;
}
public async getLidsCount(): Promise<number> {
const count: number = (await this.pupPage.evaluate(async () => {
// @ts-ignore
const WAWebApiContact = window.Store.LidUtils;
await WAWebApiContact.warmUpAllLidPnMappings();
const lidMap = WAWebApiContact.lidPnCache['$1'];
return lidMap.size;
})) as any;
return count;
}
public async findPNByLid(lid: string): Promise<string> {
const pn = await this.pupPage.evaluate(async (lid) => {
// @ts-ignore
const WAWebApiContact = window.Store.LidUtils;
// @ts-ignore
const WAWebWidFactory = window.Store.WidFactory;
const wid = WAWebWidFactory.createWid(lid);
const result = WAWebApiContact.getPhoneNumber(wid);
return result ? result._serialized : null;
}, lid);
return pn;
}
public async findLIDByPhoneNumber(phoneNumber: string): Promise<string> {
const lid: string = (await this.pupPage.evaluate(async (pn) => {
// @ts-ignore
const WAWebApiContact = window.Store.LidUtils;
// @ts-ignore
const WAWebWidFactory = window.Store.WidFactory;
const wid = WAWebWidFactory.createWid(pn);
const result = WAWebApiContact.getCurrentLid(wid);
return result ? result._serialized : null;
}, phoneNumber)) as any;
return lid;
}
}
+11 -1
View File
@@ -17,7 +17,7 @@ exports.LoadPaginator = () => {
}
return window.lodash.orderBy(
data,
[this.pagination.sortBy],
[this.NullLast(this.pagination.sortBy)],
[this.pagination.sortOrder || 'asc'],
);
}
@@ -30,6 +30,16 @@ exports.LoadPaginator = () => {
const limit = this.pagination.limit || Infinity;
return data.slice(offset, offset + limit);
}
NullLast(field) {
return (item) => {
const value = item?.[field];
if (value == null) {
return -Infinity;
}
return value;
};
}
}
window.Paginator = Paginator;
};
+163
View File
@@ -0,0 +1,163 @@
import {
areJidsSameUser,
BinaryNode,
getBinaryNodeChildren,
getStatusFromReceiptType,
isJidGroup,
isJidStatusBroadcast,
jidEncode,
jidNormalizedUser,
proto,
} from '@adiwajshing/baileys';
export interface ReceiptEvent {
key: proto.IMessageKey;
participant?: string;
messageIds: string[];
status: number;
_node: any;
}
interface Me {
id: string;
lid?: string;
}
function jid(field: any) {
if (!field) {
return field;
}
const data = field['$1'];
if (!data) {
return data;
}
let server = data.server;
if (!server) {
server =
data.domainType === 0 || data.domainType === 128
? 's.whatsapp.net'
: 'lid';
}
return jidEncode(data.user, server, data.device);
}
export function TagReceiptNodeToReceiptEvent(
node: BinaryNode,
me: Me,
): ReceiptEvent[] {
const { attrs, content } = node;
const status = getStatusFromReceiptType(attrs.type);
if (status == null) {
return null;
}
const from = jidNormalizedUser(jid(attrs.from));
const participant = jidNormalizedUser(jid(attrs.participant));
const recipient = jidNormalizedUser(jid(attrs.recipient));
const isLid = from.includes('lid');
const isNodeFromMe = areJidsSameUser(
participant || from,
isLid ? me?.lid : me?.id,
);
const remoteJid = !isNodeFromMe || isJidGroup(from) ? from : recipient;
const fromMe = !recipient || (attrs.type === 'retry' && isNodeFromMe);
// basically, we only want to know when a message from us has been delivered to/read by the other person
// or another device of ours has read some messages
if (status < proto.WebMessageInfo.Status.SERVER_ACK && isNodeFromMe) {
return [];
}
const key: proto.IMessageKey = {
remoteJid: remoteJid,
id: '',
fromMe: fromMe,
};
const ids = [attrs.id];
if (Array.isArray(content)) {
const items = getBinaryNodeChildren(content[0], 'item');
ids.push(...items.map((i) => i.attrs.id));
}
if (isJidGroup(remoteJid) || isJidStatusBroadcast(remoteJid)) {
if (participant) {
key.participant = fromMe ? (isLid ? me.lid : me.id) : recipient;
const eventParticipant = fromMe ? participant : isLid ? me.lid : me.id;
return [
{
key: key,
messageIds: ids,
status: status as any,
participant: eventParticipant,
_node: node,
},
];
} else {
// Handle grouped receipts
return handleGroupedReceipts(node, key, status, fromMe, isLid, me);
}
}
return [
{
key: key,
messageIds: ids,
status: status as any,
_node: node,
},
];
}
function handleGroupedReceipts(
node: BinaryNode,
key: proto.IMessageKey,
status: number,
fromMe: boolean,
isLid: boolean,
me: Me,
): ReceiptEvent[] | null {
const { content } = node;
if (!Array.isArray(content)) {
return [];
}
const participantsTags = content.filter((c) => c.tag === 'participants');
if (participantsTags.length === 0) {
return null;
}
const receiptEvents: ReceiptEvent[] = [];
for (const participants of participantsTags) {
const participantKey = participants.attrs?.key;
if (!participantKey) continue;
const users = getBinaryNodeChildren(participants, 'user');
for (const user of users) {
const userAttrs = user.attrs;
if (!userAttrs) continue;
const userJid = jidNormalizedUser(jid(userAttrs.jid));
if (!userJid) continue;
key.participant = fromMe ? (isLid ? me.lid : me.id) : userJid;
const eventParticipant = fromMe ? userJid : isLid ? me.lid : me.id;
const receiptEvent: ReceiptEvent = {
key: {
...key,
id: participantKey,
},
messageIds: [participantKey],
status: status as any,
participant: eventParticipant,
_node: node,
};
receiptEvents.push(receiptEvent);
}
}
return receiptEvents;
}
+202 -14
View File
@@ -1,8 +1,17 @@
import { isJidGroup, isJidStatusBroadcast } from '@adiwajshing/baileys';
import { UnprocessableEntityException } from '@nestjs/common';
import {
getChannelInviteLink,
WhatsappSession,
} from '@waha/core/abc/session.abc';
import {
getFromToParticipant,
toCusFormat,
} from '@waha/core/engines/noweb/session.noweb.core';
import {
ReceiptEvent,
TagReceiptNodeToReceiptEvent,
} from '@waha/core/engines/webjs/ack.webjs';
import {
ToGroupV2JoinEvent,
ToGroupV2LeaveEvent,
@@ -11,12 +20,22 @@ import {
} from '@waha/core/engines/webjs/groups.webjs';
import { LocalAuth } from '@waha/core/engines/webjs/LocalAuth';
import { WebjsClientCore } from '@waha/core/engines/webjs/WebjsClientCore';
import {
CallErrorEvent,
PAGE_CALL_ERROR_EVENT,
} from '@waha/core/engines/webjs/WPage';
import {
AvailableInPlusVersion,
NotImplementedByEngineError,
} from '@waha/core/exceptions';
import { IMediaEngineProcessor } from '@waha/core/media/IMediaEngineProcessor';
import { QR } from '@waha/core/QR';
import { StatusToAck } from '@waha/core/utils/acks';
import {
parseMessageIdSerialized,
SerializeMessageKey,
} from '@waha/core/utils/ids';
import { DistinctAck } from '@waha/core/utils/reactive';
import { splitAt } from '@waha/helpers';
import { PairingCodeResponse } from '@waha/structures/auth.dto';
import {
@@ -36,11 +55,14 @@ import {
GetChatMessageQuery,
GetChatMessagesFilter,
GetChatMessagesQuery,
ReadChatMessagesQuery,
ReadChatMessagesResponse,
} from '@waha/structures/chats.dto';
import {
ChatRequest,
CheckNumberStatusQuery,
EditMessageRequest,
MessageButtonReply,
MessageFileRequest,
MessageForwardRequest,
MessageImageRequest,
@@ -71,22 +93,30 @@ import {
SettingsSecurityChangeInfo,
} from '@waha/structures/groups.dto';
import { Label, LabelDTO, LabelID } from '@waha/structures/labels.dto';
import { LidToPhoneNumber } from '@waha/structures/lids.dto';
import { ReplyToMessage } from '@waha/structures/message.dto';
import { PaginationParams, SortOrder } from '@waha/structures/pagination.dto';
import { WAMessage, WAMessageReaction } from '@waha/structures/responses.dto';
import {
MessageSource,
WAMessage,
WAMessageReaction,
} from '@waha/structures/responses.dto';
import { MeInfo } from '@waha/structures/sessions.dto';
import { StatusRequest, TextStatus } from '@waha/structures/status.dto';
import {
EnginePayload,
WAMessageAckBody,
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 * as lodash from 'lodash';
import { ProtocolError } from 'puppeteer';
import { filter, fromEvent, merge, mergeMap, Observable } from 'rxjs';
import { map } from 'rxjs/operators';
import {
AuthStrategy,
Call,
Channel as WEBJSChannel,
Chat,
@@ -98,6 +128,7 @@ import {
Label as WEBJSLabel,
Location,
Message,
MessageAck,
MessageMedia,
Reaction,
WAState,
@@ -233,12 +264,29 @@ export class WhatsappSessionWebJSCore extends WhatsappSession {
this.logger.error('The browser has been disconnected');
this.failed();
});
// Listen for page close event
this.whatsapp.pupPage.on('close', () => {
this.logger.error('The WhatsApp Web page has been closed');
this.failed();
});
// Listen for function call errors
this.whatsapp.events.on(
PAGE_CALL_ERROR_EVENT,
(event: CallErrorEvent) => {
if (event.error instanceof ProtocolError) {
this.logger.error(
`ProtocolError when calling page method: ${String(
event.method,
)}, restarting client...`,
);
this.logger.error(event.error);
this.failed();
}
},
);
// Listen for page error event
if (this.isDebugEnabled()) {
this.logger.debug("Logging 'console' event for web page");
@@ -297,10 +345,11 @@ export class WhatsappSessionWebJSCore extends WhatsappSession {
private async end() {
this.engineStateCheckDelayedJob.cancel();
this.whatsapp?.removeAllListeners();
this.whatsapp?.pupBrowser?.removeAllListeners();
this.whatsapp?.pupPage?.removeAllListeners();
try {
this.whatsapp?.removeAllListeners();
this.whatsapp?.pupBrowser?.removeAllListeners();
this.whatsapp?.pupPage?.removeAllListeners();
// It's possible that browser yet starting
await waitUntil(
async () => {
@@ -311,11 +360,30 @@ export class WhatsappSessionWebJSCore extends WhatsappSession {
1_000,
10_000,
);
this.whatsapp?.destroy().catch((error) => {
this.logger.warn(error, 'Failed to destroy the client');
});
this.logger.debug(
'Successfully waited for browser to be ready for closing',
);
} catch (error) {
this.logger.error(error);
this.logger.error(
error,
'Failed while waiting for browser to be ready for closing',
);
}
try {
await this.whatsapp?.destroy();
this.logger.debug('Successfully destroyed whatsapp client');
} catch (error) {
this.logger.error(error, 'Failed to destroy whatsapp client');
}
try {
// @ts-ignore
const strategy: AuthStrategy = this.whatsapp?.authStrategy;
await strategy?.destroy();
this.logger.debug('Successfully destroyed auth strategy');
} catch (error) {
this.logger.error(error, 'Failed to destroy auth strategy');
}
}
@@ -510,6 +578,7 @@ export class WhatsappSessionWebJSCore extends WhatsappSession {
const options = {
// It's fine to sent just ids instead of Contact object
mentions: request.mentions as unknown as string[],
linkPreview: request.linkPreview,
};
return message.edit(request.text, options);
}
@@ -535,6 +604,10 @@ export class WhatsappSessionWebJSCore extends WhatsappSession {
throw new AvailableInPlusVersion();
}
sendButtonsReply(request: MessageButtonReply) {
throw new AvailableInPlusVersion();
}
async sendLocation(request: MessageLocationRequest) {
const location = new Location(request.latitude, request.longitude, {
name: request.title,
@@ -681,6 +754,17 @@ export class WhatsappSessionWebJSCore extends WhatsappSession {
return result;
}
public async readChatMessages(
chatId: string,
request: ReadChatMessagesQuery,
): Promise<ReadChatMessagesResponse> {
const chat: Chat = await this.whatsapp.getChatById(
this.ensureSuffix(chatId),
);
await chat.sendSeen();
return { ids: null };
}
public async getChatMessage(
chatId: string,
messageId: string,
@@ -688,6 +772,19 @@ export class WhatsappSessionWebJSCore extends WhatsappSession {
): Promise<null | WAMessage> {
const message = await this.whatsapp.getMessageById(messageId);
if (!message) return null;
if (
isJidGroup(message.id.remote) ||
isJidStatusBroadcast(message.id.remote)
) {
// @ts-ignore
message.rawData.receipts = await message.getInfo().catch((error) => {
this.logger.error(
{ error: error, msg: message.id._serialized },
'Failed to get receipts',
);
return null;
});
}
return await this.processIncomingMessage(message, query.downloadMedia);
}
@@ -830,6 +927,38 @@ export class WhatsappSessionWebJSCore extends WhatsappSession {
await contact.unblock();
}
/**
* Lid to Phone Number methods
*/
public async getAllLids(
pagination: PaginationParams,
): Promise<Array<LidToPhoneNumber>> {
return this.whatsapp.getAllLids(pagination);
}
public async getLidsCount(): Promise<number> {
return this.whatsapp.getLidsCount();
}
public async findPNByLid(lid: string): Promise<LidToPhoneNumber> {
const phoneNumber = await this.whatsapp.findPNByLid(lid);
return {
lid: lid,
pn: phoneNumber,
};
}
public async findLIDByPhoneNumber(
phoneNumber: string,
): Promise<LidToPhoneNumber> {
const pn = toCusFormat(phoneNumber);
const lid = await this.whatsapp.findLIDByPhoneNumber(pn);
return {
lid: lid,
pn: pn,
};
}
/**
* Group methods
*/
@@ -894,6 +1023,13 @@ export class WhatsappSessionWebJSCore extends WhatsappSession {
return paginator.apply(groups);
}
protected removeGroupsFieldParticipant(group: any) {
delete group.groupMetadata?.participants;
delete group.groupMetadata?.pendingParticipants;
delete group.groupMetadata?.pastParticipants;
delete group.groupMetadata?.membershipApprovalRequests;
}
public async refreshGroups(): Promise<boolean> {
return true;
}
@@ -1040,7 +1176,7 @@ export class WhatsappSessionWebJSCore extends WhatsappSession {
// Exclude GUEST, browser saves the data
// when we search channels or getting messages
channels = channels.filter((channel) => channel.role === 'GUEST');
channels = channels.filter((channel) => channel.role !== 'GUEST');
const promises = channels.map(async (channel) =>
this.whatsapp.getProfilePicUrl(channel.id),
@@ -1146,6 +1282,11 @@ export class WhatsappSessionWebJSCore extends WhatsappSession {
* END - Methods for API
*/
subscribeEngineEvents2() {
// Save sent message in cache
this.whatsapp.events.on('message.id', (data) => {
this.saveSentMessageId(data.id);
});
//
// All
//
@@ -1218,18 +1359,31 @@ export class WhatsappSessionWebJSCore extends WhatsappSession {
);
this.events2.get(WAHAEvents.MESSAGE_REACTION).switch(messagesReaction$);
const messageAck$ = fromEvent(
const messageAckWEBJS$ = fromEvent(
this.whatsapp,
Events.MESSAGE_ACK,
(message, ack) => {
return { message, ack };
},
);
const messagesAck$ = messageAck$.pipe(
const messagesAckDM$ = messageAckWEBJS$.pipe(
map((event) => event.message),
map(this.toWAMessage.bind(this)),
map<any, WAMessage>(this.toWAMessage.bind(this)),
filter((ack) => !isJidGroup(ack.to) && !isJidStatusBroadcast(ack.to)),
);
this.events2.get(WAHAEvents.MESSAGE_ACK).switch(messagesAck$);
const tagReceiptNode$ = fromEvent(this.whatsapp, Events.TAG_RECEIPT);
const messageAckGroups$ = tagReceiptNode$.pipe(
mergeMap((node) =>
TagReceiptNodeToReceiptEvent(node as any, this.getSessionMeInfo()),
),
mergeMap(this.TagReceiptToMessageAck.bind(this)),
filter((ack) => isJidGroup(ack.to) || isJidStatusBroadcast(ack.to)),
);
const messageAckAll$ = merge(messagesAckDM$, messageAckGroups$);
const messageAck$ = messageAckAll$.pipe(DistinctAck());
this.events2.get(WAHAEvents.MESSAGE_ACK).switch(messageAck$);
//
// Others
@@ -1345,10 +1499,12 @@ export class WhatsappSessionWebJSCore extends WhatsappSession {
}
private processMessageReaction(reaction: Reaction): WAMessageReaction {
const source = this.getMessageSource(reaction.id.id);
return {
id: reaction.id._serialized,
from: reaction.senderId,
fromMe: reaction.id.fromMe,
source: source,
participant: reaction.senderId,
to: reaction.id.remote,
timestamp: reaction.timestamp,
@@ -1359,14 +1515,45 @@ export class WhatsappSessionWebJSCore extends WhatsappSession {
};
}
protected TagReceiptToMessageAck(receipt: ReceiptEvent): WAMessageAckBody[] {
const ids = receipt.messageIds;
const acks = [];
for (const id_ of ids) {
const messageKey = {
fromMe: receipt.key.fromMe,
remoteJid: toCusFormat(receipt.key.remoteJid),
participant: toCusFormat(receipt.key.participant),
id: id_,
};
const fromToParticipant = getFromToParticipant(messageKey);
const id = SerializeMessageKey(messageKey);
const ack = StatusToAck(receipt.status);
acks.push({
id: id,
from: fromToParticipant.from,
to: fromToParticipant.to,
participant: toCusFormat(receipt.participant),
fromMe: !receipt.key.fromMe, // reverted, it's right
ack: ack,
ackName: WAMessageAck[ack] || ACK_UNKNOWN,
_data: receipt._node,
});
}
return acks;
}
protected toWAMessage(message: Message): WAMessage {
const replyTo = this.extractReplyTo(message);
const source = this.getMessageSource(message.id.id);
const key = parseMessageIdSerialized(message.id._serialized);
// @ts-ignore
return {
id: message.id._serialized,
timestamp: message.timestamp,
from: message.from,
fromMe: message.fromMe,
participant: toCusFormat(key.participant),
source: source,
to: message.to,
body: message.body,
// Media
@@ -1396,6 +1583,7 @@ export class WhatsappSessionWebJSCore extends WhatsappSession {
id: quotedMsg.id?.id,
participant: quotedMsg.author || quotedMsg.from,
body: quotedMsg.caption || quotedMsg.body,
_data: quotedMsg,
};
}
@@ -1422,7 +1610,7 @@ export class WhatsappSessionWebJSCore extends WhatsappSession {
let mentions = request.mentions;
mentions = mentions ? mentions.map(this.ensureSuffix) : undefined;
const quotedMessageId = request.reply_to;
const quotedMessageId = request.reply_to || request.replyTo;
return {
mentions: mentions,
+10 -7
View File
@@ -1,20 +1,23 @@
export function toVcard(data): string {
import { Contact, VCardContact } from '@waha/structures/chatting.dto';
export function toVcard(data: Contact | VCardContact): string {
if (data.vcard) {
return data.vcard;
}
const contact: Contact = data as any;
const parts = [];
parts.push('BEGIN:VCARD');
parts.push('VERSION:3.0');
parts.push(`FN:${data.fullName}`);
if (data.organization) {
parts.push(`ORG:${data.organization};`);
parts.push(`FN:${contact.fullName}`);
if (contact.organization) {
parts.push(`ORG:${contact.organization};`);
}
if (data.whatsappId) {
if (contact.whatsappId) {
parts.push(
`TEL;type=CELL;type=VOICE;waid=${data.whatsappId}:${data.phoneNumber}`,
`TEL;type=CELL;type=VOICE;waid=${contact.whatsappId}:${contact.phoneNumber}`,
);
} else {
parts.push(`TEL;type=CELL;type=VOICE:${data.phoneNumber}`);
parts.push(`TEL;type=CELL;type=VOICE:${contact.phoneNumber}`);
}
parts.push('END:VCARD');
return parts.join('\n');
@@ -66,7 +66,7 @@ export class WebhookSender {
const headers = {
'content-type': 'application/json',
};
Object.assign(headers, this.getWebhookHeader());
Object.assign(headers, this.getWebhookHeader(json));
Object.assign(headers, this.getHMACHeaders(body));
const ctx = {
id: headers['X-Webhook-Request-Id'],
@@ -153,12 +153,13 @@ export class WebhookSender {
};
}
protected getWebhookHeader() {
protected getWebhookHeader(json: any) {
const timestamp = json.timestamp?.toString() || Date.now().toString();
return {
// UUID, no '-' in it
'X-Webhook-Request-Id': ulid(),
// unix timestamp with ms
'X-Webhook-Timestamp': Date.now().toString(),
'X-Webhook-Timestamp': timestamp,
};
}
+7 -5
View File
@@ -45,9 +45,11 @@ import { LocalSessionAuthRepository } from './storage/LocalSessionAuthRepository
import { LocalStoreCore } from './storage/LocalStoreCore';
export class OnlyDefaultSessionIsAllowed extends UnprocessableEntityException {
constructor() {
constructor(name: string) {
const encoded = Buffer.from(name, 'utf-8').toString('base64');
super(
`WAHA Core support only 'default' session. If you want to run more then one WhatsApp account - please get WAHA PLUS version. Check this out: ${DOCS_URL}`,
`WAHA Core support only 'default' session. You tried to access '${name}' session (base64: ${encoded}). ` +
`If you want to run more then one WhatsApp account - please get WAHA PLUS version. Check this out: ${DOCS_URL}`,
);
}
}
@@ -115,7 +117,7 @@ export class SessionManagerCore extends SessionManager implements OnModuleInit {
private onlyDefault(name: string) {
if (name !== this.DEFAULT) {
throw new OnlyDefaultSessionIsAllowed();
throw new OnlyDefaultSessionIsAllowed(name);
}
}
@@ -362,9 +364,9 @@ export class SessionManagerCore extends SessionManager implements OnModuleInit {
try {
engineInfo = await promiseTimeout(1000, session.getEngineInfo());
} catch (error) {
this.log.warn(
this.log.debug(
{ session: session.name, error: `${error}` },
'Error while getting engine info',
'Can not get engine info',
);
}
}
+1 -1
View File
@@ -1,4 +1,4 @@
export interface IJsonQuery {
filter(field: string, key: string): string;
filter(field: string, key: string, value: any): [string, string];
sortBy(field: string, sortBy: string, direction: string): string;
}
+55 -10
View File
@@ -4,6 +4,7 @@ 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';
import * as lodash from 'lodash';
export type Migration = string;
@@ -79,17 +80,26 @@ export class SqlKVRepository<Entity> {
}
private async upsertBatch(entities: Entity[]): Promise<void> {
const data = entities.map((entity) => this.dump(entity));
const all = entities.map((entity) => this.dump(entity));
// make it unique by .id
const data = lodash.uniqBy(all, (d: any) => d.id);
if (data.length != all.length) {
console.warn(
`WARNING - Duplicated entities for upsert batch: ${JSON.stringify(
entities,
)}`,
);
}
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(', ')}`;
.join(', ')} ON CONFLICT(id) DO
UPDATE
SET ${columns
.map((column) => `${column} = excluded.${column}`)
.join(', ')}`;
try {
await this.raw(sql, values);
} catch (err) {
@@ -109,8 +119,43 @@ export class SqlKVRepository<Entity> {
return this.all(query);
}
getAllByIds(ids: string[]) {
return this.all(this.select().whereIn('id', ids));
async getCount(): Promise<number> {
const query = this.select().count({ count: 'id' });
const row = await this.engine.get(query);
if (!row) {
return 0;
}
return parseInt(row.count, 10);
}
async getAllByIds(ids: string[]) {
const entitiesMap = await this.getEntitiesByIds(ids);
return Array.from(entitiesMap.values()).filter(
(entity) => entity !== null,
) as Entity[];
}
async getEntitiesByIds(ids: string[]): Promise<Map<string, Entity | null>> {
if (ids.length === 0) {
return new Map();
}
const rows = await this.engine.all(this.select().whereIn('id', ids));
const entitiesMap = new Map<string, Entity | null>();
// Initialize a map with null values for all requested IDs
for (const id of ids) {
entitiesMap.set(id, null);
}
// Fill in the map with found entities
for (const row of rows) {
if (row && row.id) {
entitiesMap.set(row.id, this.parse(row));
}
}
return entitiesMap;
}
getById(id: string): Promise<Entity | null> {
@@ -187,8 +232,8 @@ export class SqlKVRepository<Entity> {
/**
* JSON helpers
*/
public filterJson(field: string, key: string): string {
return this.jsonQuery.filter(field, key);
public filterJson(field: string, key: string, value: any): [string, string] {
return this.jsonQuery.filter(field, key, value);
}
protected stringify(data: any): string {
+2 -2
View File
@@ -1,9 +1,9 @@
import { IJsonQuery } from '@waha/core/storage/sql/IJsonQuery';
export class Sqlite3JsonQuery implements IJsonQuery {
filter(field: string, key: string): string {
filter(field: string, key: string, value: any): [string, string] {
const jsonPath = `$.${key}`;
return `${field}->'${jsonPath}' = ?`;
return [`${field}->'${jsonPath}' = ?`, `${value}`];
}
sortBy(field: string, sortBy: string, direction: string): string {
+9
View File
@@ -0,0 +1,9 @@
import { WAMessageAck } from '@waha/structures/enums.dto';
export function StatusToAck(status: number): WAMessageAck {
return status - 1;
}
export function AckToStatus(ack: number): number {
return ack + 1;
}
+72
View File
@@ -0,0 +1,72 @@
import { isJidGroup, WAMessageKey } from '@adiwajshing/baileys';
import { parseMessageIdSerialized } from '@waha/core/utils/ids';
import { toJID } from '@waha/core/utils/jids';
import {
GetChatMessagesFilter,
GetChatMessagesQuery,
ReadChatMessagesQuery,
} from '@waha/structures/chats.dto';
import { SendSeenRequest } from '@waha/structures/chatting.dto';
import { WAMessageAck } from '@waha/structures/enums.dto';
export function ExtractMessageKeysForRead(
request: SendSeenRequest,
): WAMessageKey[] {
const jid = toJID(request.chatId);
const defaults = {
remoteJid: jid,
participant: request.participant,
};
const ids = request.messageIds || [];
if (request.messageId) {
ids.push(request.messageId);
}
const keys: WAMessageKey[] = [];
for (const messageId of ids) {
const parsed = parseMessageIdSerialized(messageId, false);
if (parsed.fromMe) {
continue;
}
const key: WAMessageKey = {
id: parsed.id,
remoteJid: parsed.remoteJid
? toJID(parsed.remoteJid)
: defaults.remoteJid,
participant: parsed.participant
? toJID(parsed.participant)
: defaults.participant,
};
keys.push(key);
}
return keys;
}
function daysToMs(days: number) {
return days * 24 * 60 * 60 * 1000;
}
export function MessagesForRead(
chatId: string,
request: ReadChatMessagesQuery,
): {
query: GetChatMessagesQuery;
filter: GetChatMessagesFilter;
} {
const limit = request.messages || isJidGroup(chatId) ? 100 : 30;
const query: GetChatMessagesQuery = {
offset: 0,
limit: limit,
downloadMedia: false,
};
const afterMs = Date.now() - daysToMs(request.days);
const after = Math.floor(afterMs / 1000);
const filter: GetChatMessagesFilter = {
'filter.ack': WAMessageAck.DEVICE,
'filter.fromMe': false,
'filter.timestamp.gte': after,
};
return {
query: query,
filter: filter,
};
}
+16
View File
@@ -0,0 +1,16 @@
import { EventResponseType } from '@waha/structures/events.dto';
export function ParseEventResponseType(response: number) {
switch (response) {
case 0:
return EventResponseType.UNKNOWN;
case 1:
return EventResponseType.GOING;
case 2:
return EventResponseType.NOT_GOING;
case 3:
return EventResponseType.MAYBE;
default:
return EventResponseType.UNKNOWN;
}
}
+40
View File
@@ -0,0 +1,40 @@
import { WAMessageKey } from '@adiwajshing/baileys';
import { toJID } from '@waha/core/utils/jids';
/**
* Parse message id from WAHA to engine
* false_11111111111@c.us_AAA
* {id: "AAA", remoteJid: "11111111111@s.whatsapp.net", "fromMe": false}
*/
export function parseMessageIdSerialized(
messageId: string,
soft: boolean = false,
): WAMessageKey {
if (!messageId.includes('_') && soft) {
return { id: messageId };
}
const parts = messageId.split('_');
if (parts.length != 3 && parts.length != 4) {
throw new Error(
'Message id be in format false_11111111111@c.us_AAAAAAAAAAAAAAAAAAAA[_participant]',
);
}
const fromMe = parts[0] == 'true';
const chatId = parts[1];
const remoteJid = toJID(chatId);
const id = parts[2];
const participant = parts[3] ? toJID(parts[3]) : undefined;
return {
fromMe: fromMe,
id: id,
remoteJid: remoteJid,
participant: participant,
};
}
export function SerializeMessageKey(key: WAMessageKey) {
const { fromMe, id, remoteJid, participant } = key;
const participantStr = participant ? `_${participant}` : '';
return `${fromMe ? 'true' : 'false'}_${remoteJid}_${id}${participantStr}`;
}
+37
View File
@@ -0,0 +1,37 @@
import { isJidGroup, isJidMetaIa } from '@adiwajshing/baileys';
import {
isJidBroadcast,
isLidUser,
} from '@adiwajshing/baileys/lib/WABinary/jid-utils';
export function isJidNewsletter(jid: string) {
return jid.endsWith('@newsletter');
}
export function isJidCus(jid: string) {
return jid?.endsWith('@c.us');
}
/**
* Convert from 11111111111@c.us to 11111111111@s.whatsapp.net
* @param chatId
*/
export function toJID(chatId) {
if (isJidGroup(chatId)) {
return chatId;
}
if (isJidBroadcast(chatId)) {
return chatId;
}
if (isJidNewsletter(chatId)) {
return chatId;
}
if (isLidUser(chatId)) {
return chatId;
}
if (isJidMetaIa(chatId)) {
return chatId;
}
const number = chatId.split('@')[0];
return number + '@s.whatsapp.net';
}
+10
View File
@@ -0,0 +1,10 @@
import { WAMessageAckBody } from '@waha/structures/webhooks.dto';
import { distinct, interval } from 'rxjs';
export function DistinctAck(flushEvery: number = 60_000) {
// only if we haven’t seen this key since the last flush
return distinct(
(msg: WAMessageAckBody) => `${msg.id}-${msg.ack}-${msg.participant}`,
interval(flushEvery),
);
}
+3
View File
@@ -31,6 +31,9 @@ function splitAt(str: string, index) {
}
export function sortObjectByValues(obj: any) {
if (!obj) {
return obj;
}
return Object.fromEntries(
// @ts-ignore
Object.entries(obj).sort(([, v1], [, v2]) => v2 - v1),
+2 -1
View File
@@ -4,6 +4,7 @@ export const MessageIdApiParam = ApiParam({
name: 'messageId',
required: true,
type: 'string',
description: 'Message ID',
description:
'Message ID in format <code>{fromMe}_{chat}_{message_id}[_{participant}]</code>',
example: 'true_123456789@c.us_BAE6A33293978B16',
});
+28
View File
@@ -0,0 +1,28 @@
import {
registerDecorator,
ValidationArguments,
ValidationOptions,
} from 'class-validator';
export function IsFileType(validationOptions?: ValidationOptions) {
return function (object: any, propertyName: string) {
registerDecorator({
name: 'isFileType',
target: object.constructor,
propertyName: propertyName,
options: validationOptions,
validator: {
validate(value: any, _args: ValidationArguments) {
return (
value &&
typeof value === 'object' &&
(typeof value.data === 'string' || typeof value.url === 'string')
);
},
defaultMessage() {
return 'must contain either "data" or "url".';
},
},
});
};
}
+8
View File
@@ -16,9 +16,17 @@ export class SessionQuery {
*/
export class SessionBaseRequest {
@IsString()
session: string = WHATSAPP_DEFAULT_SESSION_NAME;
}
export class Result {
success: boolean = true;
}
/**
* Responses
*/
export class CountResponse {
count: number = 0;
}
+1 -1
View File
@@ -195,5 +195,5 @@ export class PreviewChannelMessages {
@IsNumber()
@Type(() => Number)
limit: number = 100;
limit: number = 10;
}
+56 -1
View File
@@ -1,5 +1,7 @@
import { BadRequestException } from '@nestjs/common';
import { ApiProperty } from '@nestjs/swagger';
import { BooleanString } from '@waha/nestjs/validation/BooleanString';
import { WAMessageAck, WAMessageAckName } from '@waha/structures/enums.dto';
import {
LimitOffsetParams,
PaginationParams,
@@ -39,6 +41,30 @@ export class GetChatMessagesFilter {
@IsBoolean()
@IsOptional()
'filter.fromMe'?: boolean;
@ApiProperty({
required: false,
description: 'Filter messages by acknowledgment status',
enum: WAMessageAckName,
})
@IsEnum(WAMessageAckName)
@IsOptional()
'filter.ack'?: WAMessageAck;
}
export function transformAck(
filter: GetChatMessagesFilter,
): GetChatMessagesFilter {
if (!filter) return filter;
if (!filter['filter.ack']) return filter;
const ackName = filter['filter.ack'];
// @ts-ignore
const ack: WAMessageAck = WAMessageAck[ackName];
if (ack == null) {
throw new BadRequestException(`Invalid ack: '${ackName}'`);
}
filter['filter.ack'] = ack;
return filter;
}
export class ChatPictureQuery {
@@ -62,7 +88,7 @@ export class GetChatMessagesQuery {
@IsNumber()
@IsOptional()
@Type(() => Number)
limit: number = 100;
limit: number = 10;
@IsNumber()
@IsOptional()
@@ -80,6 +106,35 @@ export class GetChatMessagesQuery {
downloadMedia: boolean = true;
}
export class ReadChatMessagesQuery {
@ApiProperty({
example: 30,
required: false,
description: 'How much messages to read (latest first)',
})
@Type(() => Number)
@IsNumber()
@IsOptional()
messages: number;
@ApiProperty({
required: false,
description: 'How much days to read (latest first)',
})
@Type(() => Number)
@IsNumber()
@IsOptional()
days: number = 7;
}
export class ReadChatMessagesResponse {
@ApiProperty({
required: false,
description: 'Messages IDs that have been read',
})
ids?: string[];
}
export class GetChatMessageQuery {
@ApiProperty({
example: true,
+121 -5
View File
@@ -4,8 +4,17 @@ import {
ApiProperty,
getSchemaPath,
} from '@nestjs/swagger';
import { IsFileType } from '@waha/nestjs/validation/IsFileType';
import { GetChatMessagesQuery } from '@waha/structures/chats.dto';
import { IsNotEmpty, IsNumber, IsString } from 'class-validator';
import { plainToInstance, Transform, Type } from 'class-transformer';
import {
IsBoolean,
IsNotEmpty,
IsOptional,
IsString,
IsUrl,
ValidateNested,
} from 'class-validator';
import {
SessionBaseRequest,
@@ -14,6 +23,9 @@ import {
} from './base.dto';
import {
BinaryFile,
FileContent,
FileType,
FileURL,
RemoteFile,
VideoBinaryFile,
VideoRemoteFile,
@@ -64,24 +76,30 @@ export class GetPresenceQuery extends ChatQuery {}
*/
export class ChatRequest extends SessionBaseRequest {
@ChatIdProperty()
@IsString()
chatId: string;
}
export class SendSeenRequest extends ChatRequest {
@ApiProperty({
example: 'false_11111111111@c.us_AAAAAAAAAAAAAAAAAAAA',
example: null,
deprecated: true,
required: false,
description:
"NOWEB engine only - it's important to mark ALL messages as seen",
})
messageId?: string;
@ApiProperty({
example: ['false_11111111111@c.us_AAAAAAAAAAAAAAAAAAAA'],
required: false,
})
messageIds?: string[];
@ApiProperty({
example: '11111111111@c.us',
required: false,
default: null,
description:
'NOWEB engine only - the ID of the user that sent the message (undefined for individual chats)',
'NOWEB engine only - the ID of the user that sent the message (undefined for individual chats)',
})
participant?: string;
}
@@ -146,6 +164,9 @@ export class MessageContactVcardRequest extends ChatRequest {
],
})
contacts: (VCardContact | Contact)[];
@ReplyToProperty()
reply_to?: string;
}
export class MessageTextRequest extends ChatRequest {
@@ -158,6 +179,74 @@ export class MessageTextRequest extends ChatRequest {
reply_to?: string;
linkPreview?: boolean = true;
linkPreviewHighQuality?: boolean = false;
}
@ApiExtraModels(FileURL, FileContent)
export class LinkPreviewData {
@IsNotEmpty()
@IsUrl({
protocols: ['http', 'https'],
require_protocol: true,
require_tld: false,
})
url: string = 'https://github.com/';
@IsString()
title: string = 'Your Title';
@IsString()
description: string = 'Check this out, amazing!';
@IsOptional()
@ValidateNested()
@Transform(
({ value }) => {
if (value?.url) {
return plainToInstance(FileURL, value);
}
if (value?.data) {
return plainToInstance(FileContent, value);
}
return value;
},
{ toClassOnly: true },
)
@IsFileType({ message: 'Image must contain either "data" or "url".' })
@ApiProperty({
oneOf: [
{ $ref: getSchemaPath(FileURL) },
{ $ref: getSchemaPath(FileContent) },
],
example: {
url:
process.env.WHATSAPP_SWAGGER_JPG_EXAMPLE_URL ||
'https://github.com/devlikeapro/waha/raw/core/examples/waha.jpg',
},
})
image?: FileType;
}
export class MessageLinkCustomPreviewRequest extends ChatRequest {
@IsString()
@ApiProperty({
description:
'The text to send. MUST include the URL provided in preview.url',
})
text: string = 'Check this out! https://github.com/';
@IsBoolean()
@IsOptional()
linkPreviewHighQuality?: boolean = true;
@ValidateNested()
@Type(() => LinkPreviewData)
preview: LinkPreviewData;
@ReplyToProperty()
@IsString()
@IsOptional()
reply_to?: string;
}
export class EditMessageRequest {
@@ -165,6 +254,9 @@ export class EditMessageRequest {
@ApiHideProperty()
mentions?: string[];
linkPreview?: boolean = true;
linkPreviewHighQuality?: boolean = false;
}
export class MessageReplyRequest extends MessageTextRequest {
@@ -329,3 +421,27 @@ export class MessageDestination {
from: string;
fromMe: boolean;
}
export class MessageButtonReply extends ChatRequest {
@ReplyToProperty()
@IsString()
@IsNotEmpty()
replyTo?: string;
@IsString()
@IsNotEmpty()
selectedDisplayText: string;
@IsString()
@IsNotEmpty()
selectedButtonID: string;
}
export class NewMessageIDResponse {
@ApiProperty({
description: 'Pre-generated message id',
example: 'BBBBBBBBBBBBBBBBB',
required: true,
})
id: string;
}
+14
View File
@@ -26,9 +26,14 @@ export enum WAHAEvents {
LABEL_DELETED = 'label.deleted',
LABEL_CHAT_ADDED = 'label.chat.added',
LABEL_CHAT_DELETED = 'label.chat.deleted',
EVENT_RESPONSE = 'event.response',
EVENT_RESPONSE_FAILED = 'event.response.failed',
ENGINE_EVENT = 'engine.event',
}
export type AllEventType = WAHAEvents | '*';
export const AllEvents = [...Object.values(WAHAEvents), '*'];
// All but no state.change, it's internal one
export const WAHAEventsWild = Object.values(WAHAEvents).filter(
(e) => e !== WAHAEvents.STATE_CHANGE && e !== WAHAEvents.ENGINE_EVENT,
@@ -65,4 +70,13 @@ export enum WAMessageAck {
PLAYED = 4,
}
export enum WAMessageAckName {
ERROR = 'ERROR',
PENDING = 'PENDING',
SERVER = 'SERVER',
DEVICE = 'DEVICE',
READ = 'READ',
PLAYED = 'PLAYED',
}
export const ACK_UNKNOWN = 'UNKNOWN';
+144
View File
@@ -0,0 +1,144 @@
import { ApiProperty } from '@nestjs/swagger';
import { MessageDestination } from '@waha/structures/chatting.dto';
import { WAMessageBase } from '@waha/structures/responses.dto';
import { Type } from 'class-transformer';
import {
IsBoolean,
IsNotEmpty,
IsNumber,
IsOptional,
IsString,
ValidateNested,
} from 'class-validator';
import { ChatIdProperty, ReplyToProperty } from './properties.dto';
export class EventLocation {
@ApiProperty({
description: 'Name of the location',
example: 'Luxe Nail Studio 💅',
})
@IsString()
@IsNotEmpty()
name: string;
//
// Doesn't work right now
//
// @ApiProperty({
// description: 'Latitude of the location',
// example: 38.8937255,
// })
// @IsNumber()
// @IsOptional()
// degreesLatitude?: number;
//
// @ApiProperty({
// description: 'Longitude of the location',
// example: -77.0969763,
// })
// @IsNumber()
// @IsOptional()
// degreesLongitude?: number;
}
export class EventMessage {
@ApiProperty({
description: 'Name of the event',
example: "John's Nail Appointment 💅",
})
@IsString()
@IsNotEmpty()
name: string;
@ApiProperty({
description: 'Description of the event',
example:
"It's time for your nail care session! 🌟\\n\\nYou'll be getting a *classic gel manicure* – clean, polished, and long-lasting. 💖\\n\\n📍 *Location:* Luxe Nail Studio\\nWe're on the *2nd floor of the Plaza Mall*, next to the flower shop. Look for the *pink neon sign*!\\n\\nFeel free to arrive *5–10 mins early* so we can get started on time 😊",
required: false,
})
@IsString()
@IsOptional()
description?: string;
@ApiProperty({
description: 'Start time of the event (Unix timestamp in seconds)',
example: 2063137000,
})
@IsNumber()
@IsNotEmpty()
startTime: number;
@ApiProperty({
description: 'End time of the event (Unix timestamp in seconds)',
example: null,
required: false,
})
@IsNumber()
@IsOptional()
endTime?: number;
@ApiProperty({
description: 'Location of the event',
required: false,
type: EventLocation,
})
@ValidateNested()
@Type(() => EventLocation)
@IsOptional()
location?: EventLocation;
@ApiProperty({
description: 'Whether extra guests are allowed',
example: false,
required: false,
})
@IsBoolean()
@IsOptional()
extraGuestsAllowed?: boolean;
}
export class EventMessageRequest {
@ChatIdProperty()
chatId: string;
event: EventMessage;
@ReplyToProperty()
reply_to?: string;
}
export class EventCancelRequest {
@ApiProperty({
description: 'ID of the event message to cancel',
example: 'true_12345678901@c.us_ABCDEFGHIJKLMNOPQRST',
})
@IsString()
@IsNotEmpty()
id: string;
}
export enum EventResponseType {
UNKNOWN = 'UNKNOWN',
GOING = 'GOING',
NOT_GOING = 'NOT_GOING',
MAYBE = 'MAYBE',
}
export class EventResponse {
response: EventResponseType;
timestampMs: number;
extraGuestCount: number;
}
export class EventResponsePayload extends WAMessageBase {
eventCreationKey: MessageDestination;
eventResponse?: EventResponse;
/** Returns a message in a raw format */
@ApiProperty({
description:
'Message in a raw format that we get from WhatsApp. May be changed anytime, use it with caution! It depends a lot on the underlying backend.',
})
_data?: any;
}
+31
View File
@@ -1,4 +1,5 @@
import { ApiProperty } from '@nestjs/swagger';
import { IsBase64, IsNotEmpty, IsString, IsUrl } from 'class-validator';
export class Base64File {
mimetype: string;
@@ -117,3 +118,33 @@ export class VideoRemoteFile {
})
url: string;
}
export class FileURL {
@IsString()
@IsNotEmpty()
@IsUrl({
protocols: ['http', 'https'],
require_protocol: true,
require_tld: false,
})
@ApiProperty({
example:
process.env.WHATSAPP_SWAGGER_JPG_EXAMPLE_URL ||
'https://github.com/devlikeapro/waha/raw/core/examples/waha.jpg',
})
url?: string;
}
export class FileContent {
@IsString()
@IsNotEmpty()
@IsBase64()
@ApiProperty({
description: 'Base64-encoded data of the file',
example:
'/9j/4AAQSkZJRgABAQAAAQABAAD//gAfQ29tcHJlc3NlZCBieSBqcGVnLXJlY29tcHJlc3P/2wCEAAQEBAQEBAQEBAQGBgUGBggHBwcHCAwJCQkJCQwTDA4MDA4MExEUEA8QFBEeFxUVFx4iHRsdIiolJSo0MjRERFwBBAQEBAQEBAQEBAYGBQYGCAcHBwcIDAkJCQkJDBMMDgwMDgwTERQQDxAUER4XFRUXHiIdGx0iKiUlKjQyNEREXP/CABEIAgACAAMBIgACEQEDEQH/xAAcAAEAAgIDAQAAAAAAAAAAAAAABgcEBQEDCAL/2gAIAQEAAAAA9/AAABxix7SajXYWN1uzJzdjud1v8vkAAAAAADjRRGL6kAAbaUzDdcgAAAAAOmDQnVAAADazec9oAAAAA6K9gvQAAAA753YXcAAAAD5hNb4oAAAAMuyppyAAAAa2p4+AAAAAJBbmcAAABD6s6AAAAAAd9qy4AAAKzgIAAAAAAn9lgAAOKjiQAAAAAAJdbfIAAcU7GAAAAAAAEnuLkAAVDEwAAAAAABLbdAAKxgYAAAAAc9HR9ZXICfWYABDKoAAAAAOqLRvR4x9bSQSrbgtuXgBrKO6QAAAAPmFQfoAG/sLajuvPYgCk9CAAAABr6u1QAH1PpsN9dgBBquAAAAA1NUYwAAmNii05uB10HigAAAAwagxgPn7+XIE6nhlX13ArevQAAAAcVJpQTi2ZvteuMV1UWvBbG+LDscOig+gAAAAEQrgMr0TZde1zosiW2rm+dK6DZXHy7787xAK0AAAABxTGCO71PtPNUTD7u+6/OFYhZ0oLNnpxRWrAACPxDKnOYAR6qgvyy/JuqAXPeHkbQiQWsbS9eUfpUAANFUxsrj5AV3DRuPYfmiu74sryJ0A9WaDzkPq8fsuvfqygQAAV1DhcO1AVHpRclvePfr2z9+c6tBZnovxb1C3N0T6zOKL1QAAQqvj6uvJA5pLFHpLo86ceytn5QiQNn7N8kxUWdKDa3phUGAAD4q2Pc2VKwHNH9A9RRehVuegvKsLB2e3PLMFFkS4X5H6jAAA1FQba4ADmk8Qekurzkempr5nhFpa6vm49keSYqLMlQt3SV6AABqaf21wACo9KLjuLx0d3o+yYVM/ugqdtL0H4u6xbO9FiaeJgAAamn9rcIAruGja+xfN1bCyroluo8xxT1nEvPAXh3CWa3SgAAamn9tcAAjlWDj19WNIBx39XF4XH5F0w3ltBu8LBAAa7H4NZW2bZ4+8/IHFLYZsfZvlWNbGPh23pcfmavAsmWhnY/SAHEKhOKAA3tgbkhtdlnekYvJ+YLXWk75bZ315xggZ1zfQd3X8gDiro2AAHNnyZxUGpehpLBYItScbfHi9cVPigtKSA+nyAIPAQAAO25cxrqh6AOHH0AmViAfX31AHFJY4AACdTw0dVdQAAlFm8gd2RggGqp4AAA3tsjU1dgAAJvPwDOzNKAaGpwY/fyDr47Q2dxh1QKG/IA21h70AbvYxMA0FUD4ieJ2y7vGnjrf7sbK5AMSIxrTjjLkMrkAAJZt68A+NbF4MNVGje70QnrczkZlq9uwAfGB0fWdlcAALD3NRgx6dwwYEUJFuBD8V3TQCaWEAAAAW5IKDBHqqAaTW5u/5GLoG9zANjcoAAABfmbReqCN1aAAAAZ90cAAAAbW9OazgIRyrAAAADPujgAAACe2a0VJBo6lAAAANpcQAAAC6pA4orVjiuND0YwMHVANrnA2HbPZIAAABtL2ECrIHMeqkGk0ADfbwNjc3AAAABZc/HVQPQCP1SDFh4CXZgbG5QAAADvvvICuK8BoanAjepBtZKDZ3GAAAAWDZIOqhMUMGlwOI9pxuJDyCRWoAAAAyr67gITVYKf1IDpwGd3gLGmAAAAC0ZyAUpoA0FUAAAbK4fsAAADfXYAMCiukIlXHyAANja2eAAAB3XjswAh9Sg10M0eNxxyDjnnYSSY9gAAAC15mABWcBAcgA4AAAAE8s4AAqGJgAAAAAACV2+AAOKdjAAAAAAACT3FyAAHFRxIAAAAAAEst3kAABWcBAAAAAAE8s3kAAAQ6rOkAAAAAHdaMzAAAA19Sx8AAAAASC19mAAAAcQytcMAAAAGZY825AAAAB8V5A8cAAABkTuwO8AAAAAdUIg2qAAAG1m047wAAAAAHGiiMX1IAA20nl2+5AAAAAAA4xY/ptPrMPo6nb35mz2+5kOWAAAD//EABsBAQACAwEBAAAAAAAAAAAAAAAEBQIDBgEH/9oACAECEAAAAAA11NXBi6m2VOtLbYAACJzdLgAM7vo5YADDmeexAAZdD02YAROMggAAndnLAIHE6AAAG/tp4EThNIAAePTd3csMODhAAGuBFwbZc7Kb3mY5XnQACLV4g2223o+pInAYgANFP4trjCigtl1s7+W5CiAAKXT73EShz6Wv5dPsb7rtXzvADGqztBopnUR8ue8drztZne5/RKrjQEenX2RCrHf8D9b+YwUvqeLXmzsayhAKjdYiBXO+4Hs6+lxw7bhV1uvoNeAVm6aIlU7rjdHX9LT8/J5lfZT42kHmKt3S/cvcKPzrqWv1+zuz4DDfct2rwK6D4DKymVsH6Dh740cfoW0p68ESqAPbrZT6fHrwnWR7t0ishPfD14s5uNbDDKwnDdJgGNXFys9EFZe1ixnetUXX7ulZBPn0LCj8e2OiInoBtuwAL6z45rowAMr4ADsrX53hjS4eMto0+Pd9wADP6Jt5CiKuGzsxW61lOAAvuuROAxVkImSkaEWkwAGXfyzledRqgZsD272AA6PqRhwcJDgYA2WUgAE3vMwicJpAAAG7u5YEDiNIAADd288AicXCAABO7OWAGHM89iAAy6HpswAETnKTAAZ3fRywAAa6mrgxdbZKnWlrtAH/xAAbAQEAAgMBAQAAAAAAAAAAAAAABAUCAwYBB//aAAgBAxAAAAAAMIEGJowZ75c6fmAACPTVmIAys7mQAAxpKfwAB7cXeQAaOaigAAldLvAIvMagAAG3p5QEfltQAAePTZ1UgMeVigAGEXRizkSvZXVZCipgADRB8BnYZ3N6R+S8AAaq/wAWFhjVxWdjn1shz1UAAVuv3p49VndRKJKmWvQ4cdiB5AynDVXLzV7UeOmpoWVpl2NfzgDTXrTIjQnWcp9B4eKkXvNLPPo4NUAV+yYIsN1nJ9JErPMem5ZZbLWLEAIWySI8F1POauhuq6p3Ui0yl6NYPPEPOS99xrPOhrImHsnpeSx22Lbr8CHG8B7Mkw4vXYjVzupP3vXgjwQBZ5V+o98EqYe7NQhRnvgE2T5Cjh7LlDbuiHkDTlN1RU32ClyvWGjBs35BLl1LGs8ezNUdLRDOzAAtpvOMKwADK0AA6Sfx2ONdge7Br8e7bAAGXY589VEGOzmiFgmyQALXoUfkvEKMSd7RGJ0gAHvWyCipmmvGTE9sswAXN6MeUjI8Tzwe5TNwAJXVZBH5bUAAANvUyAIvL6wAAG3p5QBH5uKAACV0kgAMaSoxAAZW93kAA0U1XiAMrO5kAAAwgQYujW2b5U6fmAP/xABREAACAQIDAgcJCgwFAwUBAAABAgMEBQAGESExEhMgQVFhcQciMDJAQnKBkRAUFVBSU6GxwdEjMzVDVWJzgpKTorIWJDRU0mN0g0RFZMLhw//aAAgBAQABPwDyiqu1vpNlRVwx6czOAT6sVWdLPESImlm9BNB/Vpioz65BWmt4A6Xf7AMS5zvbghHiiB+Qn/InEuY73N49xk/dAT+0DD3O5P49fUntlbDTzMdWmc9rHBZjvJOAzDcSMLPMh1WZwepiMLc7kniXCpHZK2Isx3uHxLhJ+8A/9wOIs53pAA7xSgfLT/iRilz/ACJsqLeDrvZH+wjFJnSyysOOMsPpp/x1xTXW31ZHvesikPyVcE+zGvxYSBvOK3M1ooeEr1YkceZF35+jYMVuep31WgpFQfLlOp9gxV3y7VuvH10nBPmqeAvsXTyGkvt2oiOIrpQBuVjwx7G1xRZ7nTRa6kVxzvFsPsOKDNForVVI6sRSsfFl7w4BB3H4nq66koo+HUTpFGOdiBri454iTWO2wcYfnJNi+obziuvdzuOoqqtyh8xe9T2Dyehvd0tpHvWrcIPMbvk9h3YtueomKpcoDG3zke1fZvGKOto6xONpp0kj5ypB9XxHW1tLQxGaqnWNF5yfoGLpnd24UVri4I+ekG31LioqqirkMtTM8rnnc6+V09VUUkgmppnicc6nTFrzu6hYbrFwh89GNvay4oq2krIRUUsySod2h5+vy9mVQWYgADUk4vOc4KctBbAJpdxkP4tezpxV11XXymarnaV+s7B2Dm8vo66roJRNSTtG/VuPaOfFlzvFPwYLmBFLzS+Y3b8nCsGAZSCDtBHll1vNFaouMqZdCR3ka7XbsGLxmOuuxZC3FU3NEh3+kef4js+Y6+0MqK3G0+u2Jzu9E82LTeaC7RcZTy7VGrxnYwPWPKr/AJrhoOHS0RWWq3E70j7ek4qKmermeeplaSVjtZviWnqZ6SZJ6aVo5F3MuMv5shuHFUldwY6ncp3I5+/q8nZlRWZiAoGpJxmLNjTF6K1uVj3PMN7dS9XX8U5cza0BSiurloT3qzHevU334RldVZWBUjUEeSSyx08TyzOFRQWZidAAMZizLLc2alpGZKMHsMnWerq+K8uZnltTrS1TF6Jj2mP7x1YhminjSWF1ZHAKlTqCPIp5o4InllcJGgJLHYABjMOYpbtK0EJKUanYu4uRzt8W5dzFNaJVhmYtRsdq86E86/aMQzRzxJNE4aNgGVgdQQfIHdY0Z3YKqjUk7ABjMuYnukrUtMxFGjfzCOc9XR8X5ZzG9pmFNUuTRufXGTzjqwjq6q6MCrDUEbQR4fNmYjUM9son/BKdJnHnkeaOofEmh36Yeppo/wAZUwp6UijBudtG+4U381cLX0D+LXU57JVwrK+1HVvRIPgco5ka Line truncated
})
data?: string;
}
export type FileType = FileURL | FileContent;
+17
View File
@@ -74,6 +74,23 @@ export class JoinGroupResponse {
id: string;
}
export enum GroupField {
NONE = '',
PARTICIPANTS = 'participants',
}
export class GroupsListFields {
@IsOptional()
@ApiProperty({
description: 'Exclude fields',
enum: GroupField,
isArray: true,
required: false,
})
@IsEnum(GroupField, { each: true })
exclude: string[];
}
export enum GroupSortField {
ID = 'id',
SUBJECT = 'subject',
+21
View File
@@ -0,0 +1,21 @@
import { ApiProperty } from '@nestjs/swagger';
import { LimitOffsetParams } from '@waha/structures/pagination.dto';
export class LidToPhoneNumber {
@ApiProperty({
description: 'Linked ID for the user',
example: '1111111@lid',
})
lid?: string;
@ApiProperty({
description: 'Phone number (chat id) for the user',
example: '3333333@c.us',
})
pn?: string;
}
export class LidsListQueryParams extends LimitOffsetParams {
limit?: number = 100;
offset?: number = 0;
}
+5
View File
@@ -15,4 +15,9 @@ export class ReplyToMessage {
example: 'Hello!',
})
body?: string;
@ApiProperty({
description: "Raw data from reply's message",
})
_data?: any;
}
+13 -1
View File
@@ -11,7 +11,12 @@ export class WALocation {
longitude: string;
}
class WAMessageBase {
export enum MessageSource {
API = 'api',
APP = 'app',
}
export class WAMessageBase {
@MessageIdProperty()
id: string;
@@ -33,6 +38,13 @@ class WAMessageBase {
})
fromMe: boolean;
@ApiProperty({
description:
'The device that sent the message - either API or APP. Available in events (webhooks/websockets) only and only "fromMe: true" messages.',
example: MessageSource.API,
})
source: MessageSource;
@ChatIdProperty({
description: `
* ID for who this message is for.
+22 -22
View File
@@ -17,21 +17,30 @@ const ContactsProperty = ApiProperty({
required: false,
});
export interface StatusRequest {
contacts?: string[];
}
export class TextStatus {
text: string = 'Have a look! https://github.com/';
backgroundColor: string = '#38b42f';
font: number = 0;
export class StatusRequest {
@ApiProperty({
description: 'Pre-generated status message id',
example: 'BBBBBBBBBBBBBBBBB',
default: null,
required: false,
})
id?: string;
@ContactsProperty
contacts?: string[];
}
export class TextStatus extends StatusRequest {
text: string = 'Have a look! https://github.com/';
backgroundColor: string = '#38b42f';
font: number = 0;
linkPreview?: boolean = true;
linkPreviewHighQuality?: boolean = false;
}
@ApiExtraModels(RemoteFile, BinaryFile)
export class ImageStatus {
export class ImageStatus extends StatusRequest {
@ApiProperty({
oneOf: [
{ $ref: getSchemaPath(RemoteFile) },
@@ -41,13 +50,10 @@ export class ImageStatus {
file: RemoteFile | BinaryFile;
caption?: string;
@ContactsProperty
contacts?: string[];
}
@ApiExtraModels(VoiceRemoteFile, VoiceBinaryFile)
export class VoiceStatus {
export class VoiceStatus extends StatusRequest {
@ApiProperty({
oneOf: [
{ $ref: getSchemaPath(VoiceRemoteFile) },
@@ -57,13 +63,10 @@ export class VoiceStatus {
file: VoiceRemoteFile | VoiceBinaryFile;
backgroundColor: string = '#38b42f';
@ContactsProperty
contacts?: string[];
}
@ApiExtraModels(VideoRemoteFile, VideoBinaryFile)
export class VideoStatus {
export class VideoStatus extends StatusRequest {
@ApiProperty({
oneOf: [
{ $ref: getSchemaPath(VideoRemoteFile) },
@@ -73,14 +76,11 @@ export class VideoStatus {
file: VideoRemoteFile | VideoBinaryFile;
caption?: string;
@ContactsProperty
contacts?: string[];
}
export class DeleteStatusRequest {
export class DeleteStatusRequest extends StatusRequest {
@ApiProperty({
description: 'status message id',
description: 'Status message id to delete',
example: 'AAAAAAAAAAAAAAAAA',
})
id: string;
+14 -4
View File
@@ -1,15 +1,21 @@
import { ApiProperty } from '@nestjs/swagger';
import { WAHAEvents } from '@waha/structures/enums.dto';
import {
AllEvents,
AllEventType,
WAHAEvents,
} from '@waha/structures/enums.dto';
import { Type } from 'class-transformer';
import {
IsArray,
IsEnum,
IsIn,
IsNumber,
IsOptional,
IsString,
IsUrl,
ValidateNested,
} from 'class-validator';
import { each } from 'lodash';
export enum RetryPolicy {
LINEAR = 'linear',
@@ -70,16 +76,20 @@ export class WebhookConfig {
description:
'You can use https://docs.webhook.site/ to test webhooks and see the payload',
})
@IsUrl({ require_protocol: true, require_tld: false })
@IsUrl({
protocols: ['http', 'https'],
require_protocol: true,
require_tld: false,
})
url: string;
@ApiProperty({
example: ['message', 'session.status'],
required: true,
})
@IsEnum(WAHAEvents, { each: true })
@IsIn(AllEvents, { each: true })
@IsArray()
events: WAHAEvents[];
events: AllEventType[];
@ApiProperty({
example: null,
+30 -1
View File
@@ -1,5 +1,6 @@
import { ApiProperty } from '@nestjs/swagger';
import { CallData } from '@waha/structures/calls.dto';
import { EventResponsePayload } from '@waha/structures/events.dto';
import { Label, LabelChatAssociation } from '@waha/structures/labels.dto';
import { ChatArchiveEvent } from './chats.dto';
@@ -91,6 +92,7 @@ export class PollVotePayload {
export class WAMessageRevokedBody {
after?: WAMessage;
before?: WAMessage;
_data?: any;
}
export class WASessionStatusBody {
@@ -104,10 +106,18 @@ export class WASessionStatusBody {
export class WAHAWebhook {
@ApiProperty({
example: 'evt_01jcn4pjwwg47bwy2gsey6q5sx',
example: 'evt_01aaaaaaaaaaaaaaaaaaaaaaaa',
description:
'Unique identifier for the event - lower case ULID format. https://github.com/ulid/spec',
})
id: string;
@ApiProperty({
example: 1634567890123,
description: 'Unix timestamp (ms) for when the event was created.',
})
timestamp: number;
@ApiProperty({
example: 'default',
})
@@ -335,6 +345,25 @@ export class EnginePayload {
data: any;
}
export class WAHAWebhookEventResponse extends WAHAWebhook {
@ApiProperty({
description: 'The event is triggered when the event response is received.',
})
event = WAHAEvents.EVENT_RESPONSE;
payload: EventResponsePayload;
}
export class WAHAWebhookEventResponseFailed extends WAHAWebhook {
@ApiProperty({
description:
'The event is triggered when the event response is failed to decrypt.',
})
event = WAHAEvents.EVENT_RESPONSE_FAILED;
payload: EventResponsePayload;
}
export class WAHAWebhookEngineEvent extends WAHAWebhook {
@ApiProperty({
description: 'Internal engine event.',
+4
View File
@@ -10,6 +10,8 @@ import {
WAHAWebhookCallRejected,
WAHAWebhookChatArchive,
WAHAWebhookEngineEvent,
WAHAWebhookEventResponse,
WAHAWebhookEventResponseFailed,
WAHAWebhookGroupJoin,
WAHAWebhookGroupLeave,
WAHAWebhookLabelChatAdded,
@@ -50,6 +52,8 @@ const WAHA_WEBHOOKS = [
WAHAWebhookLabelDeleted,
WAHAWebhookLabelChatAdded,
WAHAWebhookLabelChatDeleted,
WAHAWebhookEventResponse,
WAHAWebhookEventResponseFailed,
WAHAWebhookEngineEvent,
WAHAWebhookGroupJoin,
WAHAWebhookGroupLeave,
+12 -2
View File
@@ -24,8 +24,8 @@ export class PaginatorInMemory extends Paginator {
}
return lodash.orderBy(
data,
this.pagination.sortBy,
this.pagination.sortOrder || 'asc',
[this.NullLast(this.pagination.sortBy)],
[this.pagination.sortOrder || 'asc'],
);
}
@@ -37,6 +37,16 @@ export class PaginatorInMemory extends Paginator {
const limit = this.pagination.limit || Infinity;
return data.slice(offset, offset + limit);
}
NullLast(field) {
return (item) => {
const value = item?.[field];
if (value == null) {
return -Infinity;
}
return value;
};
}
}
export class KnexPaginator extends Paginator {
+25
View File
@@ -0,0 +1,25 @@
import { WAHASessionStatus } from '../structures/enums.dto';
const STUCK_IN_STARTING_THRESHOLD = 60;
/**
* Keeps track of session statuses (with timestamps) for the last `trackPeriodMs` milliseconds.
*/
export class StatusTracker {
private numberOfStarting: number = 0;
public track(status: WAHASessionStatus): void {
if (status == WAHASessionStatus.STARTING) {
this.numberOfStarting += 1;
} else {
this.numberOfStarting = 0;
}
}
/**
* Checks if the session has been continuously 'STARTING'
*/
public isStuckInStarting(): boolean {
return this.numberOfStarting >= STUCK_IN_STARTING_THRESHOLD;
}
}
+3 -2
View File
@@ -33,8 +33,9 @@ export function getEngineName(): string {
}
export const VERSION: WAHAEnvironment = {
version: '2025.2.4',
version: '2025.5.4',
engine: getEngineName(),
tier: getWAHAVersion(),
browser: getBrowserExecutablePath(),
browser:
getEngineName() === WAHAEngine.WEBJS ? getBrowserExecutablePath() : null,
};
+2 -2
View File
@@ -2,11 +2,11 @@
"waha": {
"gows": {
"repo": "devlikeapro/gows",
"ref": "8b31d6b88166e0962ca47a3dfbd256a42f09dbc1"
"ref": "v1.0.6"
},
"dashboard": {
"repo": "devlikeapro/dashboard",
"ref": "d6cd941f8d7621f401305e03ad2c1db8dc0a64f0"
"ref": "3fb6694d849ca0b567679dd59311a50b9d727f2f"
}
}
}
+306 -358
View File
File diff suppressed because it is too large. Load diff