Compare commits

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

fix #698
2024-12-24 13:33:39 +07:00
devlikepro 900e3c23bc [core] 2024.12.5
Release / WEBJS - chrome - amd64 - chrome (push) Waiting to run
Release / WEBJS - chromium - amd64 - latest (push) Waiting to run
Release / WEBJS - chromium - linux/arm64 - arm (push) Waiting to run
Release / NOWEB - none - amd64 - noweb (push) Waiting to run
Release / NOWEB - none - linux/arm64 - noweb-arm (push) Waiting to run
2024-12-16 11:56:50 +07:00
devlikepro aaeffeb983 [core] Update libsignal 2024-12-16 11:56:39 +07:00
devlikepro 15600324be [core] Update NOWEB 2024-12-16 11:56:39 +07:00
devlikepro da4913edb5 [core] Use libsignal fork
Add logging
2024-12-16 11:56:39 +07:00
devlikepro e8cc1e4906 [core] 2024.12.4
Release / WEBJS - chrome - amd64 - chrome (push) Waiting to run
Release / WEBJS - chromium - amd64 - latest (push) Waiting to run
Release / WEBJS - chromium - linux/arm64 - arm (push) Waiting to run
Release / NOWEB - none - amd64 - noweb (push) Waiting to run
Release / NOWEB - none - linux/arm64 - noweb-arm (push) Waiting to run
2024-12-14 19:15:09 +07:00
devlikepro 801fc3f9fd [core] Join group via code
fix #688
2024-12-14 19:14:56 +07:00
devlikepro 703b0b4cde [core] Fix groups sort by
Release / WEBJS - chrome - amd64 - chrome (push) Waiting to run
Release / WEBJS - chromium - amd64 - latest (push) Waiting to run
Release / WEBJS - chromium - linux/arm64 - arm (push) Waiting to run
Release / NOWEB - none - amd64 - noweb (push) Waiting to run
Release / NOWEB - none - linux/arm64 - noweb-arm (push) Waiting to run
2024-12-09 11:30:39 +07:00
devlikepro d55895389a [core] 2024.12.3 2024-12-09 10:37:35 +07:00
devlikepro 56009e482b [core] Add dedicated POST /api/{session}/groups/refresh endpoint 2024-12-09 10:19:38 +07:00
devlikepro 767d1bb3e3 [core] Fix 'rate-overlimit' error for groups
fix #462
2024-12-08 18:23:39 +07:00
devlikepro 05132ac0f5 [core] Update NOWEB 1018710061 2024-12-08 18:23:39 +07:00
devlikepro fb372f8c28 [core] Set WHATSAPP_DEFAULT_ENGINE at build time
fix #414
2024-12-08 13:52:43 +07:00
devlikepro a2dc863b7a [core] 12.2
Release / amd64 - chrome - chrome (push) Waiting to run
Release / amd64 - chromium - latest (push) Waiting to run
Release / linux/arm64 - chromium - arm (push) Waiting to run
Release / amd64 - none - noweb (push) Waiting to run
Release / linux/arm64 - none - noweb-arm (push) Waiting to run
2024-12-07 00:15:49 +07:00
devlikepro 15671c6781 [core] NOWEB - potential fix for #671 2024-12-07 00:15:34 +07:00
devlikepro 6296115adb [core] Cache keys between restart 2024-12-07 00:15:34 +07:00
devlikepro 0a099a91d0 [core] 2024.12.1
Release / amd64 - chrome - chrome (push) Waiting to run
Release / amd64 - chromium - latest (push) Waiting to run
Release / linux/arm64 - chromium - arm (push) Waiting to run
Release / amd64 - none - noweb (push) Waiting to run
Release / linux/arm64 - none - noweb-arm (push) Waiting to run
2024-12-06 10:09:04 +07:00
devlikepro 5f060fe022 [core] Fix channel messages
fix #683
2024-12-06 10:09:03 +07:00
devlikepro a4bbe1cf7b [core] Fix GET channel by id
fix #682
2024-12-06 10:09:03 +07:00
devlikepro dce74ae7d5 [core] 2024.11.11
Release / amd64 - chrome - chrome (push) Waiting to run
Release / amd64 - chromium - latest (push) Waiting to run
Release / linux/arm64 - chromium - arm (push) Waiting to run
Release / amd64 - none - noweb (push) Waiting to run
Release / linux/arm64 - none - noweb-arm (push) Waiting to run
2024-12-01 16:23:50 +07:00
devlikepro 871c509493 [core] Up WEBJS
fix #664
2024-12-01 16:23:39 +07:00
devlikepro 9333470e3b [core] Fix 403 on media
fix #667
2024-12-01 15:56:40 +07:00
devlikepro 0c108f5232 [core] 2024.11.10
Release / amd64 - chrome - chrome (push) Waiting to run
Release / amd64 - chromium - latest (push) Waiting to run
Release / linux/arm64 - chromium - arm (push) Waiting to run
Release / amd64 - none - noweb (push) Waiting to run
Release / linux/arm64 - none - noweb-arm (push) Waiting to run
2024-12-01 15:37:21 +07:00
devlikepro 39d298c867 [core] WEBJS fix groups
fix #674 #670
2024-12-01 15:35:00 +07:00
devlikepro 376e804a28 [core] Up NOWEB 2024-12-01 15:16:47 +07:00
devlikepro 79e5c0589d [core] Up NOWEB
Release / amd64 - chrome - chrome (push) Waiting to run
Release / amd64 - chromium - latest (push) Waiting to run
Release / linux/arm64 - chromium - arm (push) Waiting to run
Release / amd64 - none - noweb (push) Waiting to run
Release / linux/arm64 - none - noweb-arm (push) Waiting to run
2024-11-29 14:51:16 +07:00
devlikepro f6679b7368 [core] 2024.11.9 2024-11-29 09:38:06 +07:00
devlikepro a3c0b86b67 [core] Add explicit placeholder cache 2024-11-29 09:37:49 +07:00
devlikepro c8f48dd62b [core] 11.8
Release / amd64 - chrome - chrome (push) Waiting to run
Release / amd64 - chromium - latest (push) Waiting to run
Release / linux/arm64 - chromium - arm (push) Waiting to run
Release / amd64 - none - noweb (push) Waiting to run
Release / linux/arm64 - none - noweb-arm (push) Waiting to run
2024-11-25 12:57:23 +07:00
devlikepro aed157697a [core] WEBJS - Fix QR code
fix #659
2024-11-25 12:56:51 +07:00
devlikepro 913b8606d9 [core] 2024.11.7
Release / amd64 - chrome - chrome (push) Waiting to run
Release / amd64 - chromium - latest (push) Waiting to run
Release / linux/arm64 - chromium - arm (push) Waiting to run
Release / amd64 - none - noweb (push) Waiting to run
Release / linux/arm64 - none - noweb-arm (push) Waiting to run
2024-11-24 14:06:33 +07:00
devlikepro 7cadbad36e [core] Add WebJSEngineConfigService.ts
fix #654 fix #653
2024-11-24 14:06:28 +07:00
158 changed files with 34960 additions and 1672 deletions

No files matched your search

+1
View File
@@ -1,4 +1,5 @@
Dockerfile
src/core/engines/gows/proto
.*sessions
sessions
files
+37 -22
View File
@@ -6,10 +6,12 @@
# If you know the right URL, you can use it here.
# Use any port in WHATSAPP_API_PORT and domain in WHATSAPP_API_HOSTNAME
# https://waha.devlike.pro/docs/how-to/config/#common
WAHA_BASE_URL=http://localhost:3000
#WHATSAPP_API_SCHEMA=http
#WHATSAPP_API_PORT=3000
#WHATSAPP_API_HOSTNAME=localhost
#WAHA_BASE_URL=http://localhost:3000
#
# Set your timezone to see the right time on screenshots
# Find your timezone name in the list
@@ -21,9 +23,9 @@
#
# 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
@@ -33,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
@@ -90,6 +92,17 @@ WHATSAPP_SWAGGER_PASSWORD=admin
#WHATSAPP_SWAGGER_TITLE=WAHA API
#
#
# PostgresSQL Configuration
# https://waha.devlike.pro/docs/how-to/storages/#sessions---postgresql
#WHATSAPP_SESSIONS_POSTGRESQL_URL=postgres://postgres:postgres@postgres:5432/postgres?sslmode=disable
#
# Media - PostgresSQL Storage
# https://waha.devlike.pro/docs/how-to/storages/#media---postgresql
#WAHA_MEDIA_STORAGE=POSTGRESQL
#WAHA_MEDIA_POSTGRESQL_URL=postgres://postgres:postgres@postgres:5432/postgres?sslmode=disable
#
#
# Global Proxy Settings
# https://waha.devlike.pro/docs/how-to/config/#global-proxy-configuration
@@ -101,22 +114,6 @@ WHATSAPP_SWAGGER_PASSWORD=admin
#WHATSAPP_PROXY_SERVER_PASSWORD=pass
#
#
# HTTPS Configuration
# https://waha.devlike.pro/docs/how-to/config/#https
#
# Consider using certbot for HTTPS
# https://waha.devlike.pro/blog/setting-up-https-for-waha/#lets-encrypt-certbot--waha
#
#WAHA_HTTPS_ENABLED=true
#WAHA_HTTPS_PATH_KEY=/etc/letsencrypt/live/waha.example.pro/privkey.pem
#WAHA_HTTPS_PATH_CERT=/etc/letsencrypt/live/waha.example.pro/cert.pem \
#WAHA_HTTPS_PATH_CA=/etc/letsencrypt/live/waha.example.pro/chain.pem
#WHATSAPP_API_SCHEMA=https
#WHATSAPP_API_PORT=3000
#WHATSAPP_API_HOSTNAME=waha.example.pro
#
#
# Media Configuration
#
@@ -169,8 +166,26 @@ WHATSAPP_FILES_FOLDER=/app/.media
# volumes:
# - './.sessions:/app/sessions'
#
# MongoDB Configuration
# https://waha.devlike.pro/docs/how-to/storages/#sessions---mongodb
#WHATSAPP_SESSIONS_MONGO_URL=mongodb://mongouser:mongopassword@mongodb:27017
#
#
# HTTPS Configuration
# https://waha.devlike.pro/docs/how-to/config/#https
#
# Consider using certbot for HTTPS
# https://waha.devlike.pro/blog/setting-up-https-for-waha/#lets-encrypt-certbot--waha
#
#WAHA_HTTPS_ENABLED=true
#WAHA_HTTPS_PATH_KEY=/etc/letsencrypt/live/waha.example.pro/privkey.pem
#WAHA_HTTPS_PATH_CERT=/etc/letsencrypt/live/waha.example.pro/cert.pem \
#WAHA_HTTPS_PATH_CA=/etc/letsencrypt/live/waha.example.pro/chain.pem
#WHATSAPP_API_SCHEMA=https
#WHATSAPP_API_PORT=3000
#WHATSAPP_API_HOSTNAME=waha.example.pro
#
+2
View File
@@ -0,0 +1,2 @@
src/core/engines/gows/grpc/*
src/core/engines/gows/proto/*
+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
+28 -3
View File
@@ -8,7 +8,9 @@ on:
jobs:
docker-build:
runs-on: ${{ matrix.runner }}
name: ${{ matrix.platform }} - ${{ matrix.browser }} - ${{ matrix.tag }}
name:
${{ matrix.engine }} - ${{ matrix.browser }} - ${{ matrix.platform }} -
${{ matrix.tag }}
strategy:
matrix:
include:
@@ -17,6 +19,7 @@ jobs:
tag: 'latest'
platform: 'amd64'
browser: 'chromium'
engine: 'WEBJS'
goss: 'goss-linux-amd64'
# Chromium - ARM
@@ -24,6 +27,7 @@ jobs:
tag: 'arm'
platform: 'linux/arm64'
browser: 'chromium'
engine: 'WEBJS'
goss: 'goss-linux-arm'
# Chrome - x86
@@ -31,6 +35,7 @@ jobs:
tag: 'chrome'
platform: 'amd64'
browser: 'chrome'
engine: 'WEBJS'
goss: 'goss-linux-amd64'
# Chrome - ARM (Chrome is not available for ARM)
@@ -40,18 +45,36 @@ jobs:
# browser: "chrome"
# goss: 'goss-linux-arm'
# No browser - x86
# NOWEB - x86
- runner: 'buildjet-4vcpu-ubuntu-2204'
tag: 'noweb'
platform: 'amd64'
browser: 'none'
engine: 'NOWEB'
goss: 'goss-linux-amd64'
# No browser - ARM
# NOWEB - ARM
- runner: 'buildjet-4vcpu-ubuntu-2204-arm'
tag: 'noweb-arm'
platform: 'linux/arm64'
browser: 'none'
engine: 'NOWEB'
goss: 'goss-linux-arm'
# GOWS - x86
- runner: 'buildjet-4vcpu-ubuntu-2204'
tag: 'gows'
platform: 'amd64'
browser: 'none'
engine: 'GOWS'
goss: 'goss-linux-amd64'
# No browser - ARM
- runner: 'buildjet-4vcpu-ubuntu-2204-arm'
tag: 'gows-arm'
platform: 'linux/arm64'
browser: 'none'
engine: 'GOWS'
goss: 'goss-linux-arm'
steps:
@@ -85,6 +108,7 @@ jobs:
platforms: ${{ matrix.platform }}
build-args: |
USE_BROWSER=${{ matrix.browser }}
WHATSAPP_DEFAULT_ENGINE=${{ matrix.engine }}
push: false
load: true
tags: ${{ steps.meta.outputs.tags }}
@@ -116,6 +140,7 @@ jobs:
platforms: ${{ matrix.platform }}
build-args: |
USE_BROWSER=${{ matrix.browser }}
WHATSAPP_DEFAULT_ENGINE=${{ matrix.engine }}
push: ${{ github.ref_type == 'tag' }}
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
+1
View File
@@ -1,3 +1,4 @@
src/core/engines/gows/proto
.env
*.heapsnapshot
.yarn
+4
View File
@@ -2,6 +2,8 @@ exclude: |
(?x)^(
^src/dashboard/.*|
^src/core/engines/webjs/.*html|
^src/core/engines/gows/grpc/.*|
^src/core/engines/gows/proto/.*|
^src/plus/engines/webjs/.*html|
^.yarn/.*
)$
@@ -73,4 +75,6 @@ repos:
.pre-commit-config.yaml|
^src/core/engines/webjs/session.webjs.core.ts|
^src/core/engines/webjs/_lodash.js|
^tests/perf/.*|
^scripts/.*|
)$
+63 -9
View File
@@ -6,7 +6,7 @@ FROM node:${NODE_VERSION} AS build
ENV PUPPETEER_SKIP_DOWNLOAD=True
# npm packages
WORKDIR /src
WORKDIR /git
COPY package.json .
COPY yarn.lock .
ENV YARN_CHECKSUM_BEHAVIOR=update
@@ -15,8 +15,8 @@ RUN yarn set version 3.6.3
RUN yarn install
# App
WORKDIR /src
ADD . /src
WORKDIR /git
ADD . /git
RUN yarn install
RUN yarn build && find ./dist -name "*.d.ts" -delete
@@ -25,16 +25,51 @@ RUN yarn build && find ./dist -name "*.d.ts" -delete
#
FROM node:${NODE_VERSION} AS dashboard
# Download WAHA Dashboard
ENV WAHA_DASHBOARD_SHA 45db5b46a944f4320136f2409d8b970483c8bd9d
# jq to parse json
RUN apt-get update && apt-get install -y jq && rm -rf /var/lib/apt/lists/*
COPY waha.config.json /tmp/waha.config.json
RUN \
wget https://github.com/devlikeapro/dashboard/archive/${WAHA_DASHBOARD_SHA}.zip \
WAHA_DASHBOARD_GITHUB_REPO=$(jq -r '.waha.dashboard.repo' /tmp/waha.config.json) && \
WAHA_DASHBOARD_SHA=$(jq -r '.waha.dashboard.ref' /tmp/waha.config.json) && \
wget https://github.com/${WAHA_DASHBOARD_GITHUB_REPO}/archive/${WAHA_DASHBOARD_SHA}.zip \
&& unzip ${WAHA_DASHBOARD_SHA}.zip -d /tmp/dashboard \
&& mkdir -p /dashboard \
&& mv /tmp/dashboard/dashboard-${WAHA_DASHBOARD_SHA}/* /dashboard/ \
&& rm -rf ${WAHA_DASHBOARD_SHA}.zip \
&& rm -rf /tmp/dashboard/dashboard-${WAHA_DASHBOARD_SHA}
#
# GOWS
#
FROM golang:1.23-bullseye AS gows
# jq to parse json
RUN apt-get update && apt-get install -y jq && rm -rf /var/lib/apt/lists/*
# install protoc
RUN apt-get update && \
apt-get install protobuf-compiler -y
# Image processing for thumbnails
RUN apt-get update \
&& apt-get install -y libvips-dev \
&& rm -rf /var/lib/apt/lists/*
COPY waha.config.json /tmp/waha.config.json
WORKDIR /go/gows
RUN \
GOWS_GITHUB_REPO=$(jq -r '.waha.gows.repo' /tmp/waha.config.json) && \
GOWS_SHA=$(jq -r '.waha.gows.ref' /tmp/waha.config.json) && \
ARCH=$(uname -m) && \
if [ "$ARCH" = "x86_64" ]; then ARCH="amd64"; \
elif [ "$ARCH" = "aarch64" ]; then ARCH="arm64"; \
else echo "Unsupported architecture: $ARCH" && exit 1; fi && \
mkdir -p /go/gows/bin && \
wget -O /go/gows/bin/gows https://github.com/${GOWS_GITHUB_REPO}/releases/download/${GOWS_SHA}/gows-${ARCH} && \
chmod +x /go/gows/bin/gows
#
# Final
#
@@ -44,12 +79,18 @@ ENV PUPPETEER_SKIP_DOWNLOAD=True
# https://github.com/devlikeapro/waha/issues/347
ENV NODE_OPTIONS="--max-old-space-size=16384"
ARG USE_BROWSER=chromium
ARG WHATSAPP_DEFAULT_ENGINE
RUN echo "USE_BROWSER=$USE_BROWSER"
# Install ffmpeg to generate previews for videos
RUN apt-get update && apt-get install -y ffmpeg --no-install-recommends && rm -rf /var/lib/apt/lists/*
# Image processing for thumbnails
RUN apt-get update \
&& apt-get install -y libvips \
&& rm -rf /var/lib/apt/lists/*
# Install zip and unzip - either for chromium or chrome
RUN if [ "$USE_BROWSER" = "chromium" ] || [ "$USE_BROWSER" = "chrome" ]; then \
apt-get update \
@@ -90,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 \
@@ -99,12 +140,25 @@ RUN if [ "$USE_BROWSER" = "chrome" ]; then \
&& rm -rf /var/lib/apt/lists/*; \
fi
# GOWS requirements
# libc6
RUN apt-get update \
&& apt-get install -y libc6 \
&& rm -rf /var/lib/apt/lists/*
# Set the ENV for docker image
ENV WHATSAPP_DEFAULT_ENGINE=$WHATSAPP_DEFAULT_ENGINE
# Attach sources, install packages
WORKDIR /app
COPY package.json ./
COPY --from=build /src/node_modules ./node_modules
COPY --from=build /src/dist ./dist
COPY --from=build /git/node_modules ./node_modules
COPY --from=build /git/dist ./dist
COPY --from=dashboard /dashboard ./dist/dashboard
COPY --from=gows /go/gows/bin/gows /app/gows
ENV WAHA_GOWS_PATH /app/gows
ENV WAHA_GOWS_SOCKET /tmp/gows.sock
COPY entrypoint.sh /entrypoint.sh
# Chokidar options to monitor file changes
+21 -2
View File
@@ -5,7 +5,10 @@ build-chrome:
docker build . -t devlikeapro/waha-plus:chrome --build-arg USE_BROWSER=chrome
build-noweb:
docker build . -t devlikeapro/waha:noweb --build-arg USE_BROWSER=none
docker build . -t devlikeapro/waha:noweb --build-arg USE_BROWSER=none --build-arg WHATSAPP_DEFAULT_ENGINE=NOWEB
build-plus-gows:
docker build . -t devlikeapro/waha-plus:gows --build-arg USE_BROWSER=none --build-arg WHATSAPP_DEFAULT_ENGINE=GOWS
build-all: build build-chrome build-noweb
@@ -32,10 +35,26 @@ for-swagger:
WHATSAPP_SWAGGER_CONFIG_ADVANCED=true . ${NVM_DIR}/nvm.sh && nvm exec yarn start
up-noweb:
. ${NVM_DIR}/nvm.sh && nvm exec yarn up @adiwajshing/baileys@github:devlikeapro/Baileys#fork-master-channels-678
. ${NVM_DIR}/nvm.sh && nvm exec yarn up @adiwajshing/baileys@github:devlikeapro/Baileys#fork-master-2025-05-01
up-noweb-libsignal:
. ${NVM_DIR}/nvm.sh && nvm exec yarn up libsignal@github:devlikeapro/libsignal-node#fork-master
up-webjs:
. ${NVM_DIR}/nvm.sh && nvm exec yarn up whatsapp-web.js@github:devlikeapro/whatsapp-web.js#fork-main-channels
start-proxy:
docker run --rm -d --name squid-container -e TZ=UTC -p 3128:3128 ubuntu/squid:5.2-22.04_beta
proto-gows:
node scripts/gows-proto.js build --dir ../gows/proto
gows:
cd ../gows && \
export PATH=${HOME}/go/bin:${PATH} && \
make all
copy-dashboard:
cd ../waha-hub/ui && \
make copy-waha
+21 -59
View File
@@ -6,22 +6,16 @@
**WAHA** - **W**hats**A**pp **H**TTP **A**PI (REST API) that you can install on your own server and run in less than 5 minutes!
[![Docker Pulls](https://img.shields.io/docker/pulls/devlikeapro/whatsapp-http-api)](https://hub.docker.com/r/devlikeapro/waha)
[![Docker Pulls](https://img.shields.io/docker/pulls/devlikeapro/waha)](https://hub.docker.com/r/devlikeapro/waha)
- Documentation: [https://waha.devlike.pro/](https://waha.devlike.pro/)
- Dashboard Example: [https://waha.devlike.pro/dashboard](https://waha.devlike.pro/dashboard)
- Swagger Example: [https://waha.devlike.pro/swagger](https://waha.devlike.pro/swagger)
# Tables of Contents
<!-- toc -->
- [Features](#features)
* [Messages](#messages)
* [Sessions](#sessions)
* [Security](#security)
* [Updates](#updates)
* [Groups](#groups)
* [Contacts](#contacts)
- [Quick start](#quick-start)
* [Requirements](#requirements)
* [Send your first message](#send-your-first-message)
@@ -32,52 +26,11 @@
+ [5. Get the screenshot](#5-get-the-screenshot)
+ [6. Send a text message](#6-send-a-text-message)
* [What is next?](#what-is-next)
- [Development](#development)
* [Start the project](#start-the-project)
<!-- tocstop -->
# Features
## Messages
- Send messages
- Receive messages
- Message replies
- Send location
- Receive location
- React to messages
- Send media (images/documents/files)
- Send voice messages
- Receive media (images/audio/video/documents)
- Receive messages - webhook retries
## Sessions
- Multi Device
- Get the screenshot
- Session saving (don't have to scan QR on every restart)
- Single WhatsApp account running inside one container
- Multiple WhatsApp account running inside one container
## Security
- Swagger panel authentication
- API authentication
## Updates
- Bug fixes and updates come as soon as they are implemented.
## Groups
- Create a group
- Get invite for group
- Modify group info (subject, description)
- Add group participants
- Kick group participants
- Promote/demote group participants
## Contacts
- Mute/unmute chats
- Block/unblock contacts
- Get contact info
- Get profile pictures
- Get presence (online\offline\typing) status
# Quick start
## Requirements
@@ -87,13 +40,6 @@ instruction <a href="https://docs.docker.com/get-docker/" target="_blank" rel="n
When you are ready - come back and follows the below steps to send the first text message to WhatsApp via HTTP API!
{{< details "Why Docker?" >}}
Docker makes it easy to ship all-in-one solution with the runtime and dependencies. You don't have to worry about
language-specific libraries or chrome installation.
Also Docker makes installation and update processes so simple, just one command!
{{< /details >}}
## Send your first message
Let's go over steps that allow you to send your first text message via WhatsApp HTTP API!
@@ -114,7 +60,7 @@ docker pull devlikeapro/waha-plus
docker logout
```
Read more about how to get `PASSWORD` for [Plus Version →]({{< relref "plus-version" >}})
Read more about how to get `PASSWORD` for [**➕ WAHA Plus**](https://waha.devlike.pro/docs/how-to/waha-plus/)
### 2. Run WhatsApp HTTP API
@@ -195,3 +141,19 @@ curl -d "{\"chatId\": \"${PHONE}@c.us\", \"text\": \"Hello from WhatsApp HTTP AP
## What is next?
[Go and read the full documentation!](https://waha.devlike.pro/docs/overview/introduction/)
# Development
## Start the project
1. Clone the repository
2. Make sure you're using node>=22
3. Run the following commands:
```bash
# Install dependencies
yarn install
# Fetch and compile proto files
yarn gows:proto
# Run
yarn start
# open http://localhost:3000
```
+22 -4
View File
@@ -22,9 +22,27 @@ services:
env_file:
- .env
# NOTE: Only if you're using MongoDB
# https://waha.devlike.pro/docs/how-to/storages/#sessions---mongodb
# Uncomment this block if you're using MongoDB
# NOTE: Only if you're using PostgreSQL to save sessions
# https://waha.devlike.pro/docs/how-to/storages/#media---postgresql
# postgres:
# image: postgres:17
# restart: always
# environment:
# POSTGRES_USER: postgres
# POSTGRES_PASSWORD: postgres
# POSTGRES_DB: postgres
# ports:
# - "5432:5432"
# volumes:
# - pg_data:/var/lib/postgresql/data
# command:
# - postgres
# - "-c"
# - "max_connections=3000"
# NOTE: Only if you're using MongoDB
# https://waha.devlike.pro/docs/how-to/storages/#sessions---mongodb
# Uncomment this block if you're using MongoDB
# mongodb:
# image: mongo
# container_name: mongodb
@@ -55,6 +73,6 @@ services:
# command: server /data --console-address ":9001"
volumes:
# NOTE: Only if you're using MongoDB
mongodb_data: {}
minio_data: {}
pg_data: {}
+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
+1 -1
View File
@@ -2,7 +2,7 @@
# Calculate UV_THREADPOOL_SIZE based on number of CPUs
cpus=$(node -e "const os = require('os'); console.log(os.cpus().length);")
uv_threadpool_size=$(($cpus * 2))
uv_threadpool_size=$(($cpus * 1))
# Set UV_THREADPOOL_SIZE as an environment variable
export UV_THREADPOOL_SIZE="${UV_THREADPOOL_SIZE:-$uv_threadpool_size}"
+21 -6
View File
@@ -20,10 +20,13 @@
"test:watch": "jest --watch",
"test:cov": "jest --coverage",
"test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand",
"test:e2e": "jest --config ./test/jest-e2e.json"
"test:e2e": "jest --config ./test/jest-e2e.json",
"gows:proto:fetch": "node scripts/gows-proto.js fetch",
"gows:proto:build": "node scripts/gows-proto.js build",
"gows:proto": "yarn gows:proto:fetch && yarn gows:proto:build"
},
"dependencies": {
"@adiwajshing/baileys": "github:devlikeapro/Baileys#fork-master-channels-678",
"@adiwajshing/baileys": "github:devlikeapro/Baileys#fork-master-2025-05-01",
"@adiwajshing/keyed-db": "^0.2.4",
"@aws-sdk/client-s3": "^3.633.0",
"@aws-sdk/s3-request-presigner": "^3.633.0",
@@ -44,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",
@@ -55,17 +59,21 @@
"express-basic-auth": "^1.2.1",
"file-type": "16.5.4",
"fs-extra": "^11.2.0",
"google-protobuf": "^3.21.4",
"https-proxy-agent": "^7.0.0",
"joi": "^17.13.3",
"knex": "^3.1.0",
"libphonenumber-js": "^1.10.36",
"libsignal": "github:devlikeapro/libsignal-node#fork-master",
"link-preview-js": "^3.0.4",
"lodash": "^4.17.21",
"mime-types": "^2.1.27",
"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",
"pino-http": "^10.2.0",
"pino-pretty": "^11.2.1",
"pretty-bytes": "5.6.0",
@@ -79,7 +87,8 @@
"sharp": "^0.33.4",
"swagger-ui-express": "^4.1.4",
"ulid": "^2.3.0",
"whatsapp-web.js": "github:devlikeapro/whatsapp-web.js#fork-main-channels"
"whatsapp-web.js": "github:devlikeapro/whatsapp-web.js#fork-main-channels",
"write-file-atomic": "^6.0.0"
},
"optionalDependencies": {
"bufferutil": "^4.0.8"
@@ -87,10 +96,13 @@
"resolutions": {
"bufferutil": "^4.0.8",
"ws": "^8.18.0",
"puppeteer": "^23.6.0",
"whatwg-url": "13.0.0"
"puppeteer": "^24.7.2",
"whatwg-url": "13.0.0",
"libsignal": "github:devlikeapro/libsignal-node#fork-master"
},
"devDependencies": {
"@grpc/grpc-js": "^1.12.4",
"@grpc/proto-loader": "^0.7.13",
"@nestjs/cli": "^9.0.0",
"@nestjs/schematics": "^9.0.1",
"@nestjs/testing": "^9.0.9",
@@ -104,14 +116,17 @@
"eslint-config-prettier": "^6.10.0",
"eslint-plugin-import": "^2.20.1",
"eslint-plugin-simple-import-sort": "^10.0.0",
"grpc-tools": "^1.12.4",
"jest": "^29.7.0",
"prettier": "^1.19.1",
"protoc-gen-ts": "^0.8.7",
"supertest": "^4.0.2",
"ts-jest": "^29.1.3",
"ts-loader": "^6.2.1",
"ts-node": "^10.9.1",
"tsconfig-paths": "^4.1.0",
"typescript": "^4.8.4"
"typescript": "^4.8.4",
"yargs": "^17.7.2"
},
"jest": {
"moduleFileExtensions": [
+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."
+144
View File
@@ -0,0 +1,144 @@
// eslint-disable-next-line @typescript-eslint/no-var-requires
const fs = require('fs');
// eslint-disable-next-line @typescript-eslint/no-var-requires
const path = require('path');
// eslint-disable-next-line @typescript-eslint/no-var-requires
const axios = require('axios');
// eslint-disable-next-line @typescript-eslint/no-var-requires
const { execSync } = require('child_process');
// eslint-disable-next-line @typescript-eslint/no-var-requires
const yargs = require('yargs');
// Defaults
const CONFIG_FILE = 'waha.config.json';
// Load defaults from package.json
gows = (() => {
try {
const config = JSON.parse(fs.readFileSync(CONFIG_FILE, 'utf8'));
return config.waha.gows || {};
} catch (error) {
return {};
}
})();
const DEFAULT_REPO = gows.repo;
if (!DEFAULT_REPO) {
throw new Error(`Missing default repo in ${CONFIG_FILE}`);
}
const DEFAULT_REF = gows.ref;
if (!DEFAULT_REF) {
throw new Error('Missing default ref in ${CONFIG_FILE}');
}
const DEFAULT_DIR = './src/core/engines/gows/proto';
const PROTO_FILES = ['gows.proto'];
const PROTO_OUTPUT = './src/core/engines/gows/grpc';
// Helper function to clean directory
function cleanDirectory(directory, suffix) {
if (!fs.existsSync(directory)) {
fs.mkdirSync(directory, { recursive: true });
return;
}
const files = fs.readdirSync(directory);
for (const file of files) {
const filePath = path.join(directory, file);
if (file.endsWith(suffix)) {
fs.unlinkSync(filePath);
}
}
}
// Helper function to download files
async function downloadFiles(repo, ref, directory) {
for (const file of PROTO_FILES) {
const url = `https://github.com/${repo}/releases/download/${ref}/${file}`;
const filePath = path.join(directory, file);
try {
const response = await axios.get(url, { responseType: 'arraybuffer' });
fs.writeFileSync(filePath, response.data);
console.log(`Downloaded: ${file}`);
} catch (error) {
console.error(`Failed to download ${file}: ${error.message}`);
}
}
}
// Handler for fetch command
async function handleFetch(repo, ref, dir) {
console.log(`Fetching .proto files from ${repo}@${ref} to ${dir}...`);
cleanDirectory(dir, '.proto');
await downloadFiles(repo, ref, dir);
}
// Handler for build command
function handleBuild(dir) {
console.log('Building gRPC files...');
cleanDirectory(PROTO_OUTPUT);
const command = `grpc_tools_node_protoc \
--plugin=protoc-gen-ts=node_modules/.bin/protoc-gen-ts \
--plugin=protoc-gen-grpc=node_modules/.bin/grpc_tools_node_protoc_plugin \
--js_out=import_style=commonjs,binary:${PROTO_OUTPUT} \
--grpc_out=grpc_js:${PROTO_OUTPUT} \
--ts_out=grpc_js:${PROTO_OUTPUT} \
-I ${dir} ${dir}/gows.proto`;
try {
execSync(command, { stdio: 'inherit' });
console.log('gRPC files built successfully.');
} catch (error) {
console.error(`Failed to build gRPC files: ${error.message}`);
}
}
//
// Commands
//
// fetch
yargs.command(
'fetch',
'Fetch .proto files from GitHub',
(yargs) => {
yargs
.option('repo', {
describe: 'GitHub repository (owner/repo)',
type: 'string',
default: DEFAULT_REPO,
})
.option('ref', {
describe: 'Git reference (branch or commit SHA)',
type: 'string',
default: DEFAULT_REF,
})
.option('dir', {
describe: 'Directory to output .proto files',
type: 'string',
default: DEFAULT_DIR,
});
},
async (argv) => {
await handleFetch(argv.repo, argv.ref, argv.dir);
},
);
// build
yargs.command(
'build',
'Build gRPC files from .proto files',
(yargs) => {
yargs.option('dir', {
describe: 'Directory containing .proto files',
type: 'string',
default: DEFAULT_DIR,
});
},
(argv) => {
handleBuild(argv.dir);
},
);
// Parse arguments
yargs.parse();
-22
View File
@@ -1,22 +0,0 @@
#!/bin/bash
set -e
echo "Starting WAHA..."
docker run -d \
--restart=always \
--name waha \
-p 3000:3000/tcp \
-v ./.sessions:/app/.sessions \
-v ./.media:/app/.media \
--env WHATSAPP_API_KEY=321 \
--env WAHA_DASHBOARD_USERNAME=waha \
--env WAHA_DASHBOARD_PASSWORD=waha \
--env WAHA_LOG_FORMAT=JSON \
--env WAHA_LOG_LEVEL=info \
--env WHATSAPP_DEFAULT_ENGINE=WEBJS \
--env WAHA_PRINT_QR=False \
--env WHATSAPP_FILES_LIFETIME=0 \
--env WHATSAPP_FILES_FOLDER=/app/.media \
devlikeapro/waha-plus:latest
echo "WAHA started."
-21
View File
@@ -1,21 +0,0 @@
set -e
echo "Updating WAHA..."
echo "Logging in to Docker..."
docker login -u devlikeapro # -p {KEY} - you can hard code key here https://portal.devlike.pro/
echo "Pulling the latest image..."
docker pull devlikeapro/waha-plus
echo "Logging out of Docker..."
docker logout
# Just pull if you're using WAHA Core
# docker pull devlikeapro/waha
echo "Stopping the current container..."
docker stop waha || true
echo "Removing the current container..."
docker rm waha || true
echo "Running WAHA..."
sh ./waha-run.sh
-39
View File
@@ -6,7 +6,6 @@ import {
Query,
UseInterceptors,
} from '@nestjs/common';
import { UnprocessableEntityException } from '@nestjs/common/exceptions/unprocessable-entity.exception';
import { ApiOperation, ApiSecurity, ApiTags } from '@nestjs/swagger';
import { ApiFileAcceptHeader } from '@waha/nestjs/ApiFileAcceptHeader';
import {
@@ -19,8 +18,6 @@ import { SessionManager } from '../core/abc/manager.abc';
import { WhatsappSession } from '../core/abc/session.abc';
import { BufferResponseInterceptor } from '../nestjs/BufferResponseInterceptor';
import {
CaptchaBody,
OTPRequest,
QRCodeFormat,
QRCodeQuery,
QRCodeValue,
@@ -63,42 +60,6 @@ class AuthController {
) {
return session.requestCode(request.phoneNumber, request.method, request);
}
@Post('authorize-code')
@SessionApiParam
@ApiOperation({
summary: 'Send OTP authentication code.',
})
authorizeCode(
@SessionParam session: WhatsappSession,
@Body() request: OTPRequest,
) {
return session.authorizeCode(request.code);
}
@Get('captcha')
@SessionApiParam
@ApiFileAcceptHeader()
@UseInterceptors(new BufferResponseInterceptor())
@ApiOperation({
summary: 'Get captcha image.',
})
async getCaptcha(@SessionParam session: WhatsappSession): Promise<Buffer> {
const image = await session.getCaptcha();
return image.get();
}
@Post('captcha')
@SessionApiParam
@ApiOperation({
summary: 'Enter captcha code.',
})
saveCaptcha(
@SessionParam session: WhatsappSession,
@Body() body: CaptchaBody,
) {
return session.saveCaptcha(body.code);
}
}
export { AuthController };
+103 -6
View File
@@ -3,31 +3,51 @@ import {
Controller,
Delete,
Get,
HttpCode,
HttpStatus,
Param,
Post,
Query,
UsePipes,
} from '@nestjs/common';
import { ApiOperation, ApiSecurity, ApiTags } from '@nestjs/swagger';
import { ApiOperation, ApiParam, ApiSecurity, ApiTags } from '@nestjs/swagger';
import { ChannelsInfoServiceCore } from '@waha/core/services/ChannelsInfoServiceCore';
import { isJidNewsletter } from '@waha/core/utils/jids';
import {
SessionApiParam,
WorkingSessionParam,
} from '@waha/nestjs/params/SessionApiParam';
import { WAHAValidationPipe } from '@waha/nestjs/pipes/WAHAValidationPipe';
import {
Channel,
ChannelCategory,
ChannelCountry,
ChannelListResult,
ChannelMessage,
ChannelSearchByText,
ChannelSearchByView,
ChannelView,
CreateChannelRequest,
ListChannelsQuery,
NewsletterIdApiParam,
NewsletterIdOrInviteCodeApiParam,
PreviewChannelMessages,
} from '@waha/structures/channels.dto';
import { SessionManager } from '../core/abc/manager.abc';
import { isNewsletter, WhatsappSession } from '../core/abc/session.abc';
import {
parseChannelInviteLink,
WhatsappSession,
} from '../core/abc/session.abc';
@ApiSecurity('api_key')
@Controller('api/:session/channels')
@ApiTags('📢 Channels')
export class ChannelsController {
constructor(private manager: SessionManager) {}
constructor(
private manager: SessionManager,
private channelsInfoService: ChannelsInfoServiceCore,
) {}
@Get('')
@SessionApiParam
@@ -73,15 +93,47 @@ export class ChannelsController {
@WorkingSessionParam session: WhatsappSession,
@Param('id') id: string,
): Promise<Channel> {
if (isNewsletter(id)) {
if (isJidNewsletter(id)) {
return session.channelsGetChannel(id);
} else {
// https://www.whatsapp.com/channel/123 => 123
const inviteCode = id.split('/').pop();
const inviteCode = parseChannelInviteLink(id);
return session.channelsGetChannelByInviteCode(inviteCode);
}
}
@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
@@ -125,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();
}
}
+49 -1
View File
@@ -1,3 +1,4 @@
import { isJidGroup } from '@adiwajshing/baileys';
import {
Body,
Controller,
@@ -21,13 +22,19 @@ import {
import { SessionManager } from '../core/abc/manager.abc';
import { WhatsappSession } from '../core/abc/session.abc';
import { parseBool } from '../helpers';
import {
ChatPictureQuery,
ChatPictureResponse,
ChatsPaginationParams,
ChatSummary,
GetChatMessageQuery,
GetChatMessagesFilter,
GetChatMessagesQuery,
OverviewPaginationParams,
PinMessageRequest,
ReadChatMessagesQuery,
ReadChatMessagesResponse,
transformAck,
} from '../structures/chats.dto';
import { EditMessageRequest } from '../structures/chatting.dto';
@@ -48,6 +55,20 @@ class ChatsController {
return session.getChats(pagination);
}
@Get('overview')
@SessionApiParam
@ApiOperation({
summary:
'Get chats overview. Includes all necessary things to build UI "your chats overview" page - chat id, name, picture, last message. Sorting by last message timestamp',
})
@UsePipes(new ValidationPipe({ transform: true, whitelist: true }))
getChatsOverview(
@WorkingSessionParam session: WhatsappSession,
@Query() pagination: OverviewPaginationParams,
): Promise<ChatSummary[]> {
return session.getChatsOverview(pagination);
}
@Delete(':chatId')
@SessionApiParam
@ApiOperation({ summary: 'Deletes the chat' })
@@ -59,6 +80,19 @@ class ChatsController {
return session.deleteChat(chatId);
}
@Get(':chatId/picture')
@SessionApiParam
@ApiOperation({ summary: 'Gets chat picture' })
@UsePipes(new ValidationPipe({ transform: true, whitelist: true }))
async getChatPicture(
@WorkingSessionParam session: WhatsappSession,
@Param('chatId') chatId: string,
@Query() query: ChatPictureQuery,
): Promise<ChatPictureResponse> {
const url = await session.getContactProfilePicture(chatId, query.refresh);
return { url: url };
}
@Get(':chatId/messages')
@SessionApiParam
@ApiOperation({ summary: 'Gets messages in the chat' })
@@ -70,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' })
+66 -16
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,21 +199,20 @@ export class ChattingController {
return whatsapp.sendLocation(request);
}
@Post('/sendLinkPreview')
async sendLinkPreview(@Body() request: MessageLinkPreviewRequest) {
@Post('/sendContactVcard')
async sendContactVcard(@Body() request: MessageContactVcardRequest) {
const whatsapp = await this.manager.getWorkingSession(request.session);
return whatsapp.sendLinkPreview(request);
return whatsapp.sendContactVCard(request);
}
@Get('/messages')
@ApiOperation({ summary: 'Get messages in a chat' })
@Post('/send/buttons/reply')
@ApiOperation({
summary: 'Reply on a button message',
})
@UsePipes(new ValidationPipe({ transform: true, whitelist: true }))
async getMessages(
@Query() query: GetMessageQuery,
@Query() filter: GetChatMessagesFilter,
) {
const whatsapp = await this.manager.getWorkingSession(query.session);
return whatsapp.getChatMessages(query.chatId, query, filter);
async sendButtonsReply(@Body() request: MessageButtonReply) {
const whatsapp = await this.manager.getWorkingSession(request.session);
return whatsapp.sendButtonsReply(request);
}
@Get('/sendText')
@@ -192,10 +225,20 @@ export class ChattingController {
return whatsapp.sendText(msg);
}
@Post('/sendContactVcard')
async sendContactVcard(@Body() request: MessageContactVcardRequest) {
const whatsapp = await this.manager.getWorkingSession(request.session);
return whatsapp.sendContactVCard(request);
@Get('/messages')
@ApiOperation({
summary: 'Get messages in a chat',
description: 'DEPRECATED. Use "GET /api/chats/{id}/messages" instead',
deprecated: true,
})
@UsePipes(new ValidationPipe({ transform: true, whitelist: true }))
async getMessages(
@Query() query: GetMessageQuery,
@Query() filter: GetChatMessagesFilter,
) {
filter = transformAck(filter);
const whatsapp = await this.manager.getWorkingSession(query.session);
return whatsapp.getChatMessages(query.chatId, query, filter);
}
@Get('/checkNumberStatus')
@@ -221,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);
}
}
+8 -2
View File
@@ -16,6 +16,7 @@ import {
WANumberExistResult,
} from '../structures/chatting.dto';
import {
ContactProfilePictureQuery,
ContactQuery,
ContactRequest,
ContactsPaginationParams,
@@ -75,9 +76,14 @@ export class ContactsController {
description:
'If privacy settings do not allow to get the picture, the method will return null.',
})
async getProfilePicture(@Query() query: ContactQuery) {
@UsePipes(new ValidationPipe({ transform: true, whitelist: true }))
async getProfilePicture(@Query() query: ContactProfilePictureQuery) {
const whatsapp = await this.manager.getWorkingSession(query.session);
return whatsapp.getContactProfilePicture(query);
const url = await whatsapp.getContactProfilePicture(
query.contactId,
query.refresh,
);
return { profilePictureURL: url };
}
@Post('/block')
+172 -57
View File
@@ -3,9 +3,14 @@ import {
Controller,
Delete,
Get,
HttpCode,
HttpStatus,
Param,
Post,
Put,
Query,
UsePipes,
ValidationPipe,
} from '@nestjs/common';
import { ApiOperation, ApiSecurity, ApiTags } from '@nestjs/swagger';
import { GroupIdApiParam } from '@waha/nestjs/params/ChatIdApiParam';
@@ -13,12 +18,23 @@ 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 { WhatsappSession } from '../core/abc/session.abc';
import { parseGroupInviteLink, WhatsappSession } from '../core/abc/session.abc';
import {
CreateGroupRequest,
DescriptionRequest,
GroupField,
GroupsListFields,
GroupsPaginationParams,
JoinGroupRequest,
JoinGroupResponse,
ParticipantsRequest,
SettingsSecurityChangeInfo,
SubjectRequest,
@@ -40,11 +56,65 @@ export class GroupsController {
return session.createGroup(request);
}
@Get('join-info')
@SessionApiParam
@ApiOperation({ summary: 'Get info about the group before joining.' })
async joinInfoGroup(
@WorkingSessionParam session: WhatsappSession,
@Query() query: JoinGroupRequest,
): Promise<any> {
const code = parseGroupInviteLink(query.code);
return session.joinInfoGroup(code);
}
@Post('join')
@HttpCode(HttpStatus.OK)
@SessionApiParam
@ApiOperation({ summary: 'Join group via code' })
async joinGroup(
@WorkingSessionParam session: WhatsappSession,
@Body() request: JoinGroupRequest,
): Promise<JoinGroupResponse> {
const code = parseGroupInviteLink(request.code);
const id = await session.joinGroup(code);
return { id: id };
}
@Get('')
@SessionApiParam
@ApiOperation({ summary: 'Get all groups.' })
getGroups(@WorkingSessionParam session: WhatsappSession) {
return session.getGroups();
@UsePipes(new ValidationPipe({ transform: true, whitelist: true }))
async getGroups(
@WorkingSessionParam session: WhatsappSession,
@Query() pagination: GroupsPaginationParams,
@Query() fields: GroupsListFields,
) {
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 }))
async refreshGroups(@WorkingSessionParam session: WhatsappSession) {
return { success: await session.refreshGroups() };
}
@Get(':id')
@@ -58,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
@@ -119,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
@@ -185,6 +295,7 @@ export class GroupsController {
}
@Post(':id/invite-code/revoke')
@HttpCode(HttpStatus.OK)
@SessionApiParam
@GroupIdApiParam
@ApiOperation({
@@ -210,6 +321,7 @@ export class GroupsController {
}
@Post(':id/participants/add')
@HttpCode(HttpStatus.OK)
@SessionApiParam
@GroupIdApiParam
@ApiOperation({ summary: 'Add participants' })
@@ -222,6 +334,7 @@ export class GroupsController {
}
@Post(':id/participants/remove')
@HttpCode(HttpStatus.OK)
@SessionApiParam
@GroupIdApiParam
@ApiOperation({
@@ -236,6 +349,7 @@ export class GroupsController {
}
@Post(':id/admin/promote')
@HttpCode(HttpStatus.OK)
@SessionApiParam
@GroupIdApiParam
@ApiOperation({ summary: 'Promote participants to admin users.' })
@@ -248,6 +362,7 @@ export class GroupsController {
}
@Post(':id/admin/demote')
@HttpCode(HttpStatus.OK)
@SessionApiParam
@GroupIdApiParam
@ApiOperation({ summary: 'Demotes participants to regular users.' })
+94
View File
@@ -0,0 +1,94 @@
import {
Body,
Controller,
Delete,
Get,
Put,
UnprocessableEntityException,
UsePipes,
} from '@nestjs/common';
import { ApiOperation, ApiSecurity, ApiTags } from '@nestjs/swagger';
import { SessionManager } from '@waha/core/abc/manager.abc';
import { WhatsappSession } from '@waha/core/abc/session.abc';
import {
SessionApiParam,
WorkingSessionParam,
} from '@waha/nestjs/params/SessionApiParam';
import { WAHAValidationPipe } from '@waha/nestjs/pipes/WAHAValidationPipe';
import { Result } from '@waha/structures/base.dto';
import {
MyProfile,
ProfileNameRequest,
ProfilePictureRequest,
ProfileStatusRequest,
} from '@waha/structures/profile.dto';
@ApiSecurity('api_key')
@Controller('api/:session/profile')
@ApiTags('🆔 Profile')
export class ProfileController {
constructor(private manager: SessionManager) {}
@Get('')
@SessionApiParam
@ApiOperation({ summary: 'Get my profile' })
async getMyProfile(
@WorkingSessionParam session: WhatsappSession,
): Promise<MyProfile> {
const me = session.getSessionMeInfo();
if (!me) {
throw new UnprocessableEntityException('No profile found');
}
const picture = await session.getContactProfilePicture(me.id, false);
return {
id: me.id,
name: me.pushName,
picture: picture || null,
};
}
@Put('/name')
@SessionApiParam
@UsePipes(new WAHAValidationPipe())
@ApiOperation({ summary: 'Set my profile name' })
async setProfileName(
@WorkingSessionParam session: WhatsappSession,
@Body() request: ProfileNameRequest,
): Promise<Result> {
const success = await session.setProfileName(request.name);
return { success: success ?? true };
}
@Put('/status')
@SessionApiParam
@UsePipes(new WAHAValidationPipe())
@ApiOperation({ summary: 'Set profile status (About)' })
async setProfileStatus(
@WorkingSessionParam session: WhatsappSession,
@Body() request: ProfileStatusRequest,
): Promise<Result> {
const success = await session.setProfileStatus(request.status);
return { success: success ?? true };
}
@Put('/picture')
@SessionApiParam
@ApiOperation({ summary: 'Set profile picture' })
async setProfilePicture(
@WorkingSessionParam session: WhatsappSession,
@Body() request: ProfilePictureRequest,
): Promise<Result> {
const success = await session.updateProfilePicture(request.file);
return { success: success ?? true };
}
@Delete('/picture')
@SessionApiParam
@ApiOperation({ summary: 'Delete profile picture' })
async deleteProfilePicture(
@WorkingSessionParam session: WhatsappSession,
): Promise<Result> {
const success = await session.updateProfilePicture(null);
return { success: success || true };
}
}
+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 };
+25 -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,27 +119,20 @@ 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 {
return this.configService.get('WHATSAPP_SESSIONS_MONGO_URL', undefined);
}
getSessionPostgresUrl(): string | undefined {
return this.configService.get(
'WHATSAPP_SESSIONS_POSTGRESQL_URL',
undefined,
);
}
get(name: string, defaultValue: any = undefined): any {
return this.configService.get(name, defaultValue);
}
@@ -175,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`);
}
}
}
+8 -5
View File
@@ -1,13 +1,16 @@
// eslint-disable-next-line @typescript-eslint/no-var-requires
const QRCode = require('qrcode');
export class QR {
private base64: string;
public raw?: string;
save(base64: string, raw?: string) {
this.base64 = base64.replace(/^data:image\/png;base64,/, '');
save(raw?: string) {
this.raw = raw;
}
get(): Buffer {
return Buffer.from(this.base64, 'base64');
async get(): Promise<Buffer> {
const url = await QRCode.toDataURL(this.raw);
const base64 = url.replace(/^data:image\/png;base64,/, '');
return Buffer.from(base64, 'base64');
}
}
+3 -4
View File
@@ -57,13 +57,11 @@ export class SwaggerConfiguratorCore {
.setVersion(VERSION.version)
.addTag('🖥️ Sessions', 'Control WhatsApp sessions (accounts)')
.addTag('🔑 Auth', 'Authentication')
.addTag('🆔 Profile', 'Your profile information')
.addTag('🖼️ Screenshot', 'Get screenshot of WhatsApp and show QR code')
.addTag('📤 Chatting', 'Chatting methods')
.addTag('📢 Channels', 'Channels (newsletters) methods')
.addTag(
'🟢 Status',
'Status (aka stories) methods. <b>NOWEB</b> engine only!',
)
.addTag('🟢 Status', 'Status (aka stories) methods')
.addTag('💬 Chats', `Chats methods`)
.addTag(
'👤 Contacts',
@@ -140,6 +138,7 @@ export class SwaggerConfiguratorCore {
webhooks[event] = {
post: {
summary: eventMetadata.description,
deprecated: eventMetadata.deprecated || false,
requestBody: {
content: {
'application/json': {
+15
View File
@@ -0,0 +1,15 @@
export interface EngineBootstrap {
bootstrap(): Promise<void>;
shutdown(): Promise<void>;
}
export class NoopEngineBootstrap implements EngineBootstrap {
async bootstrap(): Promise<void> {
return;
}
async shutdown(): Promise<void> {
return;
}
}
+68 -4
View File
@@ -1,8 +1,15 @@
import {
BeforeApplicationShutdown,
OnApplicationBootstrap,
UnprocessableEntityException,
} from '@nestjs/common';
import { WhatsappConfigService } from '@waha/config.service';
import {
EngineBootstrap,
NoopEngineBootstrap,
} from '@waha/core/abc/EngineBootstrap';
import { GowsEngineConfigService } from '@waha/core/config/GowsEngineConfigService';
import { GowsBootstrap } from '@waha/core/engines/gows/GowsBootstrap';
import { ISessionMeRepository } from '@waha/core/storage/ISessionMeRepository';
import { ISessionWorkerRepository } from '@waha/core/storage/ISessionWorkerRepository';
import { WAHAWebhook } from '@waha/structures/webhooks.dto';
@@ -29,7 +36,9 @@ import { WhatsappSession } from './session.abc';
// eslint-disable-next-line @typescript-eslint/no-var-requires
const AsyncLock = require('async-lock');
export abstract class SessionManager implements BeforeApplicationShutdown {
export abstract class SessionManager
implements BeforeApplicationShutdown, OnApplicationBootstrap
{
public store: any;
public sessionAuthRepository: ISessionAuthRepository;
public sessionConfigRepository: ISessionConfigRepository;
@@ -37,14 +46,21 @@ export abstract class SessionManager implements BeforeApplicationShutdown {
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;
WAIT_STATUS_TIMEOUT = 10_000;
LOCK_TIMEOUT = 10_000;
protected constructor(
protected config: WhatsappConfigService,
protected log: PinoLogger,
protected config: WhatsappConfigService,
protected gowsConfigService: GowsEngineConfigService,
) {
this.lock = new AsyncLock({ maxPending: Infinity });
this.lock = new AsyncLock({
maxPending: Infinity,
timeout: this.LOCK_TIMEOUT,
});
this.log.setContext(SessionManager.name);
}
@@ -134,6 +150,37 @@ export abstract class SessionManager implements BeforeApplicationShutdown {
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),
@@ -156,18 +203,35 @@ export abstract class SessionManager implements BeforeApplicationShutdown {
beforeApplicationShutdown(signal?: string) {
return;
}
onApplicationBootstrap(): any {
return;
}
protected getEngineBootstrap(engine: WAHAEngine): EngineBootstrap {
const logger = this.log.logger.child({ engine: engine.toLowerCase() });
if (engine === WAHAEngine.GOWS) {
const config = this.gowsConfigService.getBootstrapConfig();
return new GowsBootstrap(logger, config);
}
return new NoopEngineBootstrap();
}
}
export function populateSessionInfo(
event: WAHAEvents,
session: WhatsappSession,
) {
return (payload: any): WAHAWebhook => {
const id = payload._eventId;
const 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,
+297 -22
View File
@@ -1,17 +1,29 @@
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,
ChannelMessage,
ChannelSearchByText,
ChannelSearchByView,
CreateChannelRequest,
ListChannelsQuery,
PreviewChannelMessages,
} from '@waha/structures/channels.dto';
import {
ChatSummary,
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 { 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';
@@ -19,6 +31,7 @@ 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 * as NodeCache from 'node-cache';
import { Logger } from 'pino';
import {
BehaviorSubject,
@@ -38,10 +51,12 @@ import {
ChatRequest,
CheckNumberStatusQuery,
EditMessageRequest,
MessageButtonReply,
MessageContactVcardRequest,
MessageFileRequest,
MessageForwardRequest,
MessageImageRequest,
MessageLinkCustomPreviewRequest,
MessageLinkPreviewRequest,
MessageLocationRequest,
MessagePollRequest,
@@ -51,6 +66,7 @@ import {
MessageTextRequest,
MessageVideoRequest,
MessageVoiceRequest,
SendSeenRequest,
} from '../../structures/chatting.dto';
import { ContactQuery, ContactRequest } from '../../structures/contacts.dto';
import {
@@ -61,6 +77,8 @@ import {
} from '../../structures/enums.dto';
import {
CreateGroupRequest,
GroupField,
GroupsListFields,
ParticipantsRequest,
SettingsSecurityChangeInfo,
} from '../../structures/groups.dto';
@@ -131,6 +149,15 @@ export abstract class WhatsappSession {
private shouldPrintQR: boolean;
protected events2: DefaultMap<WAHAEvents, SwitchObservable<any>>;
private status$: Subject<WAHASessionStatus>;
protected profilePictures: NodeCache = new NodeCache({
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,
@@ -162,6 +189,7 @@ export abstract class WhatsappSession {
filter(Boolean),
map((data) => {
data._eventId = generatePrefixedId('evt');
data._timestampMs = Date.now();
return data;
}),
retry(),
@@ -234,12 +262,13 @@ export abstract class WhatsappSession {
// References:
// https://github.com/pedroslopez/whatsapp-web.js/issues/1420
// https://github.com/wppconnect-team/wppconnect/issues/1326
// https://superuser.com/questions/654565/how-to-run-google-chrome-in-a-single-process
// https://www.bannerbear.com/blog/ways-to-speed-up-puppeteer-screenshots/
return [
'--aggressive-cache-discard',
'--disable-accelerated-2d-canvas',
'--disable-application-cache',
'--disable-background-networking',
// '--disable-background-networking', // COMMENTED to test WEBJS stability
// DO NOT disable software rasterizer, it will break the video
// https://github.com/devlikeapro/waha/issues/629
// '--disable-software-rasterizer',
@@ -249,13 +278,13 @@ export abstract class WhatsappSession {
'--disable-default-apps',
'--disable-dev-shm-usage',
'--disable-extensions',
'--disable-features=site-per-process',
'--disable-gpu',
// '--disable-features=site-per-process', // COMMENTED to test WEBJS stability
//'--disable-gpu', // COMMENTED to test WEBJS stability
'--disable-offer-store-unmasked-wallet-cards',
'--disable-offline-load-stale-cache',
'--disable-popup-blocking',
'--disable-setuid-sandbox',
'--disable-site-isolation-trials', // https://superuser.com/questions/654565/how-to-run-google-chrome-in-a-single-process
'--disable-site-isolation-trials',
'--disable-speech-api',
'--disable-sync',
'--disable-translate',
@@ -264,7 +293,8 @@ export abstract class WhatsappSession {
'--hide-scrollbars',
'--ignore-certificate-errors',
'--ignore-ssl-errors',
'--in-process-gpu', // https://superuser.com/questions/654565/how-to-run-google-chrome-in-a-single-process
// https://github.com/devlikeapro/waha/issues/725
// '--in-process-gpu', // COMMENTED to test WEBJS stability
'--metrics-recording-only',
'--mute-audio',
'--no-default-browser-check',
@@ -273,7 +303,7 @@ export abstract class WhatsappSession {
'--no-sandbox',
'--no-zygote',
'--password-store=basic',
'--renderer-process-limit=2', // https://superuser.com/questions/654565/how-to-run-google-chrome-in-a-single-process
// '--renderer-process-limit=2', // COMMENTED to test WEBJS stability
'--safebrowsing-disable-auto-update',
// '--single-process',
'--use-mock-keychain',
@@ -316,27 +346,68 @@ export abstract class WhatsappSession {
throw new NotImplementedByEngineError();
}
public authorizeCode(code: string) {
throw new NotImplementedByEngineError();
}
abstract getScreenshot(): Promise<Buffer>;
public getSessionMeInfo(): MeInfo | null {
return null;
}
public getCaptcha(): Promise<QR> {
/**
* Profile methods
*/
public setProfileName(name: string): Promise<boolean> {
throw new NotImplementedByEngineError();
}
public saveCaptcha(code: string) {
public setProfileStatus(status: string): Promise<boolean> {
throw new NotImplementedByEngineError();
}
public async updateProfilePicture(
file: BinaryFile | RemoteFile | null,
): Promise<boolean> {
if (file) {
await this.setProfilePicture(file);
} else {
await this.deleteProfilePicture();
}
// Refresh profile picture after update
setTimeout(() => {
this.logger.debug('Refreshing my profile picture after update...');
this.refreshMyProfilePicture()
.then(() => {
this.logger.debug('Refreshed my profile picture after update');
})
.catch((err) => {
this.logger.error('Error refreshing my profile picture after update');
this.logger.error(err, err.stack);
});
}, 3_000);
return true;
}
protected async refreshMyProfilePicture() {
const me = this.getSessionMeInfo();
await this.getContactProfilePicture(me.id, true);
}
protected setProfilePicture(file: BinaryFile | RemoteFile): Promise<boolean> {
throw new NotImplementedByEngineError();
}
protected deleteProfilePicture(): Promise<boolean> {
throw new NotImplementedByEngineError();
}
/**
* Other methods
*/
generateNewMessageId(): Promise<string> {
throw new NotImplementedByEngineError();
}
abstract checkNumberStatus(request: CheckNumberStatusQuery);
abstract sendText(request: MessageTextRequest);
@@ -355,6 +426,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);
@@ -371,9 +448,13 @@ export abstract class WhatsappSession {
throw new NotImplementedByEngineError();
}
sendButtonsReply(request: MessageButtonReply) {
throw new NotImplementedByEngineError();
}
abstract reply(request: MessageReplyRequest);
abstract sendSeen(chat: ChatRequest);
abstract sendSeen(chat: SendSeenRequest);
abstract startTyping(chat: ChatRequest);
@@ -392,6 +473,12 @@ export abstract class WhatsappSession {
throw new NotImplementedByEngineError();
}
public getChatsOverview(
pagination: PaginationParams,
): Promise<ChatSummary[]> {
throw new NotImplementedByEngineError();
}
public deleteChat(chatId) {
throw new NotImplementedByEngineError();
}
@@ -400,10 +487,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,
@@ -499,12 +611,46 @@ export abstract class WhatsappSession {
throw new NotImplementedByEngineError();
}
public getContactAbout(query: ContactQuery) {
public getContactAbout(query: ContactQuery): Promise<{ about: string }> {
throw new NotImplementedByEngineError();
}
public getContactProfilePicture(query: ContactQuery) {
throw new NotImplementedByEngineError();
/**
* Fetch the latest profile picture of the contact (group, newsletter, etc.)
* @param id
*/
abstract fetchContactProfilePicture(id: string): Promise<string | null>;
public async getContactProfilePicture(
id: string,
refresh: boolean,
): Promise<string | null> {
const has: boolean = this.profilePictures.has(id);
if (!has || refresh) {
await this.refreshProfilePicture(id);
}
return this.profilePictures.get(id) || null;
}
protected async refreshProfilePicture(id: string) {
let fn: Promise<string>;
if (isJidBroadcast(id)) {
return null;
} else if (isJidNewsletter(id)) {
fn = this.channelsGetChannel(id).then(
(channel: Channel) => channel.picture || channel.preview,
);
} else {
fn = this.fetchContactProfilePicture(id);
}
this.profilePictures.del(id);
const url = await fn.catch((err) => {
this.logger.warn('Error fetching profile picture');
this.logger.warn(err, err.stack);
return null;
});
this.profilePictures.set(id, url);
return url;
}
public blockContact(request: ContactRequest) {
@@ -522,7 +668,31 @@ export abstract class WhatsappSession {
throw new NotImplementedByEngineError();
}
public getGroups() {
public joinGroup(code: string): Promise<string> {
throw new NotImplementedByEngineError();
}
public joinInfoGroup(code: string): Promise<any> {
throw new NotImplementedByEngineError();
}
public getGroups(pagination: PaginationParams): 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();
}
@@ -558,6 +728,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();
}
@@ -602,13 +809,32 @@ export abstract class WhatsappSession {
throw new NotImplementedByEngineError();
}
public subscribePresence(id: string): Promise<void> {
public subscribePresence(id: string): Promise<any> {
throw new NotImplementedByEngineError();
}
/**
* Channels methods
*/
public searchChannelsByView(
query: ChannelSearchByView,
): Promise<ChannelListResult> {
throw new NotImplementedByEngineError();
}
public searchChannelsByText(
query: ChannelSearchByText,
): Promise<ChannelListResult> {
throw new NotImplementedByEngineError();
}
public async previewChannelMessages(
inviteCode: string,
query: PreviewChannelMessages,
): Promise<ChannelMessage[]> {
throw new NotImplementedByEngineError();
}
public channelsList(query: ListChannelsQuery): Promise<Channel[]> {
throw new NotImplementedByEngineError();
}
@@ -670,6 +896,9 @@ export abstract class WhatsappSession {
throw new NotImplementedByEngineError();
}
/**
* Engine methods
*/
public async getEngineInfo(): Promise<any> {
return {};
}
@@ -711,12 +940,58 @@ export abstract class WhatsappSession {
);
qrcode.generate(qr.raw, { small: true });
}
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 isNewsletter(jid: string) {
return jid.endsWith('@newsletter');
export function getGroupInviteLink(code: string) {
if (code.startsWith('https://')) {
return code;
}
return `https://chat.whatsapp.com/${code}`;
}
export function parseGroupInviteLink(link: string) {
// https://chat.whatsapp.com/123 => 123
return link.split('/').pop();
}
export function getChannelInviteLink(code: string) {
return `https://whatsapp.com/channel/${code}`;
}
export function parseChannelInviteLink(link: string): string {
// https://www.whatsapp.com/channel/123 => 123
const code = link.split('/').pop();
return code;
}
export function getPublicUrlFromDirectPath(directPath: string) {
return `https://pps.whatsapp.net${directPath}`;
}
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;
}
+23 -10
View File
@@ -32,7 +32,7 @@ export class WebsocketGatewayCore
OnGatewayDisconnect,
BeforeApplicationShutdown
{
HEARTBEAT_INTERVAL = 10_000;
HEARTBEAT_INTERVAL = 60_000;
@WebSocketServer()
server: Server;
@@ -65,7 +65,12 @@ export class WebsocketGatewayCore
.getSessionEvents(session, events)
.subscribe((data) => {
this.logger.debug(`Sending data to client, event.id: ${data.id}`, data);
socket.send(JSON.stringify(data));
socket.send(JSON.stringify(data), (err) => {
if (!err) {
return;
}
this.logger.error(`Error sending data to client: ${err}`);
});
});
socket.on('close', () => {
this.logger.debug(`Client disconnected - ${socket.id}`);
@@ -91,17 +96,25 @@ export class WebsocketGatewayCore
async beforeApplicationShutdown(signal?: string) {
this.logger.log('Shutting down websocket server');
this.heartbeat?.stop();
// Allow pending messages to be sent, it can be even 1ms, just to release the event loop
await sleep(100);
this.server?.clients.forEach((options, client) => {
// @ts-ignore
this.logger.debug(`Closing client connection - ${client.id}...`);
client.close(1001, 'Server is shutting down');
});
// Do not turn off heartbeat service here,
// it's responsible for terminating the connection that is not alive
// Close clients and server
await this.close(this.server);
this.logger.log('Websocket server is down');
this.heartbeat?.stop();
}
// Cherry-pick from nestjs new version
// https://github.com/nestjs/nest/pull/13531/files
private async close(server: any) {
const closeEventSignal = new Promise((resolve, reject) =>
server.close((err) => (err ? reject(err) : resolve(undefined))),
);
for (const ws of server.clients) {
ws.terminate();
}
await closeEventSignal;
}
afterInit(server: Server) {
+11 -1
View File
@@ -5,13 +5,17 @@ import { PassportModule } from '@nestjs/passport';
import { ServeStaticModule } from '@nestjs/serve-static';
import { TerminusModule } from '@nestjs/terminus';
import { ChannelsController } from '@waha/api/channels.controller';
import { ProfileController } from '@waha/api/profile.controller';
import {
ServerController,
ServerDebugController,
} from '@waha/api/server.controller';
import { WebsocketGatewayCore } from '@waha/core/api/websocket.gateway.core';
import { GowsEngineConfigService } from '@waha/core/config/GowsEngineConfigService';
import { WebJSEngineConfigService } from '@waha/core/config/WebJSEngineConfigService';
import { MediaLocalStorageModule } from '@waha/core/media/local/media.local.storage.module';
import { MediaLocalStorageConfig } from '@waha/core/media/local/MediaLocalStorageConfig';
import { ChannelsInfoServiceCore } from '@waha/core/services/ChannelsInfoServiceCore';
import { BufferJsonReplacerInterceptor } from '@waha/nestjs/BufferJsonReplacerInterceptor';
import {
getPinoHttpUseLevel,
@@ -105,7 +109,9 @@ export const IMPORTS_CORE = [
const IMPORTS_MEDIA = [
ConfigModule.forRoot({
validationSchema: Joi.object({
WAHA_MEDIA_STORAGE: Joi.string().valid('LOCAL', 'S3').default('LOCAL'),
WAHA_MEDIA_STORAGE: Joi.string()
.valid('LOCAL', 'S3', 'POSTGRESQL')
.default('LOCAL'),
}),
}),
MediaLocalStorageModule,
@@ -116,6 +122,7 @@ const IMPORTS = [...IMPORTS_CORE, ...IMPORTS_MEDIA];
export const CONTROLLERS = [
AuthController,
SessionsController,
ProfileController,
ChattingController,
ChatsController,
ChannelsController,
@@ -144,8 +151,11 @@ const PROVIDERS = [
provide: APP_INTERCEPTOR,
useClass: BufferJsonReplacerInterceptor,
},
ChannelsInfoServiceCore,
DashboardConfigServiceCore,
SwaggerConfigServiceCore,
WebJSEngineConfigService,
GowsEngineConfigService,
WhatsappConfigService,
EngineConfigService,
WebsocketGatewayCore,
+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,
});
}
}
@@ -0,0 +1,26 @@
import { Injectable } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { BootstrapConfig } from '@waha/core/engines/gows/GowsBootstrap';
import { GowsConfig } from '@waha/core/engines/gows/session.gows.core';
@Injectable()
export class GowsEngineConfigService {
constructor(protected configService: ConfigService) {}
getBootstrapConfig(): BootstrapConfig {
return {
path: this.configService.get('WAHA_GOWS_PATH'),
socket: this.getSocket(),
};
}
getSocket() {
return this.configService.get('WAHA_GOWS_SOCKET', '/tmp/gows.sock');
}
getConfig(): GowsConfig {
return {
connection: 'unix:' + this.getSocket(),
};
}
}
@@ -0,0 +1,37 @@
import { Injectable } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { WebJSConfig } from '../../core/engines/webjs/session.webjs.core';
@Injectable()
export class WebJSEngineConfigService {
constructor(protected configService: ConfigService) {}
getConfig(): WebJSConfig {
let webVersion = this.configService.get<string>(
'WAHA_WEBJS_WEB_VERSION',
undefined,
);
if (webVersion === '2.2412.54-videofix') {
// Deprecated version
webVersion = undefined;
}
return {
webVersion: webVersion,
cacheType: this.getCacheType(),
};
}
getCacheType(): 'local' | 'none' {
const cacheType = this.configService
.get<string>('WAHA_WEBJS_CACHE_TYPE', 'none')
.toLowerCase();
if (cacheType != 'local' && cacheType != 'none') {
throw new Error(
'Invalid cache type, only "local" and "none" are allowed',
);
}
return cacheType;
}
}
+3
View File
@@ -0,0 +1,3 @@
export enum Jid {
BROADCAST = 'status@broadcast',
}
@@ -0,0 +1,31 @@
import { EventEmitter } from 'events';
import { Observable, Subscription } from 'rxjs';
interface EventValue<T> {
event: string;
data: T;
}
export class EventsFromObservable<Events extends string> extends EventEmitter {
private subscription: Subscription;
constructor(private observable: Observable<EventValue<any>>) {
super();
}
on(event: Events, listener: (...args: any[]) => void): this {
return super.on(event as string, listener);
}
start() {
this.subscription = this.observable.subscribe({
next: (data) => this.emit(data.event, data.data),
complete: () => this.removeAllListeners(),
error: (err) => null,
});
}
stop() {
this.subscription.unsubscribe();
}
}
+72
View File
@@ -0,0 +1,72 @@
import { EngineBootstrap } from '@waha/core/abc/EngineBootstrap';
import { GowsSubprocess } from '@waha/core/engines/gows/GowsSubprocess';
import { promises as fs } from 'fs';
import { Logger } from 'pino';
export async function isUnixSocket(socketPath: string): Promise<boolean> {
try {
// Get the file stats
const stats = await fs.lstat(socketPath);
// Check if the path is a socket
return stats.isSocket();
} catch (error) {
// Handle errors (e.g., file does not exist)
if (error.code === 'ENOENT') {
return false; // Path does not exist
}
// Re-throw other errors for debugging purposes
throw error;
}
}
export interface BootstrapConfig {
path: string;
socket: string;
}
export class GowsBootstrap implements EngineBootstrap {
private gows: GowsSubprocess;
constructor(
private logger: Logger,
private config: BootstrapConfig,
) {}
async bootstrap(): Promise<void> {
if (!this.config.path) {
this.logger.warn('GOWS path is not set, skipping GOWS initialization.');
this.logger.warn('Make sure to run GOWS manually.');
this.checkSocket(this.config.socket);
return;
}
this.gows = new GowsSubprocess(
this.logger,
this.config.path,
this.config.socket,
);
this.gows.start(() => {
this.logger.info(`GOWS stopped, exiting...`);
process.kill(process.pid, 'SIGTERM');
});
await this.gows.waitWhenReady(10_000);
await this.checkSocket(this.config.socket);
return;
}
async shutdown(): Promise<void> {
if (this.gows) {
await this.gows.stop();
}
}
/**
* Check the provided path exists and is a socket
* @param path
*/
async checkSocket(path: string) {
if (!(await isUnixSocket(path))) {
throw new Error(`Invalid socket path: ${path}`);
}
}
}
@@ -0,0 +1,69 @@
import * as grpc from '@grpc/grpc-js';
import { messages } from '@waha/core/engines/gows/grpc/gows';
import { EnginePayload } from '@waha/structures/webhooks.dto';
import { sleep } from '@waha/utils/promiseTimeout';
import { Logger } from 'pino';
import { Observable } from 'rxjs';
/**
* Observable that listens to a gRPC stream and emits EnginePayload objects.
* Pass a factory function that returns a client and a stream.
*/
export class GowsEventStreamObservable extends Observable<EnginePayload> {
_client: grpc.Client;
CLIENT_CLOSE_TIMEOUT = 1_000;
constructor(
private logger: Logger,
factory: () => {
client: grpc.Client;
stream: grpc.ClientReadableStream<messages.EventJson>;
},
) {
super((subscriber) => {
this.logger.debug('Creating grpc client and stream...');
const { client, stream } = factory();
this._client = client;
stream.on('data', (raw) => {
const obj = raw.toObject();
obj.data = JSON.parse(obj.data);
subscriber?.next(obj);
});
stream.on('end', (...args) => {
this.logger.debug('Stream ended', args);
subscriber?.complete();
subscriber = null;
});
stream.on('error', async (err: any) => {
const CLIENT_CANCELLED_CODE = grpc.status.CANCELLED;
if (err.code === CLIENT_CANCELLED_CODE) {
this.logger.debug('Stream cancelled by client');
return;
}
this.logger.error(err, 'Stream error');
// Give some time to node event loop to process the error
await sleep(100);
subscriber?.error(err);
subscriber = null;
});
return async () => {
this.logger.debug('Closing stream client...');
client.close();
await sleep(this.CLIENT_CLOSE_TIMEOUT);
this.logger.debug('Stream client closed');
this.logger.debug('Cancelling stream...');
stream.cancel();
this.logger.debug('Stream cancelled');
};
});
}
get client(): Omit<grpc.Client, 'close'> {
return this._client;
}
}
+100
View File
@@ -0,0 +1,100 @@
import { sleep, waitUntil } from '@waha/utils/promiseTimeout';
import { spawn } from 'child_process';
import { Logger } from 'pino';
export class GowsSubprocess {
private checkIntervalMs: number = 100;
private readyDelayMs: number = 1_000;
private readyText = 'gRPC server started!';
private child: any;
private ready: boolean = false;
constructor(
private logger: Logger,
readonly path: string,
readonly socket: string,
) {}
start(onExit: (code: number) => void) {
this.logger.info('Starting GOWS subprocess...');
this.logger.debug(`GOWS path '${this.path}', socket: '${this.socket}'...`);
this.child = spawn(this.path, ['--socket', this.socket], {
detached: true,
});
this.logger.debug(`GOWS started with PID: ${this.child.pid}`);
this.child.on('close', async (code, singal) => {
const msg = code
? `GOWS subprocess closed with code ${code}`
: `GOWS subprocess closed by signal ${singal}`;
this.logger.debug(msg);
onExit(code);
});
this.child.on('error', (err) => {
this.logger.error(`GOWS subprocess error: ${err}`);
});
this.child.stderr.setEncoding('utf8');
this.child.stderr.on('data', (data) => {
this.logger.error(data);
});
this.child.stdout.setEncoding('utf8');
this.child.stdout.on('data', async (data) => {
// remove empty line at the end, split by \n
const lines = data.trim().split('\n');
lines.forEach((line) => this.log(line));
});
this.listenReady();
}
listenReady() {
this.child.stdout.on('data', async (data) => {
if (this.ready) {
return;
}
if (!data.includes(this.readyText)) {
return;
}
await sleep(this.readyDelayMs);
this.ready = true;
this.logger.info('GOWS is ready');
});
}
async waitWhenReady(timeout: number) {
const started = await waitUntil(
async () => this.ready,
this.checkIntervalMs,
timeout,
);
if (!started) {
const msg = 'GOWS did not start after 10 seconds';
this.logger.error(msg);
throw new Error(msg);
}
}
async stop() {
this.logger.info('Stopping GOWS subprocess...');
this.child?.kill('SIGTERM');
this.logger.info('GOWS subprocess stopped');
}
private log(msg) {
if (msg.startsWith('ERROR | ')) {
this.logger.error(msg.slice(8));
} else if (msg.startsWith('WARN | ')) {
this.logger.warn(msg.slice(7));
} else if (msg.startsWith('INFO | ')) {
this.logger.info(msg.slice(7));
} else if (msg.startsWith('DEBUG | ')) {
this.logger.debug(msg.slice(8));
} else if (msg.startsWith('TRACE | ')) {
this.logger.trace(msg.slice(8));
} else {
this.logger.info(msg);
}
}
}
+191
View File
@@ -0,0 +1,191 @@
import {
GOWSGroupParticipant,
GroupInfoEvent,
GroupInfoFull,
JoinedGroupEvent,
} from '@waha/core/engines/gows/types.group';
import { toJID } from '@waha/core/utils/jids';
import {
GroupId,
GroupInfo,
GroupParticipant,
GroupParticipantRole,
} from '@waha/structures/groups.dto';
import {
GroupParticipantType,
GroupV2JoinEvent,
GroupV2LeaveEvent,
GroupV2ParticipantsEvent,
GroupV2UpdateEvent,
} from '@waha/structures/groups.events.dto';
import { MeInfo } from '@waha/structures/sessions.dto';
export function ToGroupV2JoinEvent(event: JoinedGroupEvent): GroupV2JoinEvent {
return {
timestamp: Date.now(),
group: ToGroupInfo(event),
_data: event,
};
}
export function ToGroupV2LeaveEvent(
me: MeInfo | null,
event: GroupInfoEvent,
): GroupV2LeaveEvent | null {
if (!event.Leave || event.Leave.length === 0) {
return null;
}
if (!me?.id) {
return null;
}
const jid = toJID(me.id);
const left = event.Leave;
if (!left.includes(jid)) {
return null;
}
return {
timestamp: Date.now(),
group: {
id: event.JID,
},
_data: event,
};
}
export function ToGroupV2UpdateEvent(
event: GroupInfoEvent,
): GroupV2UpdateEvent | null {
const group = ToGroupInfoPartial(event);
if (!group) {
return null;
}
return {
timestamp: Date.now(),
group: group,
_data: event,
};
}
export function ToGroupV2ParticipantsEvents(
event: GroupInfoEvent,
): GroupV2ParticipantsEvent[] {
const id: GroupId = {
id: event.JID,
};
const events: GroupV2ParticipantsEvent[] = [];
const maps = [
{ participants: event.Leave, type: GroupParticipantType.LEAVE },
{ participants: event.Join, type: GroupParticipantType.JOIN },
{ participants: event.Promote, type: GroupParticipantType.PROMOTE },
{ participants: event.Demote, type: GroupParticipantType.DEMOTE },
];
for (const map of maps) {
const participants = getParticipants(map.participants, map.type);
if (participants) {
events.push({
group: id,
type: map.type,
timestamp: Date.now(),
participants: participants,
_data: event,
});
}
}
return events;
}
function getParticipants(jids: string[] | null, type: GroupParticipantType) {
if (!jids) {
return;
}
let role: GroupParticipantRole;
switch (type) {
case GroupParticipantType.JOIN:
role = GroupParticipantRole.PARTICIPANT;
break;
case GroupParticipantType.LEAVE:
role = GroupParticipantRole.LEFT;
break;
case GroupParticipantType.PROMOTE:
role = GroupParticipantRole.ADMIN;
break;
case GroupParticipantType.DEMOTE:
role = GroupParticipantRole.PARTICIPANT;
break;
}
const participants: GroupParticipant[] = [];
for (const jid of jids) {
participants.push({
id: jid,
role: role,
});
}
return participants;
}
function ToGroupInfo(group: GroupInfoFull): GroupInfo {
const participants: GroupParticipant[] = extractParticipants(
group.Participants,
);
return {
id: group.JID,
subject: group.Name,
description: group.Topic,
invite: undefined,
participants: participants,
membersCanAddNewMember: group.MemberAddMode === 'all_member_add',
membersCanSendMessages: !!group.IsAnnounce,
newMembersApprovalRequired: group.IsJoinApprovalRequired,
};
}
function ToGroupInfoPartial(group: GroupInfoEvent): GroupInfo | null {
const announce: boolean | undefined = group.Announce?.IsAnnounce;
let membersCanSendMessages: boolean | undefined;
// revert announce or undefined
if (announce === undefined) {
membersCanSendMessages = undefined;
} else {
membersCanSendMessages = !announce;
}
const info = {
id: group.JID,
subject: group.Name?.Name,
description: group.Topic?.Topic,
invite: group.NewInviteLink || undefined,
membersCanAddNewMember: undefined,
membersCanSendMessages: membersCanSendMessages,
newMembersApprovalRequired:
group.MembershipApprovalMode?.IsJoinApprovalRequired,
participants: undefined,
};
// check if all fields are undefined but id - return null
for (const key in info) {
if (key !== 'id' && info[key] !== undefined) {
return info;
}
}
return null;
}
function extractParticipants(
participants: GOWSGroupParticipant[],
): GroupParticipant[] {
const result: GroupParticipant[] = [];
for (const participant of participants) {
let role: GroupParticipantRole;
if (participant.IsSuperAdmin) {
role = GroupParticipantRole.SUPERADMIN;
} else if (participant.IsAdmin) {
role = GroupParticipantRole.ADMIN;
} else {
role = GroupParticipantRole.PARTICIPANT;
}
result.push({
id: participant.JID,
role: role,
});
}
return result;
}
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
+42
View File
@@ -0,0 +1,42 @@
import { messages } from '@waha/core/engines/gows/grpc/gows';
import { MessageStatus } from '@waha/core/engines/gows/types';
import { WAMessageAck } from '@waha/structures/enums.dto';
export function optional(value: any, type) {
if (value === null || value === undefined) {
return null;
}
return new type({ value: value });
}
export function parseJson(value: messages.Json | { data?: string }) {
if (value instanceof messages.Json) {
value = value.toObject();
}
if (value.data === undefined) {
return undefined;
}
return JSON.parse(value.data);
}
export function parseJsonList(value: messages.JsonList) {
return value.toObject().elements.map((value) => parseJson(value));
}
export function statusToAck(status: MessageStatus): WAMessageAck | null {
switch (status) {
case MessageStatus.Error:
return WAMessageAck.ERROR;
case MessageStatus.Pending:
return WAMessageAck.PENDING;
case MessageStatus.ServerAck:
return WAMessageAck.SERVER;
case MessageStatus.DeliveryAck:
return WAMessageAck.DEVICE;
case MessageStatus.Read:
return WAMessageAck.READ;
case MessageStatus.Played:
return WAMessageAck.PLAYED;
}
return null;
}
File diff suppressed because it is too large. Load diff
+4
View File
@@ -0,0 +1,4 @@
export interface GowsAuth {
address(): string;
dialect(): string;
}
@@ -0,0 +1,26 @@
import * as path from 'node:path';
import { DataStore } from '@waha/core/abc/DataStore';
import { GowsAuthSimple } from '@waha/core/engines/gows/store/GowsAuthSimple';
import { LocalStore } from '@waha/core/storage/LocalStore';
import { GowsAuth } from './GowsAuth';
export class GowsAuthFactoryCore {
buildAuth(store: DataStore, name: string): Promise<GowsAuth> {
if (store instanceof LocalStore) return this.buildSqlite3(store, name);
throw new Error(`Unsupported store type '${store.constructor.name}'`);
}
protected async buildSqlite3(
store: LocalStore,
name: string,
): Promise<GowsAuth> {
await store.init(name);
const authFolder = store.getSessionDirectory(name);
// resolve path
const authFolderFullPath = path.resolve(authFolder);
const connection = `file:${authFolderFullPath}/gows.db`;
return new GowsAuthSimple(connection, 'sqlite3');
}
}
@@ -0,0 +1,19 @@
import { GowsAuth } from '@waha/core/engines/gows/store/GowsAuth';
export class GowsAuthSimple implements GowsAuth {
private _address: string;
private _dialect: string;
constructor(address: string, dialect: string) {
this._address = address;
this._dialect = dialect;
}
address() {
return this._address;
}
dialect(): string {
return this._dialect;
}
}
+108
View File
@@ -0,0 +1,108 @@
// JID just a string alias, we dump it as String in golang
type JID = string;
interface GroupName {
Name: string;
NameSetAt: string; // ISO string format for time
NameSetBy: JID;
}
interface GroupTopic {
Topic: string;
TopicID: string;
TopicSetAt: string;
TopicSetBy: JID;
TopicDeleted: boolean;
}
interface GroupLocked {
IsLocked: boolean;
}
interface GroupAnnounce {
IsAnnounce: boolean;
AnnounceVersionID: string;
}
interface GroupEphemeral {
IsEphemeral: boolean;
DisappearingTimer: number;
}
interface GroupMembershipApprovalMode {
IsJoinApprovalRequired: boolean;
}
interface GroupDelete {
Deleted: boolean;
DeleteReason: string;
}
interface GroupLinkTarget {
JID: JID;
GroupName: GroupName;
GroupIsDefaultSub: GroupIsDefaultSub;
}
interface GroupLinkChange {
Type: string; // Enum in Go: GroupLinkChangeType
UnlinkReason: string; // Enum in Go: GroupUnlinkReason
Group: GroupLinkTarget;
}
interface GroupIsDefaultSub {
IsDefaultSubGroup: boolean;
}
export interface GroupInfoEvent {
JID: JID;
Notify: string;
Sender?: JID;
Timestamp: string;
Name?: GroupName;
Topic?: GroupTopic;
Locked?: GroupLocked;
Announce?: GroupAnnounce;
Ephemeral?: GroupEphemeral;
MembershipApprovalMode?: GroupMembershipApprovalMode;
Delete?: GroupDelete;
Link?: GroupLinkChange;
Unlink?: GroupLinkChange;
NewInviteLink?: string;
PrevParticipantVersionID: string;
ParticipantVersionID: string;
JoinReason: string;
Join: JID[];
Leave: JID[];
Promote: JID[];
Demote: JID[];
}
export interface GOWSGroupParticipant {
JID: JID;
IsAdmin: boolean;
IsSuperAdmin: boolean;
}
export interface GroupInfoFull {
JID: JID;
OwnerJID: JID;
Name: string; // Name has additional properties here
Topic: string; // Topic has additional properties here
IsLocked: boolean; // specifies whether the group info can only be edited by admins.
IsAnnounce: boolean; // specifies whether only admins can send messages in the group
Participants: GOWSGroupParticipant[];
MemberAddMode: string; // all_member_add | admin_add
IsJoinApprovalRequired: boolean;
}
export interface JoinedGroupEvent extends GroupInfoFull {
Reason: string;
Type: string;
}
+41
View File
@@ -0,0 +1,41 @@
export interface Presence {
From: string;
Unavailable: boolean;
LastSeen: string;
}
export enum ChatPresenceState {
COMPOSING = 'composing',
PAUSED = 'paused',
}
export enum ChatPresenceMedia {
TEXT = '',
AUDIO = 'audio',
}
export interface ChatPresence {
Chat: string;
Sender: string;
IsFromMe: boolean;
IsGroup: boolean;
State: ChatPresenceState;
Media: ChatPresenceMedia;
}
/**
* StatusError Status = 0
* StatusPending Status = 1
* StatusServerAck Status = 2
* StatusDeliveryAck Status = 3
* StatusRead Status = 4
* StatusPlayed Status = 5
*/
export enum MessageStatus {
Error = 0,
Pending = 1,
ServerAck = 2,
DeliveryAck = 3,
Read = 4,
Played = 5,
}
@@ -11,7 +11,7 @@ export class NowebAuthFactoryCore {
protected async buildLocalAuth(store: LocalStore, name: string) {
await store.init(name);
const authFolder = store.getSessionDirectory(name);
const { state, saveCreds } = await useMultiFileAuthState(authFolder);
return { state, saveCreds };
const { state, saveCreds, close } = await useMultiFileAuthState(authFolder);
return { state, saveCreds, close };
}
}
+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;
},
},
);
}
}
+143
View File
@@ -0,0 +1,143 @@
import { GroupMetadata, jidNormalizedUser } from '@adiwajshing/baileys';
import type { Contact } from '@adiwajshing/baileys/lib/Types/Contact';
import {
GroupParticipant as NOWEBGroupParticipant,
ParticipantAction,
} from '@adiwajshing/baileys/lib/Types/GroupMetadata';
import { getGroupInviteLink } from '@waha/core/abc/session.abc';
import { toCusFormat } from '@waha/core/engines/noweb/session.noweb.core';
import {
GroupInfo,
GroupParticipant,
GroupParticipantRole,
} from '@waha/structures/groups.dto';
import {
GroupParticipantType,
GroupV2JoinEvent,
GroupV2LeaveEvent,
GroupV2ParticipantsEvent,
GroupV2UpdateEvent,
} from '@waha/structures/groups.events.dto';
export function ToGroupInfo(group: Partial<GroupMetadata>): GroupInfo {
let participants: GroupParticipant[] = undefined;
if (group.participants && group.participants.length > 0) {
participants = group.participants.map(ToGroupParticipant);
}
return {
id: group.id,
subject: group.subject,
description: group.desc,
invite: group.inviteCode ? getGroupInviteLink(group.inviteCode) : undefined,
participants: participants,
membersCanAddNewMember: group.memberAddMode,
membersCanSendMessages: group.announce,
newMembersApprovalRequired: group.joinApprovalMode,
};
}
export function ToGroupV2JoinEvent(group: GroupMetadata): GroupV2JoinEvent {
return {
timestamp: Date.now(),
group: ToGroupInfo(group),
_data: group,
};
}
export function ToGroupParticipant(
participant: NOWEBGroupParticipant,
): GroupParticipant {
let role: GroupParticipantRole = GroupParticipantRole.PARTICIPANT;
if (participant.admin === 'admin') {
role = GroupParticipantRole.ADMIN;
} else if (participant.admin === 'superadmin') {
role = GroupParticipantRole.SUPERADMIN;
}
return {
id: toCusFormat(participant.id),
role: role,
};
}
interface GroupParticipantUpdate {
id: string;
author: string;
participants: string[];
action: ParticipantAction;
}
export function ToGroupV2Participants(
update: GroupParticipantUpdate,
): GroupV2ParticipantsEvent {
let role: GroupParticipantRole;
let type: GroupParticipantType;
switch (update.action) {
case 'add':
role = GroupParticipantRole.PARTICIPANT;
type = GroupParticipantType.JOIN;
break;
case 'remove':
role = GroupParticipantRole.LEFT;
type = GroupParticipantType.LEAVE;
break;
case 'promote':
role = GroupParticipantRole.ADMIN;
type = GroupParticipantType.PROMOTE;
break;
case 'demote':
role = GroupParticipantRole.ADMIN;
type = GroupParticipantType.DEMOTE;
break;
}
const participants: GroupParticipant[] = update.participants.map((id) => {
return {
id: toCusFormat(id),
role: role,
};
});
return {
group: {
id: toCusFormat(update.id),
},
type: type,
timestamp: Date.now(),
participants: participants,
_data: update,
};
}
export function ToGroupV2UpdateEvent(
group: Partial<GroupMetadata>,
): GroupV2UpdateEvent {
return {
timestamp: Date.now(),
group: ToGroupInfo(group),
_data: group,
};
}
export function ToGroupV2LeaveEvent(
me: Contact,
update: GroupParticipantUpdate,
): GroupV2LeaveEvent | null {
if (update.action !== 'remove') {
return null;
}
if (!me) {
return null;
}
const meId = jidNormalizedUser(me.id);
if (!update.participants.includes(meId)) {
return null;
}
return {
timestamp: Date.now(),
group: {
id: toCusFormat(update.id),
},
_data: update,
};
}
File diff suppressed because it is too large. Load diff
@@ -6,7 +6,10 @@ export interface IChatRepository {
getAllByIds(ids: string[]): Promise<Chat[]>;
getAllWithMessages(pagination: PaginationParams): Promise<Chat[]>;
getAllWithMessages(
pagination: PaginationParams,
broadcast: boolean,
): Promise<Chat[]>;
getById(id: string): Promise<Chat | null>;
@@ -15,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>>;
}
@@ -0,0 +1,14 @@
import { GroupMetadata } from '@adiwajshing/baileys';
import { PaginationParams } from '@waha/structures/pagination.dto';
export interface IGroupRepository {
getAll(pagination?: PaginationParams): Promise<GroupMetadata[]>;
getById(id: string): Promise<GroupMetadata | null>;
deleteAll(): Promise<void>;
deleteById(id: string): Promise<void>;
save(group: GroupMetadata): Promise<void>;
}
@@ -8,6 +8,8 @@ export interface IMessagesRepository {
upsertOne(message: any): Promise<void>;
getById(id: string): Promise<any | null>;
getAllByJid(
jid: string,
filter: GetChatMessagesFilter,
+3 -18
View File
@@ -1,5 +1,4 @@
import { WAMessage } from '@adiwajshing/baileys';
import { LabelAssociation } from '@adiwajshing/baileys/lib/Types/LabelAssociation';
import { IGroupRepository } from '@waha/core/engines/noweb/store/IGroupRepository';
import { ILabelAssociationRepository } from '@waha/core/engines/noweb/store/ILabelAssociationsRepository';
import { ILabelsRepository } from '@waha/core/engines/noweb/store/ILabelsRepository';
@@ -16,25 +15,11 @@ export abstract class INowebStorage {
abstract getChatRepository(): IChatRepository;
abstract getGroupRepository(): IGroupRepository;
abstract getMessagesRepository(): IMessagesRepository;
abstract getLabelsRepository(): ILabelsRepository;
abstract getLabelAssociationRepository(): ILabelAssociationRepository;
protected getMessagesMetadata(): Map<string, any> {
return new Map()
.set('jid', (msg: WAMessage) => msg.key.remoteJid)
.set('id', (msg: WAMessage) => msg.key.id)
.set('messageTimestamp', (msg: WAMessage) => msg.messageTimestamp);
}
protected getLabelAssociationMetadata() {
return new Map().set(
'id',
(a: LabelAssociation) =>
// @ts-ignore
`${a.type}_${a.labelId}_${a.chatId}_${a.messageId}`,
);
}
}
+8 -1
View File
@@ -4,6 +4,7 @@ import {
Contact,
proto,
} from '@adiwajshing/baileys';
import { GroupMetadata } from '@adiwajshing/baileys/lib/Types/GroupMetadata';
import { Label } from '@adiwajshing/baileys/lib/Types/Label';
import { GetChatMessagesFilter } from '@waha/structures/chats.dto';
import { PaginationParams } from '@waha/structures/pagination.dto';
@@ -27,7 +28,9 @@ export interface INowebStore {
getMessageById(chatId: string, messageId: string): Promise<any>;
getChats(pagination: PaginationParams): Promise<Chat[]>;
getChats(pagination: PaginationParams, broadcast: boolean): Promise<Chat[]>;
getChat(jid: string): Promise<Chat | null>;
getContacts(pagination: PaginationParams): Promise<Contact[]>;
@@ -40,4 +43,8 @@ export interface INowebStore {
getChatsByLabelId(labelId: string): Promise<Chat[]>;
getChatLabels(chatId: string): Promise<Label[]>;
getGroups(pagination: PaginationParams): Promise<GroupMetadata[]>;
resetGroupsCache(): void;
}
@@ -1,15 +1,24 @@
import { Chat, Contact, makeInMemoryStore, proto } from '@adiwajshing/baileys';
import makeWASocket, {
Chat,
Contact,
GroupMetadata,
proto,
} from '@adiwajshing/baileys';
import { Label } from '@adiwajshing/baileys/lib/Types/Label';
import { BadRequestException } from '@nestjs/common';
import { GetChatMessagesFilter } from '@waha/structures/chats.dto';
import { PaginationParams } from '@waha/structures/pagination.dto';
import { PaginatorInMemory } from '@waha/utils/Paginator';
import { INowebStore } from './INowebStore';
import makeInMemoryStore from './memory/make-in-memory-store';
// eslint-disable-next-line @typescript-eslint/no-var-requires
const logger = require('pino')();
export class NowebInMemoryStore implements INowebStore {
private socket: ReturnType<typeof makeWASocket>;
private store: ReturnType<typeof makeInMemoryStore>;
errorMessage =
'Enable NOWEB store "config.noweb.store.enabled=True" and "config.noweb.store.full_sync=True" when starting a new session. ' +
@@ -17,6 +26,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> {
@@ -33,6 +46,7 @@ export class NowebInMemoryStore implements INowebStore {
bind(ev: any, socket: any) {
this.store.bind(ev);
this.socket = socket;
}
loadMessage(jid: string, id: string): Promise<proto.IWebMessageInfo> {
@@ -51,10 +65,14 @@ export class NowebInMemoryStore implements INowebStore {
throw new BadRequestException(this.errorMessage);
}
getChats(pagination: PaginationParams): Promise<Chat[]> {
getChats(pagination: PaginationParams, broadcast: boolean): Promise<Chat[]> {
throw new BadRequestException(this.errorMessage);
}
getChat(jid: string): Promise<Chat | null> {
return null;
}
getContacts(pagination: PaginationParams): Promise<Contact[]> {
throw new BadRequestException(this.errorMessage);
}
@@ -78,4 +96,15 @@ export class NowebInMemoryStore implements INowebStore {
getChatLabels(chatId: string): Promise<Label[]> {
throw new BadRequestException(this.errorMessage);
}
async getGroups(pagination: PaginationParams): Promise<GroupMetadata[]> {
const response = await this.socket?.groupFetchAllParticipating();
const groups: any[] = Object.values(response);
const paginator = new PaginatorInMemory(pagination);
return paginator.apply(groups);
}
resetGroupsCache() {
return;
}
}
@@ -1,27 +1,34 @@
import {
import makeWASocket, {
areJidsSameUser,
BaileysEventEmitter,
Chat,
ChatUpdate,
Contact,
GroupParticipant,
isRealMessage,
jidNormalizedUser,
ParticipantAction,
proto,
updateMessageWithReaction,
updateMessageWithReceipt,
} from '@adiwajshing/baileys';
import { GroupMetadata } from '@adiwajshing/baileys/lib/Types/GroupMetadata';
import { Label } from '@adiwajshing/baileys/lib/Types/Label';
import {
LabelAssociation,
LabelAssociationType,
} from '@adiwajshing/baileys/lib/Types/LabelAssociation';
import { IGroupRepository } from '@waha/core/engines/noweb/store/IGroupRepository';
import { ILabelAssociationRepository } from '@waha/core/engines/noweb/store/ILabelAssociationsRepository';
import { ILabelsRepository } from '@waha/core/engines/noweb/store/ILabelsRepository';
import { GetChatMessagesFilter } from '@waha/structures/chats.dto';
import { PaginationParams, SortOrder } from '@waha/structures/pagination.dto';
import { DefaultMap } from '@waha/utils/DefaultMap';
import { sleep, waitUntil } from '@waha/utils/promiseTimeout';
import * as lodash from 'lodash';
import { toNumber } from 'lodash';
import { Logger } from 'pino';
import { toJID } from '../session.noweb.core';
import { IChatRepository } from './IChatRepository';
import { IContactRepository } from './IContactRepository';
import { IMessagesRepository } from './IMessagesRepository';
@@ -31,15 +38,27 @@ import { INowebStore } from './INowebStore';
// eslint-disable-next-line @typescript-eslint/no-var-requires
const AsyncLock = require('async-lock');
type ms = number;
const HOUR: ms = 60 * 60 * 1000;
export class NowebPersistentStore implements INowebStore {
private socket: any;
private socket: ReturnType<typeof makeWASocket>;
private chatRepo: IChatRepository;
private groupRepo: IGroupRepository;
private contactRepo: IContactRepository;
private messagesRepo: IMessagesRepository;
private labelsRepo: ILabelsRepository;
private labelAssociationsRepo: ILabelAssociationRepository;
public presences: any;
private lock: any;
private groupsFetchLock: any = new AsyncLock({
maxPending: Infinity,
maxExecutionTime: 60_000,
});
private lastTimeGroupUpdate: Date = new Date(0);
private lastTimeGroupFetch: Date = new Date(0);
private GROUP_METADATA_CACHE_TIME = 24 * HOUR;
constructor(
private logger: Logger,
@@ -47,6 +66,7 @@ export class NowebPersistentStore implements INowebStore {
) {
this.socket = null;
this.chatRepo = storage.getChatRepository();
this.groupRepo = storage.getGroupRepository();
this.contactRepo = storage.getContactsRepository();
this.messagesRepo = storage.getMessagesRepository();
this.labelsRepo = storage.getLabelsRepository();
@@ -88,6 +108,19 @@ export class NowebPersistentStore implements INowebStore {
ev.on('chats.delete', (data) =>
this.withLock('chats', () => this.onChatDelete(data)),
);
// Groups
ev.on('groups.upsert', (data) =>
this.withLock('groups', () => this.onGroupUpsert(data)),
);
ev.on('groups.update', (data) =>
this.withLock('groups', () => this.onGroupUpdate(data)),
);
ev.on('group-participants.update', (data) =>
this.withLock(`group-${data.id}`, () =>
this.onGroupParticipantsUpdate(data),
),
);
// Contacts
ev.on('contacts.upsert', (data) =>
this.withLock('contacts', () => this.onContactsUpsert(data)),
@@ -161,9 +194,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);
@@ -192,17 +242,97 @@ export class NowebPersistentStore implements INowebStore {
private async onChatUpsert(chats: Chat[]) {
for (const chat of chats) {
delete chat['messages'];
chat.conversationTimestamp = toNumber(chat.conversationTimestamp);
await this.chatRepo.save(chat);
chat.conversationTimestamp = toNumber(chat.conversationTimestamp) || null;
}
await this.chatRepo.upsertMany(chats);
this.logger.info(`store sync - '${chats.length}' synced chats`);
}
private async onGroupUpsert(groups: GroupMetadata[]) {
for (const group of groups) {
await this.groupRepo.save(group);
}
this.logger.info(`store sync - '${groups.length}' synced groups`);
}
private async onGroupUpdate(groups: Partial<GroupMetadata>[]) {
for (const update of groups) {
let group = await this.groupRepo.getById(update.id);
group = Object.assign(group || {}, update) as GroupMetadata;
await this.groupRepo.save(group);
}
this.logger.info(`store sync - '${groups.length}' updated groups`);
this.lastTimeGroupUpdate = new Date();
}
private async onGroupParticipantsUpdate(data) {
const id: string = data.id;
const participants: string[] = data.participants;
const action: ParticipantAction = data.action;
if (action == 'remove') {
// Remove the group if the current user is removed
const myJid = this.socket?.authState?.creds?.me?.id;
const participantsIncludesMe = lodash.find(participants, (p) =>
areJidsSameUser(p, myJid),
);
if (participantsIncludesMe) {
await this.groupRepo.deleteById(id);
return;
}
}
let group = await this.groupRepo.getById(id);
if (!group) {
group = { id: id, participants: [] } as GroupMetadata;
}
if (!group.participants) {
group.participants = [];
}
const participantsById = new DefaultMap<string, GroupParticipant>((key) => {
return { id: key, admin: null } as GroupParticipant;
});
for (const participant of group.participants) {
participantsById.set(participant.id, participant);
}
for (const participant of participants) {
this.participantUpdate(participantsById, participant, action);
}
group.participants = Array.from(participantsById.values());
await this.groupRepo.save(group);
}
private participantUpdate(
participantsById: DefaultMap<string, GroupParticipant>,
participant: string,
action: ParticipantAction,
) {
switch (action) {
case 'add':
// if there's no participant - add it (by id)
participantsById.get(participant);
break;
case 'remove':
// remove the participant (by id)
participantsById.delete(participant);
break;
case 'promote':
// set admin: admin
participantsById.get(participant).admin = 'admin';
break;
case 'demote':
participantsById.get(participant).admin = null;
break;
}
this.logger.info(`history sync - '${chats.length}' synced chats`);
}
private async onChatUpdate(updates: ChatUpdate[]) {
for (const update of updates) {
const chat = (await this.chatRepo.getById(update.id)) || ({} as Chat);
Object.assign(chat, update);
chat.conversationTimestamp = toNumber(chat.conversationTimestamp);
chat.conversationTimestamp = toNumber(chat.conversationTimestamp) || null;
delete chat['messages'];
await this.chatRepo.save(chat);
}
@@ -220,20 +350,24 @@ 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>[]) {
for (const update of updates) {
const contact = await this.contactRepo.getById(update.id);
let contact = await this.contactRepo.getById(update.id);
if (!contact) {
this.logger.warn(
@@ -241,7 +375,7 @@ export class NowebPersistentStore implements INowebStore {
update,
)}'`,
);
continue;
contact = {} as Contact;
// TODO: Find contact by hash if not found
// find contact by attrs.hash, when user is not saved as a contact
// check the in-memory for that
@@ -250,7 +384,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;
@@ -317,7 +456,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;
}
@@ -338,10 +482,49 @@ export class NowebPersistentStore implements INowebStore {
return this.messagesRepo.getByJidById(chatId, messageId);
}
getChats(pagination: PaginationParams): Promise<Chat[]> {
getChats(pagination: PaginationParams, broadcast: boolean): Promise<Chat[]> {
pagination.sortBy ||= 'conversationTimestamp';
pagination.sortOrder ||= SortOrder.DESC;
return this.chatRepo.getAllWithMessages(pagination);
return this.chatRepo.getAllWithMessages(pagination, broadcast);
}
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;
}
private async fetchGroups() {
await this.groupsFetchLock.acquire('groups-fetch', async () => {
if (!this.shouldFetchGroup()) {
// Update has been done by another request
return;
}
const lastTimeGroupUpdate = this.lastTimeGroupUpdate;
await this.groupRepo.deleteAll();
await this.socket?.groupFetchAllParticipating();
// Wait until the groups update is done
await waitUntil(
async () => this.lastTimeGroupUpdate > lastTimeGroupUpdate,
100,
5_000,
);
this.lastTimeGroupFetch = new Date();
});
}
resetGroupsCache() {
this.lastTimeGroupFetch = new Date(0);
}
async getGroups(pagination: PaginationParams): Promise<GroupMetadata[]> {
if (this.shouldFetchGroup()) {
await this.fetchGroups();
}
return this.groupRepo.getAll(pagination);
}
getContactById(jid) {
@@ -1,9 +1,19 @@
import { LocalStore } from '@waha/core/storage/LocalStore';
import { DataStore } from '../../../abc/DataStore';
import { INowebStorage } from './INowebStorage';
import { Sqlite3Storage } from './sqlite3/Sqlite3Storage';
export class NowebStorageFactoryCore {
createStorage(store: DataStore, name: string): INowebStorage {
return new Sqlite3Storage(':memory:');
if (store instanceof LocalStore) {
return this.buildStorageSqlite3(store, name);
}
throw new Error(`Unsupported store type '${store.constructor.name}'`);
}
private buildStorageSqlite3(store: LocalStore, name: string) {
const filePath = store.getFilePath(name, 'store.sqlite3');
return new Sqlite3Storage(filePath);
}
}
-58
View File
@@ -1,58 +0,0 @@
import { Field, Index, Schema } from '@waha/core/storage/sqlite3/Schema';
export const NOWEB_STORE_SCHEMA = [
new Schema(
'contacts',
[new Field('id', 'TEXT'), new Field('data', 'TEXT')],
[new Index('contacts_id_index', ['id'])],
),
new Schema(
'chats',
[
new Field('id', 'TEXT'),
new Field('conversationTimestamp', 'INTEGER'),
new Field('data', 'TEXT'),
],
[
new Index('chats_id_index', ['id']),
new Index('chats_conversationTimestamp_index', ['conversationTimestamp']),
],
),
new Schema(
'messages',
[
new Field('jid', 'TEXT'),
new Field('id', 'TEXT'),
new Field('messageTimestamp', 'INTEGER'),
new Field('data', 'TEXT'),
],
[
new Index('messages_id_index', ['id']),
new Index('messages_jid_id_index', ['jid', 'id']),
new Index('messages_jid_timestamp_index', ['jid', 'messageTimestamp']),
new Index('timestamp_index', ['messageTimestamp']),
],
),
new Schema(
'labels',
[new Field('id', 'TEXT'), new Field('data', 'TEXT')],
[new Index('labels_id_index', ['id'])],
),
new Schema(
'labelAssociations',
[
new Field('id', 'TEXT'),
new Field('type', 'TEXT'),
new Field('labelId', 'TEXT'),
new Field('chatId', 'TEXT'),
new Field('messageId', 'TEXT'),
new Field('data', 'TEXT'),
],
[
new Index('label_assoc_id_index', ['id']),
new Index('label_assoc_type_label_index', ['type', 'labelId']),
new Index('label_assoc_type_chat_index', ['type', 'chatId']),
new Index('label_assoc_type_message_index', ['type', 'messageId']),
],
),
];
@@ -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
@@ -0,0 +1,14 @@
import { WAMessage } from '@adiwajshing/baileys';
import { LabelAssociation } from '@adiwajshing/baileys/lib/Types/LabelAssociation';
export const NowebMessagesMetadata = new Map()
.set('jid', (msg: WAMessage) => msg.key.remoteJid)
.set('id', (msg: WAMessage) => msg.key.id)
.set('messageTimestamp', (msg: WAMessage) => msg.messageTimestamp);
export const NowebLabelAssociationsMetadata = new Map().set(
'id',
(a: LabelAssociation) =>
// @ts-ignore
`${a.type}_${a.labelId}_${a.chatId}_${a.messageId}`,
);
+103
View File
@@ -0,0 +1,103 @@
import { Field, Index, Schema } from '@waha/core/storage/Schema';
export const NowebContactSchema = new Schema(
'contacts',
[new Field('id', 'TEXT'), new Field('data', 'TEXT')],
[new Index('contacts_id_index', ['id'])],
);
export const NowebChatSchema = new Schema(
'chats',
[
new Field('id', 'TEXT'),
new Field('conversationTimestamp', 'INTEGER'),
new Field('data', 'TEXT'),
],
[
new Index('chats_id_index', ['id']),
new Index('chats_conversationTimestamp_index', ['conversationTimestamp']),
],
);
export const NowebGroupsSchema = new Schema(
'groups',
[new Field('id', 'TEXT'), new Field('data', 'TEXT')],
[new Index('groups_id_index', ['id'])],
);
export const NowebMessagesSchema = new Schema(
'messages',
[
new Field('jid', 'TEXT'),
new Field('id', 'TEXT'),
new Field('messageTimestamp', 'INTEGER'),
new Field('data', 'TEXT'),
],
[
new Index('messages_id_index', ['id']),
new Index('messages_jid_id_index', ['jid', 'id']),
new Index('messages_jid_timestamp_index', ['jid', 'messageTimestamp']),
new Index('timestamp_index', ['messageTimestamp']),
],
);
export const NowebLabelsSchema = new Schema(
'labels',
[new Field('id', 'TEXT'), new Field('data', 'TEXT')],
[new Index('labels_id_index', ['id'])],
);
export const NowebLabelAssociationsSchema = new Schema(
'labelAssociations',
[
new Field('id', 'TEXT'),
new Field('type', 'TEXT'),
new Field('labelId', 'TEXT'),
new Field('chatId', 'TEXT'),
new Field('messageId', 'TEXT'),
new Field('data', 'TEXT'),
],
[
new Index('label_assoc_id_index', ['id']),
new Index('label_assoc_type_label_index', ['type', 'labelId']),
new Index('label_assoc_type_chat_index', ['type', 'chatId']),
new Index('label_assoc_type_message_index', ['type', 'messageId']),
],
);
export const NOWEB_STORE_SCHEMA = [
NowebContactSchema,
NowebChatSchema,
NowebGroupsSchema,
NowebMessagesSchema,
NowebLabelsSchema,
NowebLabelAssociationsSchema,
];
export const Migrations: string[] = [
// Contacts
'CREATE TABLE IF NOT EXISTS contacts (id TEXT PRIMARY KEY, data TEXT)',
'CREATE UNIQUE INDEX IF NOT EXISTS contacts_id_index ON contacts (id)',
// Chats
'CREATE TABLE IF NOT EXISTS chats (id TEXT PRIMARY KEY, "conversationTimestamp" INTEGER, data TEXT)',
'CREATE UNIQUE INDEX IF NOT EXISTS chats_id_index ON chats (id)',
'CREATE INDEX IF NOT EXISTS "chats_conversationTimestamp_index" ON chats ("conversationTimestamp")',
// Groups
'CREATE TABLE IF NOT EXISTS groups (id TEXT PRIMARY KEY, data TEXT)',
'CREATE UNIQUE INDEX IF NOT EXISTS groups_id_index ON groups (id)',
// Messages
'CREATE TABLE IF NOT EXISTS messages (jid TEXT, id TEXT, "messageTimestamp" INTEGER, data TEXT)',
'CREATE UNIQUE INDEX IF NOT EXISTS messages_id_index ON messages (id)',
'CREATE INDEX IF NOT EXISTS messages_jid_id_index ON messages (jid, id)',
'CREATE INDEX IF NOT EXISTS messages_jid_timestamp_index ON messages (jid, "messageTimestamp")',
'CREATE INDEX IF NOT EXISTS timestamp_index ON messages ("messageTimestamp")',
// Labels
'CREATE TABLE IF NOT EXISTS labels (id TEXT PRIMARY KEY, data TEXT)',
'CREATE UNIQUE INDEX IF NOT EXISTS labels_id_index ON labels (id)',
// Label associations
'CREATE TABLE IF NOT EXISTS "labelAssociations" (id TEXT PRIMARY KEY, type TEXT, "labelId" TEXT, "chatId" TEXT, "messageId" TEXT, data TEXT)',
'CREATE UNIQUE INDEX IF NOT EXISTS label_assoc_id_index ON "labelAssociations" (id)',
'CREATE INDEX IF NOT EXISTS label_assoc_type_label_index ON "labelAssociations" (type, "labelId")',
'CREATE INDEX IF NOT EXISTS label_assoc_type_chat_index ON "labelAssociations" (type, "chatId")',
'CREATE INDEX IF NOT EXISTS label_assoc_type_message_index ON "labelAssociations" (type, "messageId")',
];
@@ -0,0 +1,23 @@
import { Chat } from '@adiwajshing/baileys';
import { SqlKVRepository } from '@waha/core/storage/sql/SqlKVRepository';
import { PaginationParams } from '@waha/structures/pagination.dto';
export class SqlChatMethods {
constructor(private repository: SqlKVRepository<any>) {}
async getAllWithMessages(
pagination: PaginationParams,
broadcast: boolean,
): Promise<Chat[]> {
// Get chats with conversationTimestamp is not Null
let query = this.repository.select().whereNotNull('conversationTimestamp');
if (!broadcast) {
// filter out chat by id if it ends at @newsletter or @broadcast
query = query
.andWhereNot('id', 'like', '%@broadcast')
.andWhereNot('id', 'like', '%@newsletter');
}
query = this.repository.pagination(query, pagination);
return await this.repository.all(query);
}
}
@@ -0,0 +1,40 @@
import {
LabelAssociation,
LabelAssociationType,
} from '@adiwajshing/baileys/lib/Types/LabelAssociation';
import { SqlKVRepository } from '@waha/core/storage/sql/SqlKVRepository';
export class SqlLabelAssociationsMethods {
constructor(private repository: SqlKVRepository<any>) {}
async deleteOne(association: LabelAssociation): Promise<void> {
await this.repository.deleteBy({
type: association.type,
chatId: association.chatId,
labelId: association.labelId,
// @ts-ignore: messageId doesn't existing in ChatLabelAssociation
messageId: association.messageId || null,
});
}
async deleteByLabelId(labelId: string): Promise<void> {
await this.repository.deleteBy({ labelId: labelId });
}
getAssociationsByLabelId(
labelId: string,
type: LabelAssociationType,
): Promise<LabelAssociation[]> {
return this.repository.getAllBy({
type: type,
labelId: labelId,
});
}
getAssociationsByChatId(chatId: string): Promise<LabelAssociation[]> {
return this.repository.getAllBy({
chatId: chatId,
type: LabelAssociationType.Chat,
});
}
}
@@ -0,0 +1,86 @@
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';
export class SqlMessagesMethods {
constructor(private repository: SqlKVRepository<any>) {}
upsert(messages: any[]): Promise<void> {
return this.repository.upsertMany(messages);
}
async getAllByJid(
jid: string,
filter: GetChatMessagesFilter,
pagination: PaginationParams,
): Promise<any[]> {
let query = this.repository.select();
if (jid !== ALL_JID) {
query = this.repository.select().where({ jid: jid });
}
if (filter['filter.timestamp.lte'] != null) {
query = query.where(
'messageTimestamp',
'<=',
filter['filter.timestamp.lte'],
);
}
if (filter['filter.timestamp.gte'] != null) {
query = query.where(
'messageTimestamp',
'>=',
filter['filter.timestamp.gte'],
);
}
if (filter['filter.fromMe'] != null) {
// filter by data json inside
const [sql, 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 });
}
async updateByJidAndId(
jid: string,
id: string,
update: any,
): Promise<boolean> {
const entity = await this.getByJidById(jid, id);
if (!entity) {
return false;
}
Object.assign(entity, update);
await this.repository.upsertOne(entity);
}
async deleteByJidByIds(jid: string, ids: string[]): Promise<void> {
const query = `DELETE
FROM "${this.repository.table}"
WHERE jid = ?
AND id IN (${ids.map(() => '?').join(', ')})`;
await this.repository.raw(query, [jid, ...ids]);
}
deleteAllByJid(jid: string): Promise<void> {
return this.repository.deleteBy({ jid: jid });
}
}
@@ -1,4 +1,6 @@
import { Chat } from '@adiwajshing/baileys';
import { NowebChatSchema } from '@waha/core/engines/noweb/store/schemas';
import { SqlChatMethods } from '@waha/core/engines/noweb/store/sql/SqlChatMethods';
import { PaginationParams } from '@waha/structures/pagination.dto';
import { KnexPaginator } from '@waha/utils/Paginator';
@@ -15,10 +17,18 @@ export class Sqlite3ChatRepository
{
protected Paginator = ChatPaginator;
async getAllWithMessages(pagination: PaginationParams): Promise<Chat[]> {
// Get chats with conversationTimestamp is not Null
let query = this.select().whereNotNull('conversationTimestamp');
query = this.pagination(query, pagination);
return await this.all(query);
get schema() {
return NowebChatSchema;
}
get methods() {
return new SqlChatMethods(this);
}
getAllWithMessages(
pagination: PaginationParams,
broadcast: boolean,
): Promise<Chat[]> {
return this.methods.getAllWithMessages(pagination, broadcast);
}
}
@@ -1,4 +1,5 @@
import { Contact } from '@adiwajshing/baileys';
import { NowebContactSchema } from '@waha/core/engines/noweb/store/schemas';
import { KnexPaginator } from '@waha/utils/Paginator';
import { IContactRepository } from '../IContactRepository';
@@ -13,4 +14,8 @@ export class Sqlite3ContactRepository
implements IContactRepository
{
protected Paginator = ContactPaginator;
get schema() {
return NowebContactSchema;
}
}
@@ -0,0 +1,21 @@
import { GroupMetadata } from '@adiwajshing/baileys/lib/Types/GroupMetadata';
import { IGroupRepository } from '@waha/core/engines/noweb/store/IGroupRepository';
import { NowebGroupsSchema } from '@waha/core/engines/noweb/store/schemas';
import { KnexPaginator } from '@waha/utils/Paginator';
import { NOWEBSqlite3KVRepository } from './NOWEBSqlite3KVRepository';
class Paginator extends KnexPaginator {
indexes = ['id'];
}
export class Sqlite3GroupRepository
extends NOWEBSqlite3KVRepository<GroupMetadata>
implements IGroupRepository
{
protected Paginator = Paginator;
get schema() {
return NowebGroupsSchema;
}
}
@@ -3,6 +3,9 @@ import {
LabelAssociationType,
} from '@adiwajshing/baileys/lib/Types/LabelAssociation';
import { ILabelAssociationRepository } from '@waha/core/engines/noweb/store/ILabelAssociationsRepository';
import { NowebLabelAssociationsMetadata } from '@waha/core/engines/noweb/store/metadata';
import { NowebLabelAssociationsSchema } from '@waha/core/engines/noweb/store/schemas';
import { SqlLabelAssociationsMethods } from '@waha/core/engines/noweb/store/sql/SqlLabelAssociationsMethods';
import { NOWEBSqlite3KVRepository } from './NOWEBSqlite3KVRepository';
@@ -10,31 +13,34 @@ export class Sqlite3LabelAssociationsRepository
extends NOWEBSqlite3KVRepository<LabelAssociation>
implements ILabelAssociationRepository
{
get schema() {
return NowebLabelAssociationsSchema;
}
get metadata() {
return NowebLabelAssociationsMetadata;
}
get methods() {
return new SqlLabelAssociationsMethods(this);
}
async deleteOne(association: LabelAssociation): Promise<void> {
await this.deleteBy({
type: association.type,
chatId: association.chatId,
labelId: association.labelId,
// @ts-ignore: messageId doesn't existing in ChatLabelAssociation
messageId: association.messageId || null,
});
return this.methods.deleteOne(association);
}
async deleteByLabelId(labelId: string): Promise<void> {
await this.deleteBy({ labelId: labelId });
return this.methods.deleteByLabelId(labelId);
}
getAssociationsByLabelId(
labelId: string,
type: LabelAssociationType,
): Promise<LabelAssociation[]> {
return this.getAllBy({
type: type,
labelId: labelId,
});
return this.methods.getAssociationsByLabelId(labelId, type);
}
getAssociationsByChatId(chatId: string): Promise<LabelAssociation[]> {
return this.getAllBy({ chatId: chatId, type: LabelAssociationType.Chat });
return this.methods.getAssociationsByChatId(chatId);
}
}
@@ -1,8 +1,14 @@
import { Label } from '@adiwajshing/baileys/lib/Types/Label';
import { ILabelsRepository } from '@waha/core/engines/noweb/store/ILabelsRepository';
import { NowebLabelsSchema } from '@waha/core/engines/noweb/store/schemas';
import { NOWEBSqlite3KVRepository } from './NOWEBSqlite3KVRepository';
export class Sqlite3LabelsRepository
extends NOWEBSqlite3KVRepository<Label>
implements ILabelsRepository {}
implements ILabelsRepository
{
get schema() {
return NowebLabelsSchema;
}
}
@@ -1,4 +1,6 @@
import { ALL_JID } from '@waha/core/engines/noweb/session.noweb.core';
import { NowebMessagesMetadata } from '@waha/core/engines/noweb/store/metadata';
import { NowebMessagesSchema } from '@waha/core/engines/noweb/store/schemas';
import { SqlMessagesMethods } from '@waha/core/engines/noweb/store/sql/SqlMessagesMethods';
import { GetChatMessagesFilter } from '@waha/structures/chats.dto';
import { PaginationParams } from '@waha/structures/pagination.dto';
@@ -9,8 +11,20 @@ export class Sqlite3MessagesRepository
extends NOWEBSqlite3KVRepository<any>
implements IMessagesRepository
{
get schema() {
return NowebMessagesSchema;
}
get methods() {
return new SqlMessagesMethods(this);
}
get metadata() {
return NowebMessagesMetadata;
}
upsert(messages: any[]): Promise<void> {
return this.upsertMany(messages);
return this.methods.upsert(messages);
}
async getAllByJid(
@@ -18,36 +32,11 @@ export class Sqlite3MessagesRepository
filter: GetChatMessagesFilter,
pagination: PaginationParams,
): Promise<any[]> {
let query = this.select();
if (jid !== ALL_JID) {
query = this.select().where({ jid: jid });
}
if (filter['filter.timestamp.lte'] != null) {
query = query.where(
'messageTimestamp',
'<=',
filter['filter.timestamp.lte'],
);
}
if (filter['filter.timestamp.gte'] != null) {
query = query.where(
'messageTimestamp',
'>=',
filter['filter.timestamp.gte'],
);
}
if (filter['filter.fromMe'] != null) {
// filter by data->"$.key.fromMe"
query = query.whereRaw("data->'$.key.fromMe' = ?", [
filter['filter.fromMe'] ? 'true' : 'false',
]);
}
query = this.pagination(query, pagination);
return this.all(query);
return this.methods.getAllByJid(jid, filter, pagination);
}
async getByJidById(jid: string, id: string): Promise<any> {
return this.getBy({ jid: jid, id: id });
return this.methods.getByJidById(jid, id);
}
async updateByJidAndId(
@@ -55,23 +44,14 @@ export class Sqlite3MessagesRepository
id: string,
update: any,
): Promise<boolean> {
const entity = await this.getByJidById(jid, id);
if (!entity) {
return false;
}
Object.assign(entity, update);
await this.upsertOne(entity);
return this.methods.updateByJidAndId(jid, id, update);
}
async deleteByJidByIds(jid: string, ids: string[]): Promise<void> {
const query = `DELETE
FROM ${this.table}
WHERE jid = ?
AND id IN (${ids.map(() => '?').join(', ')})`;
this.db.prepare(query).run([jid, ...ids]);
return this.methods.deleteByJidByIds(jid, ids);
}
deleteAllByJid(jid: string): Promise<void> {
return this.deleteBy({ jid: jid });
return this.methods.deleteAllByJid(jid);
}
}
@@ -1,4 +1,4 @@
import { Schema } from '@waha/core/storage/sqlite3/Schema';
import { Schema } from '@waha/core/storage/Schema';
// eslint-disable-next-line @typescript-eslint/no-var-requires
const Database = require('better-sqlite3');
@@ -1,13 +1,12 @@
import { WAMessage } from '@adiwajshing/baileys';
import { LabelAssociation } from '@adiwajshing/baileys/lib/Types/LabelAssociation';
import { ILabelAssociationRepository } from '@waha/core/engines/noweb/store/ILabelAssociationsRepository';
import { ILabelsRepository } from '@waha/core/engines/noweb/store/ILabelsRepository';
import { Sqlite3GroupRepository } from '@waha/core/engines/noweb/store/sqlite3/Sqlite3GroupRepository';
import { Sqlite3LabelAssociationsRepository } from '@waha/core/engines/noweb/store/sqlite3/Sqlite3LabelAssociationsRepository';
import { Sqlite3LabelsRepository } from '@waha/core/engines/noweb/store/sqlite3/Sqlite3LabelsRepository';
import { Field, Index, Schema } from '@waha/core/storage/sqlite3/Schema';
import { Schema } from '@waha/core/storage/Schema';
import { INowebStorage } from '../INowebStorage';
import { NOWEB_STORE_SCHEMA } from '../Schema';
import { Migrations, NOWEB_STORE_SCHEMA } from '../schemas';
import { Sqlite3ChatRepository } from './Sqlite3ChatRepository';
import { Sqlite3ContactRepository } from './Sqlite3ContactRepository';
import { Sqlite3MessagesRepository } from './Sqlite3MessagesRepository';
@@ -43,109 +42,36 @@ export class Sqlite3Storage extends INowebStorage {
}
private migration0001init() {
// Contacts
this.db.exec(
'CREATE TABLE IF NOT EXISTS contacts (id TEXT PRIMARY KEY, data TEXT)',
);
this.db.exec(
'CREATE UNIQUE INDEX IF NOT EXISTS contacts_id_index ON contacts (id)',
);
// Chats
this.db.exec(
'CREATE TABLE IF NOT EXISTS chats (id TEXT PRIMARY KEY, conversationTimestamp INTEGER, data TEXT)',
);
this.db.exec(
'CREATE UNIQUE INDEX IF NOT EXISTS chats_id_index ON chats (id)',
);
this.db.exec(
'CREATE INDEX IF NOT EXISTS chats_conversationTimestamp_index ON chats (conversationTimestamp)',
);
// Messages
this.db.exec(
'CREATE TABLE IF NOT EXISTS messages (jid TEXT, id TEXT, messageTimestamp INTEGER, data TEXT)',
);
this.db.exec(
'CREATE UNIQUE INDEX IF NOT EXISTS messages_id_index ON messages (id)',
);
this.db.exec(
'CREATE INDEX IF NOT EXISTS messages_jid_id_index ON messages (jid, id)',
);
this.db.exec(
'CREATE INDEX IF NOT EXISTS messages_jid_timestamp_index ON messages (jid, messageTimestamp)',
);
this.db.exec(
'CREATE INDEX IF NOT EXISTS timestamp_index ON messages (messageTimestamp)',
);
//
// Labels
//
this.db.exec(
'CREATE TABLE IF NOT EXISTS labels (id TEXT PRIMARY KEY, data TEXT)',
);
this.db.exec(
'CREATE UNIQUE INDEX IF NOT EXISTS labels_id_index ON labels (id)',
);
// Label associations
this.db.exec(
'CREATE TABLE IF NOT EXISTS labelAssociations (id TEXT PRIMARY KEY, type TEXT, labelId TEXT, chatId TEXT, messageId TEXT, data TEXT)',
);
this.db.exec(
'CREATE UNIQUE INDEX IF NOT EXISTS label_assoc_id_index ON labelAssociations (id)',
);
this.db.exec(
'CREATE INDEX IF NOT EXISTS label_assoc_type_label_index ON labelAssociations (type, labelId)',
);
this.db.exec(
'CREATE INDEX IF NOT EXISTS label_assoc_type_chat_index ON labelAssociations (type, chatId)',
);
this.db.exec(
'CREATE INDEX IF NOT EXISTS label_assoc_type_message_index ON labelAssociations (type, messageId)',
);
for (const migration of Migrations) {
this.db.exec(migration);
}
}
async close() {
this.db.close();
return this.db.close();
}
getContactsRepository() {
return new Sqlite3ContactRepository(this.db, this.getSchema('contacts'));
return new Sqlite3ContactRepository(this.db);
}
getChatRepository() {
return new Sqlite3ChatRepository(this.db, this.getSchema('chats'));
return new Sqlite3ChatRepository(this.db);
}
getGroupRepository() {
return new Sqlite3GroupRepository(this.db);
}
getLabelsRepository(): ILabelsRepository {
return new Sqlite3LabelsRepository(this.db, this.getSchema('labels'));
return new Sqlite3LabelsRepository(this.db);
}
getLabelAssociationRepository(): ILabelAssociationRepository {
const metadata = this.getLabelAssociationMetadata();
return new Sqlite3LabelAssociationsRepository(
this.db,
this.getSchema('labelAssociations'),
metadata,
);
return new Sqlite3LabelAssociationsRepository(this.db);
}
getMessagesRepository() {
const metadata = this.getMessagesMetadata();
return new Sqlite3MessagesRepository(
this.db,
this.getSchema('messages'),
metadata,
);
}
getSchema(name: string) {
const schema = this.tables.find((table) => table.name === name);
if (!schema) {
throw new Error(`Schema not found: ${name}`);
}
return schema;
return new Sqlite3MessagesRepository(this.db);
}
}
@@ -4,9 +4,12 @@ import {
proto,
} from '@adiwajshing/baileys';
import { BufferJSON, initAuthCreds } from '@adiwajshing/baileys/lib/Utils';
import { mkdir, readFile, stat, unlink, writeFile } from 'fs/promises';
import { mkdir, readFile, stat, unlink } from 'fs/promises';
import { join } from 'path';
// eslint-disable-next-line @typescript-eslint/no-var-requires
const writeFileAtomic = require('write-file-atomic');
// eslint-disable-next-line @typescript-eslint/no-var-requires
const AsyncLock = require('async-lock');
@@ -29,11 +32,15 @@ export const useMultiFileAuthState = async (
): Promise<{
state: AuthenticationState;
saveCreds: () => Promise<void>;
close: () => Promise<void>;
}> => {
const writeData = (data: any, file: string) => {
const filePath = join(folder, fixFileName(file));
return fileLock.acquire(filePath, () =>
writeFile(join(filePath), JSON.stringify(data, BufferJSON.replacer)),
writeFileAtomic(
join(filePath),
JSON.stringify(data, BufferJSON.replacer),
),
);
};
@@ -111,5 +118,8 @@ export const useMultiFileAuthState = async (
saveCreds: () => {
return writeData(creds, 'creds.json');
},
close: async () => {
return;
},
};
};
+1 -1
View File
@@ -1,7 +1,7 @@
import { extractMessageContent, proto } from '@adiwajshing/baileys';
export function extractMediaContent(
content: proto.IMessage | null | undefined,
content: any | proto.IMessage | null | undefined,
) {
content = extractMessageContent(content);
const mediaContent =
+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;
}
}
@@ -2,9 +2,14 @@ import { GetChatMessagesFilter } from '@waha/structures/chats.dto';
import { Label } from '@waha/structures/labels.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');
@@ -18,17 +23,65 @@ const { LoadPaginator } = require('./_Paginator.js');
// eslint-disable-next-line @typescript-eslint/no-var-requires
const ChatFactory = require('whatsapp-web.js/src/factories/ChatFactory');
export class WebjsClient extends Client {
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.pupPage.evaluate(LoadLodash);
await this.pupPage.evaluate(LoadPaginator);
await this.pupPage.evaluate(LoadWAHA);
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(
pushName,
);
}, name);
if (this.info) {
this.info.pushname = name;
}
}
async unpair() {
await this.pupPage.evaluate(async () => {
if (
@@ -163,6 +216,9 @@ export class WebjsClient extends Client {
) {
return false;
}
if (filter['filter.ack'] != null && m.ack !== filter['filter.ack']) {
return false;
}
return true;
};
@@ -174,8 +230,17 @@ export class WebjsClient 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) {
+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;
};
+10
View File
@@ -11,4 +11,14 @@ exports.LoadWAHA = () => {
const chatPromises = chats.map((chat) => window.WWebJS.getChatModel(chat));
return await Promise.all(chatPromises);
};
// Set push name
window.WAHA.WAWebSetPushnameConnAction = window.require(
'WAWebSetPushnameConnAction',
);
// Get my status
window.WAHA.WAWebTextStatusCollection = window.require(
'WAWebTextStatusCollection',
);
};
+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;
}
+154
View File
@@ -0,0 +1,154 @@
import { WebjsClientCore } from '@waha/core/engines/webjs/WebjsClientCore';
import {
GroupId,
GroupInfo,
GroupParticipant,
GroupParticipantRole,
} from '@waha/structures/groups.dto';
import {
GroupParticipantType,
GroupV2JoinEvent,
GroupV2LeaveEvent,
GroupV2ParticipantsEvent,
GroupV2UpdateEvent,
} from '@waha/structures/groups.events.dto';
import {
GroupChat,
GroupNotification,
GroupNotificationTypes,
GroupParticipant as WEBJSGroupParticipant,
} from 'whatsapp-web.js';
function ToGroupInfo(
group: GroupChat,
invite: string,
participants = [],
): GroupInfo {
// @ts-ignore
const groupMetadata = group.groupMetadata;
const info: GroupInfo = {
// @ts-ignore
id: group.id._serialized,
subject: group.name,
description: group.description,
invite: invite,
membersCanAddNewMember: groupMetadata.restrict,
membersCanSendMessages: groupMetadata.announce,
newMembersApprovalRequired: groupMetadata.membershipApprovalMode,
participants: participants,
};
return info;
}
export async function ToGroupV2JoinEvent(
client: WebjsClientCore,
me: string,
notification: GroupNotification,
): Promise<GroupV2JoinEvent | null> {
if (!notification.recipientIds.includes(me)) {
return null;
}
// @ts-ignore
const group: GroupChat = await client.getChatById(notification.id.remote);
const invite = await group.getInviteCode();
const participants = getParticipants(group.participants);
const info: GroupInfo = ToGroupInfo(group, invite, participants);
return {
timestamp: notification.timestamp,
group: info,
_data: notification,
};
}
function getParticipants(
participants: WEBJSGroupParticipant[],
): GroupParticipant[] {
return participants.map((participant) => {
let role: GroupParticipantRole = GroupParticipantRole.PARTICIPANT;
if (participant.isSuperAdmin) {
role = GroupParticipantRole.SUPERADMIN;
} else if (participant.isAdmin) {
role = GroupParticipantRole.ADMIN;
}
return {
id: participant.id._serialized,
role: role,
};
});
}
export function ToGroupV2LeaveEvent(
me: string,
notification: GroupNotification,
): GroupV2LeaveEvent | null {
if (!notification.recipientIds.includes(me)) {
return null;
}
const group: GroupId = {
// @ts-ignore
id: notification.id.remote,
};
return {
timestamp: notification.timestamp,
group: group,
_data: notification,
};
}
export async function ToGroupV2UpdateEvent(
client: WebjsClientCore,
notification: GroupNotification,
): Promise<GroupV2UpdateEvent> {
// @ts-ignore
const group: GroupChat = await client.getChatById(notification.id.remote);
const invite = await group.getInviteCode();
const info: GroupInfo = ToGroupInfo(group, invite, undefined);
return {
group: info,
timestamp: notification.timestamp,
_data: notification,
};
}
export function ToGroupV2ParticipantsEvent(
notification: GroupNotification,
): GroupV2ParticipantsEvent | null {
let type: GroupParticipantType;
let role: GroupParticipantRole;
if (['add', 'invite', 'linked_group_join'].includes(notification.type)) {
type = GroupParticipantType.JOIN;
role = GroupParticipantRole.PARTICIPANT;
} else if (['remove', 'leave'].includes(notification.type)) {
type = GroupParticipantType.LEAVE;
role = GroupParticipantRole.LEFT;
} else if (['promote'].includes(notification.type)) {
type = GroupParticipantType.PROMOTE;
role = GroupParticipantRole.ADMIN;
} else if (['demote'].includes(notification.type)) {
type = GroupParticipantType.DEMOTE;
role = GroupParticipantRole.PARTICIPANT;
} else {
return null;
}
const participants: GroupParticipant[] = notification.recipientIds.map(
(id) => {
return {
id: id,
role: role,
};
},
);
const group: GroupId = {
// @ts-ignore
id: notification.id.remote,
};
return {
group: group,
type: type,
timestamp: notification.timestamp,
participants: participants,
_data: notification,
};
}
+395 -59
View File
@@ -1,36 +1,68 @@
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,
ToGroupV2ParticipantsEvent,
ToGroupV2UpdateEvent,
} from '@waha/core/engines/webjs/groups.webjs';
import { LocalAuth } from '@waha/core/engines/webjs/LocalAuth';
import { WebjsClient } from '@waha/core/engines/webjs/WebjsClient';
import { WebjsClientCore } from '@waha/core/engines/webjs/WebjsClientCore';
import {
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 { CallData } from '@waha/structures/calls.dto';
import {
Channel,
ChannelListResult,
ChannelMessage,
ChannelRole,
ChannelSearchByText,
ChannelSearchByView,
CreateChannelRequest,
ListChannelsQuery,
PreviewChannelMessages,
} from '@waha/structures/channels.dto';
import {
ChatArchiveEvent,
ChatSortField,
ChatSummary,
GetChatMessageQuery,
GetChatMessagesFilter,
GetChatMessagesQuery,
ReadChatMessagesQuery,
ReadChatMessagesResponse,
} from '@waha/structures/chats.dto';
import {
ChatRequest,
CheckNumberStatusQuery,
EditMessageRequest,
MessageButtonReply,
MessageFileRequest,
MessageForwardRequest,
MessageImageRequest,
@@ -53,31 +85,37 @@ import {
WAHASessionStatus,
WAMessageAck,
} from '@waha/structures/enums.dto';
import { BinaryFile, RemoteFile } from '@waha/structures/files.dto';
import {
CreateGroupRequest,
GroupSortField,
ParticipantsRequest,
SettingsSecurityChangeInfo,
} from '@waha/structures/groups.dto';
import { Label, LabelDTO, LabelID } from '@waha/structures/labels.dto';
import { ReplyToMessage } from '@waha/structures/message.dto';
import { PaginationParams } from '@waha/structures/pagination.dto';
import { WAMessage, WAMessageReaction } from '@waha/structures/responses.dto';
import { MeInfo } from '@waha/structures/sessions.dto';
import { PaginationParams, SortOrder } from '@waha/structures/pagination.dto';
import {
BROADCAST_ID,
StatusRequest,
TextStatus,
} from '@waha/structures/status.dto';
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 { fromEvent, merge, mergeMap, Observable } from 'rxjs';
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,
@@ -85,20 +123,20 @@ import {
Contact,
Events,
GroupChat,
GroupNotification,
Label as WEBJSLabel,
Location,
Message,
MessageAck,
MessageMedia,
Reaction,
WAState,
} from 'whatsapp-web.js';
import { Message as MessageInstance } from 'whatsapp-web.js/src/structures';
// eslint-disable-next-line @typescript-eslint/no-var-requires
const QRCode = require('qrcode');
export interface WebJSConfig {
webVersion?: string;
cacheType: 'local' | 'none';
}
export class WhatsappSessionWebJSCore extends WhatsappSession {
@@ -111,7 +149,7 @@ export class WhatsappSessionWebJSCore extends WhatsappSession {
private engineStateCheckDelayedJob: SingleDelayedJobRunner;
private shouldRestart: boolean;
whatsapp: WebjsClient;
whatsapp: WebjsClientCore;
protected qr: QR;
public constructor(config) {
@@ -143,7 +181,11 @@ export class WhatsappSessionWebJSCore extends WhatsappSession {
const path = this.getClassDirName();
const webVersion =
this.engineConfig?.webVersion || '2.3000.1018072227-alpha';
this.logger.info(`Using web version: '${webVersion}'`);
const cacheType = this.engineConfig?.cacheType || 'none';
this.logger.info(`Using cache type: '${cacheType}'`);
if (cacheType === 'local') {
this.logger.info(`Using web version: '${webVersion}'`);
}
return {
puppeteer: {
headless: true,
@@ -153,8 +195,7 @@ export class WhatsappSessionWebJSCore extends WhatsappSession {
},
webVersion: webVersion,
webVersionCache: {
// type: 'none',
type: 'local',
type: cacheType,
path: path,
strict: true,
},
@@ -163,14 +204,15 @@ export class WhatsappSessionWebJSCore extends WhatsappSession {
protected async buildClient() {
const clientOptions = this.getClientOptions();
const base = process.env.WAHA_LOCAL_STORE_BASE_DIR || './.sessions';
clientOptions.authStrategy = new LocalAuth({
clientId: this.name,
dataPath: undefined,
dataPath: `${base}/webjs/default`,
logger: this.logger,
rmMaxRetries: undefined,
});
this.addProxyConfig(clientOptions);
return new WebjsClient(clientOptions);
return new WebjsClientCore(clientOptions);
}
private restartClient() {
@@ -221,12 +263,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");
@@ -265,6 +324,7 @@ export class WhatsappSessionWebJSCore extends WhatsappSession {
this.status = WAHASessionStatus.STOPPED;
this.stopEvents();
this.startDelayedJob.cancel();
this.mediaManager.close();
await this.end();
}
@@ -284,10 +344,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 () => {
@@ -298,11 +359,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');
}
}
@@ -334,8 +414,7 @@ export class WhatsappSessionWebJSCore extends WhatsappSession {
this.whatsapp.on(Events.QR_RECEIVED, async (qr) => {
this.logger.debug('QR received');
// Convert to image and save
const url = await QRCode.toDataURL(qr);
this.qr.save(url, qr);
this.qr.save(qr);
this.printQR(this.qr);
this.status = WAHASessionStatus.SCAN_QR_CODE;
});
@@ -451,6 +530,30 @@ export class WhatsappSessionWebJSCore extends WhatsappSession {
};
}
/**
* Profile methods
*/
public async setProfileName(name: string): Promise<boolean> {
await this.whatsapp.setPushName(name);
return true;
}
public async setProfileStatus(status: string): Promise<boolean> {
await this.whatsapp.setStatus(status);
return true;
}
protected setProfilePicture(file: BinaryFile | RemoteFile): Promise<boolean> {
throw new AvailableInPlusVersion();
}
protected deleteProfilePicture(): Promise<boolean> {
throw new AvailableInPlusVersion();
}
/**
* Other methods
*/
sendText(request: MessageTextRequest) {
const options = this.getMessageOptions(request);
return this.whatsapp.sendMessage(
@@ -474,13 +577,18 @@ 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);
}
reply(request: MessageReplyRequest) {
const options = this.getMessageOptions(request);
return this.whatsapp.sendMessage(request.chatId, request.text, options);
return this.whatsapp.sendMessage(
this.ensureSuffix(request.chatId),
request.text,
options,
);
}
sendImage(request: MessageImageRequest) {
@@ -495,17 +603,25 @@ 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,
});
const options = this.getMessageOptions(request);
return this.whatsapp.sendMessage(request.chatId, location, options);
return this.whatsapp.sendMessage(
this.ensureSuffix(request.chatId),
location,
options,
);
}
async forwardMessage(request: MessageForwardRequest): Promise<WAMessage> {
const forwardMessage = this.recreateMessage(request.messageId);
const msg = await forwardMessage.forward(request.chatId);
const msg = await forwardMessage.forward(this.ensureSuffix(request.chatId));
// Return "sent: true" for now
// need to research how to get the data from WebJS
// @ts-ignore
@@ -513,17 +629,23 @@ export class WhatsappSessionWebJSCore extends WhatsappSession {
}
async sendSeen(request: SendSeenRequest) {
const chat: Chat = await this.whatsapp.getChatById(request.chatId);
const chat: Chat = await this.whatsapp.getChatById(
this.ensureSuffix(request.chatId),
);
await chat.sendSeen();
}
async startTyping(request: ChatRequest) {
const chat: Chat = await this.whatsapp.getChatById(request.chatId);
const chat: Chat = await this.whatsapp.getChatById(
this.ensureSuffix(request.chatId),
);
await chat.sendStateTyping();
}
async stopTyping(request: ChatRequest) {
const chat: Chat = await this.whatsapp.getChatById(request.chatId);
const chat: Chat = await this.whatsapp.getChatById(
this.ensureSuffix(request.chatId),
);
await chat.clearState();
}
@@ -567,6 +689,41 @@ export class WhatsappSessionWebJSCore extends WhatsappSession {
return this.whatsapp.getChats(pagination);
}
public async getChatsOverview(
pagination: PaginationParams,
): Promise<ChatSummary[]> {
pagination = {
...pagination,
sortBy: ChatSortField.CONVERSATION_TIMESTAMP,
sortOrder: SortOrder.DESC,
};
const chats = await this.getChats(pagination);
const promises = [];
for (const chat of chats) {
promises.push(this.fetchChatSummary(chat));
}
const result = await Promise.all(promises);
return result;
}
protected async fetchChatSummary(chat: Chat): Promise<ChatSummary> {
const picture = await this.getContactProfilePicture(
chat.id._serialized,
false,
);
const lastMessage = !!chat.lastMessage
? this.toWAMessage(chat.lastMessage)
: null;
return {
id: chat.id._serialized,
name: chat.name || null,
picture: picture,
lastMessage: lastMessage,
_chat: chat,
};
}
public async getChatMessages(
chatId: string,
query: GetChatMessagesQuery,
@@ -596,6 +753,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,
@@ -603,6 +771,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);
}
@@ -624,7 +805,7 @@ export class WhatsappSessionWebJSCore extends WhatsappSession {
}
async deleteChat(chatId) {
const chat = await this.whatsapp.getChatById(chatId);
const chat = await this.whatsapp.getChatById(this.ensureSuffix(chatId));
return chat.delete();
}
@@ -725,11 +906,10 @@ export class WhatsappSessionWebJSCore extends WhatsappSession {
return { about: await contact.getAbout() };
}
public async getContactProfilePicture(query: ContactQuery) {
const contact = await this.whatsapp.getContactById(
this.ensureSuffix(query.contactId),
);
return { profilePictureURL: await contact.getProfilePicUrl() };
public async fetchContactProfilePicture(id: string) {
const contact = await this.whatsapp.getContactById(this.ensureSuffix(id));
const url = await contact.getProfilePicUrl();
return url;
}
public async blockContact(request: ContactRequest) {
@@ -756,6 +936,14 @@ export class WhatsappSessionWebJSCore extends WhatsappSession {
return this.whatsapp.createGroup(request.name, participantIds);
}
public joinGroup(code: string) {
return this.whatsapp.acceptInvite(code);
}
public joinInfoGroup(code: string) {
return this.whatsapp.getInviteInfo(code);
}
public async getInfoAdminsOnly(id): Promise<SettingsSecurityChangeInfo> {
const groupChat = (await this.whatsapp.getChatById(id)) as GroupChat;
return {
@@ -785,10 +973,32 @@ export class WhatsappSessionWebJSCore extends WhatsappSession {
return groupChat.setMessagesAdminsOnly(value);
}
public getGroups() {
return this.whatsapp
.getChats()
.then((chats) => chats.filter((chat) => chat.isGroup));
public async getGroups(pagination: PaginationParams) {
const chats = await this.whatsapp.getChats();
const groups = lodash.filter(chats, (chat) => chat.isGroup);
switch (pagination.sortBy) {
case GroupSortField.ID:
pagination.sortBy = 'id._serialized';
break;
case GroupSortField.SUBJECT:
pagination.sortBy = 'groupMetadata.subject';
break;
}
const paginator = new PaginatorInMemory(pagination);
return paginator.apply(groups);
}
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;
}
public getGroup(id) {
@@ -866,6 +1076,25 @@ export class WhatsappSessionWebJSCore extends WhatsappSession {
/**
* Channels methods
*/
public searchChannelsByView(
query: ChannelSearchByView,
): Promise<ChannelListResult> {
throw new AvailableInPlusVersion();
}
public searchChannelsByText(
query: ChannelSearchByText,
): Promise<ChannelListResult> {
throw new AvailableInPlusVersion();
}
public async previewChannelMessages(
inviteCode: string,
query: PreviewChannelMessages,
): Promise<ChannelMessage[]> {
throw new AvailableInPlusVersion();
}
protected ChatToChannel(chat: WEBJSChannel): Channel {
// @ts-ignore
const metadata = chat.channelMetadata;
@@ -882,6 +1111,7 @@ export class WhatsappSessionWebJSCore extends WhatsappSession {
picture: null,
verified: metadata.verified,
role: role,
subscribersCount: null,
};
}
@@ -899,6 +1129,7 @@ export class WhatsappSessionWebJSCore extends WhatsappSession {
picture: metadata.pictureUrl,
verified: metadata.isVerified,
role: role,
subscribersCount: metadata.subscribersCount,
};
}
@@ -910,6 +1141,10 @@ export class WhatsappSessionWebJSCore extends WhatsappSession {
channels = channels.filter((channel) => channel.role === query.role);
}
// Exclude GUEST, browser saves the data
// when we search channels or getting messages
channels = channels.filter((channel) => channel.role !== 'GUEST');
const promises = channels.map(async (channel) =>
this.whatsapp.getProfilePicUrl(channel.id),
);
@@ -998,7 +1233,7 @@ export class WhatsappSessionWebJSCore extends WhatsappSession {
* Status methods
*/
protected checkStatusRequest(request: StatusRequest) {
if (request.contacts?.length > 0) {
if (request.contacts && request.contacts?.length > 0) {
const msg =
"WEBJS doesn't accept 'contacts'. Remove the field to send status to all contacts.";
throw new UnprocessableEntityException(msg);
@@ -1014,6 +1249,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
//
@@ -1086,18 +1326,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
@@ -1108,10 +1361,52 @@ export class WhatsappSessionWebJSCore extends WhatsappSession {
//
// Groups
//
const groupJoin$ = fromEvent(this.whatsapp, Events.GROUP_JOIN);
this.events2.get(WAHAEvents.GROUP_JOIN).switch(groupJoin$);
const groupLeave$ = fromEvent(this.whatsapp, Events.GROUP_LEAVE);
this.events2.get(WAHAEvents.GROUP_LEAVE).switch(groupLeave$);
const groupJoin$ = fromEvent<GroupNotification>(
this.whatsapp,
Events.GROUP_JOIN,
);
this.events2.get(WAHAEvents.GROUP_JOIN).switch(groupJoin$); // v1
const groupV2Join$ = groupJoin$.pipe(
mergeMap((evt) =>
ToGroupV2JoinEvent(this.whatsapp, this.getSessionMeInfo().id, evt),
),
filter(Boolean),
);
this.events2.get(WAHAEvents.GROUP_V2_JOIN).switch(groupV2Join$);
const groupLeave$ = fromEvent<GroupNotification>(
this.whatsapp,
Events.GROUP_LEAVE,
);
this.events2.get(WAHAEvents.GROUP_LEAVE).switch(groupLeave$); // v1
const groupV2Leave$ = groupLeave$.pipe(
map((evt) => ToGroupV2LeaveEvent(this.getSessionMeInfo().id, evt)),
filter(Boolean),
);
this.events2.get(WAHAEvents.GROUP_V2_LEAVE).switch(groupV2Leave$);
const groupAdminChanged$ = fromEvent(
this.whatsapp,
Events.GROUP_ADMIN_CHANGED,
);
const groupV2Participants = merge(
groupJoin$,
groupLeave$,
groupAdminChanged$,
).pipe(map(ToGroupV2ParticipantsEvent), filter(Boolean));
this.events2
.get(WAHAEvents.GROUP_V2_PARTICIPANTS)
.switch(groupV2Participants);
const groupUpdate$ = fromEvent<GroupNotification>(
this.whatsapp,
Events.GROUP_UPDATE,
);
const groupV2Update$ = groupUpdate$.pipe(
mergeMap((evt) => ToGroupV2UpdateEvent(this.whatsapp, evt)),
filter(Boolean),
);
this.events2.get(WAHAEvents.GROUP_V2_UPDATE).switch(groupV2Update$);
//
// Chats
@@ -1155,7 +1450,10 @@ export class WhatsappSessionWebJSCore extends WhatsappSession {
this.events2.get(WAHAEvents.CALL_RECEIVED).switch(calls$);
}
private async processIncomingMessage(message: Message, downloadMedia = true) {
protected async processIncomingMessage(
message: Message,
downloadMedia = true,
) {
if (downloadMedia) {
try {
message = await this.downloadMedia(message);
@@ -1168,10 +1466,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,
@@ -1182,21 +1482,52 @@ 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
// @ts-ignore
hasMedia: Boolean(message.media),
hasMedia: Boolean(message.hasMedia),
// @ts-ignore
media: message.media,
media: message.media || null,
// @ts-ignore
mediaUrl: message.media?.url,
// @ts-ignore
@@ -1211,7 +1542,7 @@ export class WhatsappSessionWebJSCore extends WhatsappSession {
protected extractReplyTo(message: Message): ReplyToMessage | null {
// @ts-ignore
const quotedMsg = message.rawData.quotedMsg;
const quotedMsg = message.rawData?.quotedMsg;
if (!quotedMsg) {
return;
}
@@ -1219,6 +1550,7 @@ export class WhatsappSessionWebJSCore extends WhatsappSession {
id: quotedMsg.id?.id,
participant: quotedMsg.author || quotedMsg.from,
body: quotedMsg.caption || quotedMsg.body,
_data: quotedMsg,
};
}
@@ -1245,7 +1577,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,
@@ -1266,6 +1598,10 @@ export class WEBJSEngineMediaProcessor
return message.type !== 'revoked';
}
getChatId(message: Message): string {
return message.id.remote;
}
getMessageId(message: Message): string {
return message.id._serialized;
}
@@ -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,
};
}
+46 -14
View File
@@ -1,8 +1,13 @@
import {
Injectable,
NotFoundException,
OnModuleInit,
UnprocessableEntityException,
} from '@nestjs/common';
import { EngineBootstrap } from '@waha/core/abc/EngineBootstrap';
import { GowsEngineConfigService } from '@waha/core/config/GowsEngineConfigService';
import { WebJSEngineConfigService } from '@waha/core/config/WebJSEngineConfigService';
import { WhatsappSessionGoWSCore } from '@waha/core/engines/gows/session.gows.core';
import { WebhookConductor } from '@waha/core/integrations/webhooks/WebhookConductor';
import { MediaStorageFactory } from '@waha/core/media/MediaStorageFactory';
import { DefaultMap } from '@waha/utils/DefaultMap';
@@ -40,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}`,
);
}
}
@@ -53,7 +60,7 @@ enum DefaultSessionStatus {
}
@Injectable()
export class SessionManagerCore extends SessionManager {
export class SessionManagerCore extends SessionManager implements OnModuleInit {
SESSION_STOP_TIMEOUT = 3000;
// session - exists and running (or failed or smth)
@@ -65,18 +72,22 @@ export class SessionManagerCore extends SessionManager {
protected readonly EngineClass: typeof WhatsappSession;
protected events2: DefaultMap<WAHAEvents, SwitchObservable<any>>;
protected readonly engineBootstrap: EngineBootstrap;
constructor(
config: WhatsappConfigService,
private engineConfigService: EngineConfigService,
private webjsEngineConfigService: WebJSEngineConfigService,
gowsConfigService: GowsEngineConfigService,
log: PinoLogger,
private mediaStorageFactory: MediaStorageFactory,
) {
super(config, log);
super(log, config, gowsConfigService);
this.session = DefaultSessionStatus.STOPPED;
this.sessionConfig = null;
const engineName = this.engineConfigService.getDefaultEngineName();
this.EngineClass = this.getEngine(engineName);
this.engineBootstrap = this.getEngineBootstrap(engineName);
this.events2 = new DefaultMap<WAHAEvents, SwitchObservable<any>>(
(key) =>
@@ -87,7 +98,6 @@ export class SessionManagerCore extends SessionManager {
this.store = new LocalStoreCore(engineName.toLowerCase());
this.sessionAuthRepository = new LocalSessionAuthRepository(this.store);
this.startPredefinedSessions();
this.clearStorage().catch((error) => {
this.log.error({ error }, 'Error while clearing storage');
});
@@ -98,6 +108,8 @@ export class SessionManagerCore extends SessionManager {
return WhatsappSessionWebJSCore;
} else if (engine === WAHAEngine.NOWEB) {
return WhatsappSessionNoWebCore;
} else if (engine === WAHAEngine.GOWS) {
return WhatsappSessionGoWSCore;
} else {
throw new NotFoundException(`Unknown whatsapp engine '${engine}'.`);
}
@@ -105,20 +117,26 @@ export class SessionManagerCore extends SessionManager {
private onlyDefault(name: string) {
if (name !== this.DEFAULT) {
throw new OnlyDefaultSessionIsAllowed();
throw new OnlyDefaultSessionIsAllowed(name);
}
}
async beforeApplicationShutdown(signal?: string) {
this.stopEvents();
if (!this.session) {
return;
if (this.session) {
await this.stop(this.DEFAULT, true);
}
await this.stop(this.DEFAULT, true);
this.stopEvents();
await this.engineBootstrap.shutdown();
}
async onApplicationBootstrap() {
await this.engineBootstrap.bootstrap();
this.startPredefinedSessions();
}
private async clearStorage() {
const storage = this.mediaStorageFactory.build(
const storage = await this.mediaStorageFactory.build(
'all',
this.log.logger.child({ name: 'Storage' }),
);
await storage.purge();
@@ -154,7 +172,8 @@ export class SessionManagerCore extends SessionManager {
logger.level = getPinoLogLevel(this.sessionConfig?.debug);
const loggerBuilder: LoggerBuilder = logger;
const storage = this.mediaStorageFactory.build(
const storage = await this.mediaStorageFactory.build(
name,
loggerBuilder.child({ name: 'Storage' }),
);
await storage.init();
@@ -175,6 +194,11 @@ export class SessionManagerCore extends SessionManager {
proxyConfig: proxyConfig,
sessionConfig: this.sessionConfig,
};
if (this.EngineClass === WhatsappSessionWebJSCore) {
sessionConfig.engineConfig = this.webjsEngineConfigService.getConfig();
} else if (this.EngineClass === WhatsappSessionGoWSCore) {
sessionConfig.engineConfig = this.gowsConfigService.getConfig();
}
await this.sessionAuthRepository.init(name);
// @ts-ignore
const session = new this.EngineClass(sessionConfig);
@@ -340,9 +364,9 @@ export class SessionManagerCore extends SessionManager {
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',
);
}
}
@@ -369,4 +393,12 @@ export class SessionManagerCore extends SessionManager {
protected stopEvents() {
complete(this.events2);
}
async onModuleInit() {
await this.init();
}
async init() {
await this.store.init();
}
}
+2
View File
@@ -11,6 +11,8 @@ interface IMediaEngineProcessor<Message> {
getMessageId(message: Message): string;
getChatId(message: Message): string;
getMediaBuffer(message: Message): Promise<Buffer | null>;
}
+1
View File
@@ -10,6 +10,7 @@ interface IMediaManager {
message: Message,
session: string,
): Promise<Message>;
close(): void;
}
export { IMediaManager };
+16
View File
@@ -6,6 +6,9 @@ import { S3MediaData } from '@waha/structures/media.s3.dto';
interface Message {
// false_111111111@c.us_AAAAAAAAAAAAAA
id: string;
// 1111111@c.us
chatId: string;
}
interface File {
@@ -37,6 +40,19 @@ abstract class IMediaStorage {
abstract getStorageData(data: MediaData): Promise<MediaStorageData>;
abstract purge(): Promise<void>;
abstract close(): Promise<void>;
}
export function getMetadata(data: MediaData): any {
const now = new Date();
return {
'waha-session': data.session,
'waha-chat-id': data.message.chatId,
'waha-message-id': data.message.id,
'waha-media-filename': data.file.filename,
'waha-created-at': now.getTime(),
};
}
export { IMediaStorage };
+8
View File
@@ -52,6 +52,7 @@ export class MediaManager implements IMediaManager {
session: string,
): Promise<WAMedia | null> {
const messageId = processor.getMessageId(message);
const chatId = processor.getChatId(message);
const mimetype = processor.getMimetype(message);
const filename = processor.getFilename(message);
if (!this.shouldProcessMimetype(mimetype)) {
@@ -66,6 +67,7 @@ export class MediaManager implements IMediaManager {
session: session,
message: {
id: messageId,
chatId: chatId,
},
file: {
extension: extension,
@@ -195,4 +197,10 @@ export class MediaManager implements IMediaManager {
throw error;
}
}
close() {
this.storage.close().catch((err) => {
this.log.error(`Failed to close media storage: ${err}`);
});
}
}
Loaded 100 of 158 files, more files were not shown because too many files have changed in this diff. Show more