Compare commits

...
430 Commits
Author SHA1 Message Date
devlikepro d8ffd3e543 [core] 2025.7.11
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-08-03 13:59:23 +07:00
devlikepro ea4ec9310d [core] WEBJS - fix Caught error, dropping value from, event: 'message.ack'
fix #1178
2025-08-03 13:59:22 +07:00
devlikepro 479ec85e0d [core] Dashboard - i18n 2025-08-03 13:59:22 +07:00
devlikepro 3e4b450260 [core] ChatWoot - find chat id - jid, lid, chat id 2025-08-03 13:59:22 +07:00
devlikepro f6a81949d0 [core] ChatWoot - Urdu 2025-08-03 13:59:21 +07:00
devlikepro 6896962606 [core] Always show list of chatwoot languages
fix #1138
2025-08-03 13:59:21 +07:00
devlikepro 9b6d6ab4b0 [core] Inline LOCALE_NAME in yaml 2025-08-03 13:59:21 +07:00
devlikepro d5f399f7d5 [core] 2025.7.10
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-07-27 17:39:17 +07:00
devlikepro dbd3bd024c [core] Up GOWS
Fix #1100
2025-07-27 17:39:16 +07:00
devlikepro 9ba7e19da3 [core] Use sqlite3 instead of better-sqlite3
better-sqlite3 can freeze the whole system
2025-07-27 17:39:16 +07:00
devlikepro 5f6d1412cf [core] Add sqlite3 2025-07-27 17:39:15 +07:00
devlikepro 0b5af3145e [core] Always save config, not null 2025-07-27 17:39:15 +07:00
devlikepro ad10ea4e96 [core] 2025.7.9
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-07-25 12:46:33 +07:00
devlikepro 9de53a972a [core] Up NOWEB
fix #1143
2025-07-25 12:46:33 +07:00
devlikepro 4043efbb9a [core] Up Dashboard 2025-07-25 12:46:33 +07:00
devlikepro 42019644f0 [core] cpus*2 for UV_THREADPOOL_SIZE 2025-07-25 12:46:33 +07:00
devlikepro bcc25ef42f [core] Adjust UV_THREADPOOL_SIZE logic 2025-07-25 12:46:33 +07:00
devlikepro 602bbf3b60 [core] WEBJS - WAHA_RUN_XVFB
fix #1151
2025-07-25 12:46:32 +07:00
devlikeaproandsnyk-bot 585ac6074f [core] fix: package.json to reduce vulnerabilities (#16)
The following vulnerabilities are fixed with an upgrade:
- https://snyk.io/vuln/SNYK-JS-MULTER-10773732

Co-authored-by: snyk-bot <snyk-bot@snyk.io>
2025-07-24 10:37:37 +07:00
devlikepro 5bd1a43df6 [core] 2025.7.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-07-23 15:02:05 +07:00
devlikepro c9279a4b59 [core] ChatWoot - send png as images 2025-07-23 15:02:04 +07:00
devlikepro 9ff8eed6ba [core] Add, edit, and remove contact API
fix #1124
2025-07-23 15:02:03 +07:00
devlikepro 96ffdd251f [core] Up GOWS 2025-07-23 15:01:53 +07:00
devlikepro 35b8b7d01c [core] 2025.7.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-07-22 14:53:35 +07:00
devlikepro 4e2e2b2b3c [core] WEBJS - add 1GB cache back
fix #1131 fix #1140
2025-07-22 14:53:35 +07:00
devlikepro bfff0c8cf4 [core] 2025.7.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-07-20 14:06:59 +07:00
devlikepro e253f445b9 [core] WEBJS - fix sendSeen
fix #1117
2025-07-20 14:06:58 +07:00
devlikepro 73ed663c9c [core] Dashboard - show tier 2025-07-20 14:06:58 +07:00
devlikepro 2236150b83 [core] ChatWoot - adjust commands list text 2025-07-20 14:06:58 +07:00
devlikepro da24feb390 [core] Fix "Not implemented by engine" in status
fix #1115
2025-07-20 14:06:58 +07:00
devlikepro 44a5ada162 [core] Fix "You provided 'null' where a stream was expected" 2025-07-20 14:06:58 +07:00
devlikepro e57cc2587c [core] WEBJS - share media properly 2025-07-20 14:06:58 +07:00
Alan Frigo 38bfb9baa5 Added WHATSAPP_HOOK_CUSTOM_HEADERS to env example 2025-07-20 13:57:46 +07:00
devlikepro 540636d59b [core] Fix MongoDB
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
Release / WEBJS - chromium - amd64 - latest (push) Waiting to run
Release / WEBJS - chromium - linux/arm64 - arm (push) Waiting to run
2025-07-13 18:40:19 +07:00
devlikepro 3cc784353e [core] 2025.7.5 2025-07-13 18:32:54 +07:00
devlikepro 44946ee784 [core] Add ChatWoot App
fix #441
2025-07-13 18:32:53 +07:00
devlikepro a9d77487c6 [core] Fix LegacyKeyValueFormat in Dockerfile 2025-07-13 18:32:53 +07:00
devlikepro 70d4f27abd [core] WEBJS - ignore old reactions
fixes #494
2025-07-13 18:32:53 +07:00
devlikepro 9fca09661d [core] WEBJS - render process 2 2025-07-13 18:32:53 +07:00
devlikepro 2f0efd0223 [core] 2025.7.4
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
Release / WEBJS - chromium - amd64 - latest (push) Waiting to run
Release / WEBJS - chromium - linux/arm64 - arm (push) Waiting to run
2025-07-06 16:48:47 +07:00
devlikepro 5c31ed53df [core] WEBJS - fix error for message.ack
TypeError: You provided 'null' where a stream was expected.
2025-07-06 16:48:47 +07:00
devlikepro 87640ea238 [core] WEBJS - avoid STARTING => FAILED cycles
fix #1071
2025-07-06 16:48:47 +07:00
devlikepro eb8f771b06 [core] WEBJS - protocolTimeout to 300s (5m)
fix #1103
2025-07-06 16:48:46 +07:00
devlikepro b080b5862e [core] NOWEB - no lock on 'lids' updates 2025-07-06 16:48:46 +07:00
devlikepro df50e7527e [core] NOWEB - save @lid to @c.us from the first message
fix #1073
fix #1097
2025-07-06 16:48:46 +07:00
devlikepro e2b43575c0 [core] Up NOWEB - 2025-07-04
fix #531
fix #1098
2025-07-06 16:48:45 +07:00
devlikepro 2059793562 [core] Log update for unknown jid 2025-07-06 16:48:45 +07:00
devlikepro e5df21e165 [core] 2025.7.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-07-03 17:13:06 +07:00
devlikepro 792029b13f [core] Up GOWS
fix #1043
fix #1068
fix #1094
2025-07-03 17:13:06 +07:00
devlikepro 0ef49d7116 [core] GOWS - fallback to storage in case of JSON errors
2 UNKNOWN: json: cannot unmarshal object into Go struct field InteractiveAnnotation.imageMessage.annotations. Action of type waE2
E.isInteractiveAnnotation_Action
2025-07-03 17:13:06 +07:00
devlikepro 600d3291eb [core] add dns in waha by default 2025-07-03 17:13:06 +07:00
devlikepro b78413190c [core] WEBJS - disable GPU 2025-07-03 17:13:06 +07:00
devlikepro c9729af139 [core] 2025.7.2
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 / WEBJS - chrome - amd64 - chrome (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-07-02 14:47:05 +07:00
devlikepro 697f267fc3 [core] Add - in prefix 2025-07-02 14:47:05 +07:00
devlikepro ac49ce3cc5 [core] trace 2025-07-02 14:47:05 +07:00
devlikepro 555daf6888 [core] fix heap API 2025-07-02 14:47:05 +07:00
devlikepro 8f0c2994a6 [core] One build-all 2025-07-02 14:47:05 +07:00
devlikepro 873e66db19 [core] Add browser tracing debug API 2025-07-02 14:45:52 +07:00
devlikepro 38b542def4 [core] Add TmpDir (from PLUS) 2025-07-02 14:45:51 +07:00
devlikepro a646e5efc0 [core] remove healthcheck 2025-07-02 14:45:50 +07:00
devlikepro 31590c9229 [core] Add server.debug.controller.ts 2025-07-02 14:45:50 +07:00
devlikepro 98c2172908 [core] WEBJS - onTag handler
related #1092
2025-07-02 14:45:49 +07:00
devlikepro 314b02daf1 [core] WEBJS add waha arguments to chrome to identify it better 2025-07-02 14:45:49 +07:00
devlikepro d2ab78b65c [core] WEBJS Up user agent 2025-07-02 14:45:49 +07:00
devlikepro db2c352fd1 [core] WEBJS --renderer-process-limit=1 2025-07-02 14:45:49 +07:00
devlikepro 6e79ad2793 [core] WEBJS - rm --single-process at all 2025-07-02 14:45:49 +07:00
devlikepro 77eaa287fe [core] WEBJS - remove disable-background-networking, it's default puppeter flag 2025-07-02 14:45:49 +07:00
devlikepro a64820048c [core] Add health-cmd
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-07-01 13:35:00 +07:00
devlikepro 4ff4a5a893 [core] 2025.7.1 2025-07-01 13:23:38 +07:00
devlikepro 5ea5681d2b [core] Use PORT first
fix #1090
2025-07-01 13:23:38 +07:00
devlikepro 2bb327b747 [core] move HEALTHCHECK from Dockerfile to docker-compose.yaml
fix #1090
2025-07-01 13:23:38 +07:00
devlikepro 4dcdf60309 [core] make build-all 2025-07-01 13:23:37 +07:00
devlikepro 009fd5b2ec [core] NOWEB - share() events 2025-07-01 13:23:37 +07:00
devlikepro 091e7e5bc1 [core] Use this.knex or query methods directly 2025-07-01 13:23:37 +07:00
devlikepro ea1202bb65 [core] Use knex by for sqlite3, not database connection 2025-07-01 13:23:32 +07:00
devlikepro cb413febaa [core] Handle WhatsApp News profile picture 2025-07-01 13:23:32 +07:00
devlikepro 364e55d477 [core] Add KnexEngine (copy paste from psql) 2025-07-01 13:23:32 +07:00
devlikepro 680a35a26d [core] rm start-interval from docker run - docker 24 doesn't have it (used in CI)
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-06-29 11:31:54 +07:00
devlikepro 08ff59e463 [core] 2025.6.7 2025-06-29 11:17:51 +07:00
devlikepro 9db4c8c333 [core] Check docker health in CI
fix #1085
2025-06-29 11:17:51 +07:00
devlikepro 2489c2da46 [core] Install curl in all images
fix #1085
2025-06-29 11:17:51 +07:00
devlikepro fbc58b0707 [core] 2025.6.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-06-27 11:09:33 +07:00
devlikepro d5a525789e [core] NOWEB - show poll on the device
fix #988
2025-06-27 11:09:32 +07:00
devlikepro 34066921e1 [core] run xvfb only for WEBJS engine 2025-06-27 11:09:32 +07:00
devlikepro 901c594311 [core] WEBJS - fix pin message
fix #1081
2025-06-27 11:09:32 +07:00
devlikepro 5f619e608b [core] WEBJS - return response for a message
fix #1083
2025-06-27 11:09:31 +07:00
devlikepro 49295090ad [core] Use 501 Not Implemented HTTP status for NotImplemented by engine error
fix #1047
2025-06-27 11:09:31 +07:00
devlikepro a6e43adb0a [core] use xvfb-run 2025-06-27 11:09:30 +07:00
devlikepro 1f165bb6cb [core] Puppeteer 24.10.0 2025-06-27 11:09:30 +07:00
devlikepro 8fc9d88d2e [core] No security reminder, it creates a zombie process
fix #1078
2025-06-27 11:09:29 +07:00
devlikepro d7e5a5ce05 [core] Add ping check 2025-06-27 11:09:29 +07:00
devlikepro 09d446fdeb [core] Install wget for chrome 2025-06-27 11:09:28 +07:00
devlikepro c0bbd60e15 [core] Up Dashboard 2025-06-27 11:09:28 +07:00
devlikepro af3fc137be [core] fix del usage 2025-06-27 11:09:27 +07:00
devlikepro 0ee5e1bd88 [core] Up del, rimraf 2025-06-27 11:09:27 +07:00
devlikepro f92ea18651 [core] Use Debian Bookworm 2025-06-27 11:09:27 +07:00
devlikepro 3cce2bb35f [core] 2025.6.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-06-25 13:38:56 +07:00
devlikepro e080ce42ee [core] no dashboard username by default 2025-06-25 13:38:56 +07:00
devlikepro 5dbe41da78 [core] postgresql 127.0.0.1 2025-06-25 13:38:55 +07:00
devlikepro eef3957d4f [core] Warning about no API key 2025-06-25 13:38:55 +07:00
devlikepro 680f4596b9 [core] Up Dashboard 2025-06-25 13:38:55 +07:00
devlikepro 8de344f220 [core] Add enabled vars for dashboard and swagger 2025-06-25 13:38:54 +07:00
devlikepro 8005846955 [core] admin/admin by default for dashboard 2025-06-25 13:38:54 +07:00
devlikepro 3edd60448b [core] format .env better 2025-06-25 13:38:54 +07:00
devlikepro 4a2fa5c076 [core] message about plain key 2025-06-25 13:38:54 +07:00
devlikepro 96e9c2cc4e [core] hash key in entrypoint.sh always 2025-06-25 13:38:53 +07:00
devlikepro 2089a1bb35 [core] Handle empty key in hash case 2025-06-25 13:38:52 +07:00
devlikepro 023c2718e2 [core] Move websocket.gateway.core.ts to /api 2025-06-25 13:38:52 +07:00
devlikepro f7aacb193a [core] Add Auth methods - NoAuth, PlainApiKeyAuth, Hash 2025-06-25 13:38:52 +07:00
devlikepro e710040904 [core] Add base providers 2025-06-25 13:38:52 +07:00
devlikepro fb12826801 [core] Refactor websocket authorization - use DI 2025-06-25 13:38:51 +07:00
devlikepro 833a70c6e0 [core] Remove hot fix for closing websockets 2025-06-25 13:38:50 +07:00
devlikepro c807f9021c [core] Support WAHA_API_KEY as well as env variable
gpt often suggests this env variable, IDK why
2025-06-25 13:38:49 +07:00
devlikepro 46b66bcdf0 [core] Move built-in SSL (DEPRECATED) to WAHA Core
fix #1069
2025-06-25 13:38:48 +07:00
devlikepro 2ebade817e [core] add swagger security to WAHA Core
fix #1069
2025-06-25 13:38:48 +07:00
devlikepro 080342e619 [core] add /ws authentication
fix #1069
2025-06-25 13:38:48 +07:00
devlikepro 75b06d3ba1 [core] Copy security file to WAHA Core
fix #1069
2025-06-25 13:38:48 +07:00
devlikepro 3b6fa3343d [core] 2025.6.4
Release / WEBJS - chrome - amd64 - chrome (push) Waiting to run
Release / WEBJS - chromium - amd64 - latest (push) Waiting to run
Release / WEBJS - chromium - linux/arm64 - arm (push) Waiting to run
Release / GOWS - none - amd64 - gows (push) Waiting to run
Release / GOWS - none - linux/arm64 - gows-arm (push) Waiting to run
Release / NOWEB - none - amd64 - noweb (push) Waiting to run
Release / NOWEB - none - linux/arm64 - noweb-arm (push) Waiting to run
2025-06-21 17:34:41 +07:00
devlikepro 469cdc9b1d [core] no xvfb if not installed (noweb/gows image) 2025-06-21 17:34:41 +07:00
devlikepro 8a179e7194 [core] NOWEB - sleep for sqlite3 in batch 2025-06-21 17:34:41 +07:00
devlikepro c133795753 [core] NOWEB - log length, not json
fix devlikeapro/waha#1054
2025-06-21 17:34:41 +07:00
devlikepro 941ace7461 [core] 2025.6.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-06-16 13:05:20 +07:00
devlikepro 8589420f5d [core] do not stop ws socket in gateway 2025-06-16 13:05:20 +07:00
devlikepro ece24e182f [core] WEBJS - xvfb 2025-06-16 13:05:19 +07:00
devlikepro 38e5bb7bdf [core] NOWEB - Add timeout 5s for async lock 2025-06-16 13:05:19 +07:00
devlikepro d09e931827 [core] chrome 137.0.7151.103-1 2025-06-16 13:05:19 +07:00
devlikepro 3f3f1e1c20 [core] Inline WAMessage in message.edited 2025-06-16 13:05:18 +07:00
devlikepro 32f2468ba8 [core] Add WAHAWebhookMessageEdited to webhooks list 2025-06-16 13:05:17 +07:00
devlikepro 1d6540e0d9 [core] editedMessageId 2025-06-16 13:05:17 +07:00
devlikepro 31da4bfca1 [core] WEBJS - message.edited
fix #1041
2025-06-16 13:05:17 +07:00
devlikepro 41d49894e2 [core] WEBJS - message.revoked add revokedMessageId 2025-06-16 13:05:17 +07:00
devlikepro 6dcdd15b1c [core] message.revoked - add revokedMessageId - NOWEB, GOWS 2025-06-16 13:05:17 +07:00
devlikepro c2b275fd90 [core] NOWEB - message.edited 2025-06-16 13:05:16 +07:00
devlikepro e4838ef3ef [core] GOWS - message.edited
fix #916
2025-06-16 13:05:16 +07:00
devlikepro 55ca4660d7 [core] GOWS - message.revoked
fix #917
2025-06-16 13:05:15 +07:00
devlikepro c16c903ec4 [core] node 22.16 2025-06-16 13:05:15 +07:00
devlikepro 937cf38ff6 [core] Up libs 2025-06-16 13:05:15 +07:00
devlikepro 07f76e854d [core] Update axios 2025-06-16 13:05:15 +07:00
devlikepro cde1d084c6 [core] NOWEB fix issue with can not read property of null 2025-06-16 13:05:14 +07:00
devlikepro 9362c5d6ae [core] 2025.6.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-06-11 15:41:56 +07:00
devlikepro 117a9108c0 [core] puppeter 24.10.0 2025-06-11 15:41:56 +07:00
devlikepro 5214e461f2 [core] chrome 137.0.7151.55 2025-06-11 15:41:56 +07:00
devlikepro 9bdb178b7d [core] Use github examples 2025-06-11 15:41:56 +07:00
devlikepro 9c526ffdba [core] Add converting voice and video files
fix #724
2025-06-11 15:41:56 +07:00
devlikepro f80a74edd1 [core] Add avi and mp3 in examples 2025-06-11 12:20:58 +07:00
devlikepro 7d8c6c0a62 [core] 2025.6.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-06-09 14:01:26 +07:00
devlikepro 17b26b869b [core] Add --pprof-host listen to 0.0.0.0 if debug mode enabled 2025-06-09 14:01:26 +07:00
devlikepro dfbe54e92f [core] Up GOWS
fix #919
2025-06-09 14:01:25 +07:00
devlikepro 2321de409b [core] Up WEBJS
fix #1023
fix #1024
fix #1025
fix #1026
fix #1027
2025-06-09 14:01:25 +07:00
devlikepro 502287a258 [core] WEBJS - add exposeFunctionIfAbsent 2025-06-09 14:01:25 +07:00
devlikepro 052ff0ea6e [core] WEBJS - fork-main-2025-06-09 branch 2025-06-09 14:01:24 +07:00
devlikepro 2b76789cdf [core] Run GOWS with pprof if WAHA_DEBUG_MODE=true 2025-06-09 14:01:24 +07:00
devlikepro 7bbf5df231 [core] Update grpc 2025-06-09 14:01:24 +07:00
devlikepro 794a04e7d3 [core] Update grpc-js and grpc-tools 2025-06-09 14:01:23 +07:00
devlikepro 645467f13a [core] echo -e 2025-06-09 14:01:23 +07:00
devlikepro 7e87637341 [core] 2025.5.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-05-30 11:18:16 +07:00
devlikepro b7abeed54a [core] 2025.5.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-05-28 16:42:04 +07:00
devlikepro d53646eef2 [core] docker-compose.yaml - set logging by default 100m/10 files 2025-05-28 16:42:04 +07:00
devlikepro c54743de6c [core] Up GOWS 2025-05-28 16:42:03 +07:00
devlikepro e1fdc41132 [core] NOWEB - skip updates with no id 2025-05-28 16:42:03 +07:00
devlikepro 060aa82294 [core] WEBJS - Presence - API and Events
fix #636
2025-05-28 16:42:02 +07:00
devlikepro 2df08eac3f [core] NOWEB - hosted account pairing
fix #864
2025-05-28 16:42:02 +07:00
devlikepro 0ecdb9d3d0 [core] GOWS - Always subscribe to presence 2025-05-28 16:42:02 +07:00
devlikepro 6a7fe1bd1f [core] NOWEB - do not resubscribe to presences 2025-05-28 16:42:02 +07:00
devlikepro 87c69cb3d0 [core] NOWEB - Always subscribe to presence 2025-05-28 16:42:01 +07:00
devlikepro c654855c22 [core] Add ids filter to /overview
fix #791
2025-05-28 16:42:00 +07:00
devlikepro 11bb006801 [core] WEBJS - adjust logging on stop 2025-05-28 16:42:00 +07:00
devlikepro e5551f3f64 [core] Refactor path handling to improve security and safety.
Replaced `path.join` with `safeJoin` to validate and normalize paths, ensuring they stay within the base directory and prevent malicious access. Added `safeJoin` implementation in `utils/files.ts`, enforcing strict path validation rules. This improves robustness against path traversal vulnerabilities.
2025-05-28 16:41:59 +07:00
devlikepro 0de148005f [core] require session "name" for deprecated API 2025-05-28 16:41:58 +07:00
devlikepro 27e4ce628e [core] NOWEB - handle no content found messages: TypeError: Cannot read properties of undefined (reading 'pollUpdateMessage') 2025-05-28 16:41:58 +07:00
devlikepro 19b752bf15 [core] WEBJS - avoid "Can not get engine info" warning in logs 2025-05-28 16:41:58 +07:00
devlikepro 6528e5bba9 [core] WEBJS - hide "Fresh look" modal form
fix #987
2025-05-28 16:41:57 +07:00
devlikepro 37e1183e87 [core] Update nestjs 2025-05-28 16:41:56 +07:00
devlikepro 824512cf8e [core] add tini to Dockerfile
fix #990
2025-05-28 16:41:56 +07:00
devlikepro 82f296b17d [core] GOWS - Polls
fix devlikeapro/waha#899
2025-05-28 16:41:56 +07:00
devlikepro 8302530954 [core] 2025.5.4
Release / WEBJS - chrome - amd64 - chrome (push) Waiting to run
Release / WEBJS - chromium - amd64 - latest (push) Waiting to run
Release / WEBJS - chromium - linux/arm64 - arm (push) Waiting to run
Release / GOWS - none - amd64 - gows (push) Waiting to run
Release / GOWS - none - linux/arm64 - gows-arm (push) Waiting to run
Release / NOWEB - none - amd64 - noweb (push) Waiting to run
Release / NOWEB - none - linux/arm64 - noweb-arm (push) Waiting to run
2025-05-23 14:47:37 +07:00
devlikepro 6c271829aa [core] Up Dashboard 2025-05-23 14:47:37 +07:00
devlikepro 3b30d339ee [core] Up GOWS 2025-05-23 14:47:37 +07:00
devlikepro c5a268bdbd [core] Up NOWEB - lid support
fix #957
2025-05-23 14:47:36 +07:00
devlikepro 8457ea8e47 [core] Find a phone number by lid - do not require @lid 2025-05-23 14:47:36 +07:00
devlikepro 72d69eeab5 [core] Add lid to phone number mapping - all engines
fix #957
2025-05-23 14:47:35 +07:00
devlikepro 31885e41af [core] Use maxExecutionTime instead of timeout in async-lock to avoid "async-lock timed out in queue" error
fix #831
2025-05-23 14:47:34 +07:00
devlikepro e63726d18b [core] GOWS - WhatsApp Event Message 2025-05-23 14:47:33 +07:00
devlikepro 3d2ab45d25 [core] GOWS - Labels support
fix #922
2025-05-23 14:47:33 +07:00
devlikepro 246478e1d8 [core] GOWS - add send vcard
fix #902
2025-05-23 14:47:32 +07:00
devlikepro 9418de3b23 [core] add reply_to to sendContactVcard 2025-05-23 14:47:32 +07:00
devlikepro c6b68cad37 [core] add types for toVcard 2025-05-23 14:47:32 +07:00
devlikepro ef36e4d3ac [core] 2025.5.3
Release / WEBJS - chrome - amd64 - chrome (push) Waiting to run
Release / WEBJS - chromium - amd64 - latest (push) Waiting to run
Release / WEBJS - chromium - linux/arm64 - arm (push) Waiting to run
Release / GOWS - none - amd64 - gows (push) Waiting to run
Release / GOWS - none - linux/arm64 - gows-arm (push) Waiting to run
Release / NOWEB - none - amd64 - noweb (push) Waiting to run
Release / NOWEB - none - linux/arm64 - noweb-arm (push) Waiting to run
2025-05-16 18:33:10 +07:00
devlikepro ba2ddd62ad [core] NOWEB - fix poll.vote event
fix #960
2025-05-16 18:33:10 +07:00
devlikepro 6a03e1488c [core] 2025.5.2
Release / WEBJS - chrome - amd64 - chrome (push) Waiting to run
Release / WEBJS - chromium - amd64 - latest (push) Waiting to run
Release / WEBJS - chromium - linux/arm64 - arm (push) Waiting to run
Release / GOWS - none - amd64 - gows (push) Waiting to run
Release / GOWS - none - linux/arm64 - gows-arm (push) Waiting to run
Release / NOWEB - none - amd64 - noweb (push) Waiting to run
Release / NOWEB - none - linux/arm64 - noweb-arm (push) Waiting to run
2025-05-16 17:42:09 +07:00
devlikepro cabd2ed562 [core] GOWS - pairing with official account
fix #864
2025-05-16 17:42:09 +07:00
devlikepro 4a9296276d [core] NOWEB - fix pairing code issue
fix #961
2025-05-16 17:42:09 +07:00
devlikepro 1fd84d824c [core] senderKeyDistributionMessage - ignore undefined messages 2025-05-16 17:42:08 +07:00
devlikepro faa55ffe2d [core] 2025.5.1
Release / WEBJS - chrome - amd64 - chrome (push) Waiting to run
Release / WEBJS - chromium - amd64 - latest (push) Waiting to run
Release / WEBJS - chromium - linux/arm64 - arm (push) Waiting to run
Release / GOWS - none - amd64 - gows (push) Waiting to run
Release / GOWS - none - linux/arm64 - gows-arm (push) Waiting to run
Release / NOWEB - none - amd64 - noweb (push) Waiting to run
Release / NOWEB - none - linux/arm64 - noweb-arm (push) Waiting to run
2025-05-10 15:55:42 +07:00
devlikepro d45af4191f [core] WEBJS - properly close remote auth storage 2025-05-10 15:55:41 +07:00
devlikepro 9f8dead8b8 [core] NOWEB + SQL - speedup Contacts upsert
fix #952
2025-05-10 15:55:41 +07:00
devlikepro 564037cd53 [core] NOWEB + SQL - speedup Chats upsert
fix #952
2025-05-10 15:55:40 +07:00
devlikepro 434de6292c [core] SQL - Fix inserting duplicate entities in one batch 2025-05-10 15:55:40 +07:00
devlikepro a12e6990e5 [core] WEBJS - fix sorting groups in /chats and /chats/overview
fix #915
2025-05-10 15:55:40 +07:00
devlikepro ce0c4a094b [core] WEBJS - fix groups participant management (add/remove)
fix #944
2025-05-10 15:55:39 +07:00
devlikepro e36728ec08 [core] NOWEB - use ILogger 2025-05-10 15:55:39 +07:00
devlikepro 2c1f3090a0 [core] NOWEB - fix participant for message.ack in groups 2025-05-10 15:55:39 +07:00
devlikepro 0c6d4b3581 [core] NOWEB - update status only if not old 2025-05-10 15:55:39 +07:00
devlikepro 1d5b9eb241 [core] NOWEB - DistinctAck
fix #948
2025-05-10 15:55:38 +07:00
devlikepro b0567dafe3 [core] DistinctAck function 2025-05-10 15:55:37 +07:00
devlikepro 518f78f067 [core] Add StatusToAck and AckToStatus methods 2025-05-10 15:55:37 +07:00
devlikepro 47a2c21824 [core] Up GOWS
Release / WEBJS - chrome - amd64 - chrome (push) Waiting to run
Release / WEBJS - chromium - amd64 - latest (push) Waiting to run
Release / WEBJS - chromium - linux/arm64 - arm (push) Waiting to run
Release / GOWS - none - amd64 - gows (push) Waiting to run
Release / GOWS - none - linux/arm64 - gows-arm (push) Waiting to run
Release / NOWEB - none - amd64 - noweb (push) Waiting to run
Release / NOWEB - none - linux/arm64 - noweb-arm (push) Waiting to run
2025-05-05 13:08:45 +07:00
devlikepro aca5547965 [core] 2025.4.2 2025-05-05 13:08:45 +07:00
devlikepro 0e07876630 [core] Use POST in /api/chats/messages/read 2025-05-05 13:08:45 +07:00
devlikepro a43901b4a4 [core] /api/sendSeen - If no message id provided - switch to readChatMessages 2025-05-05 13:08:44 +07:00
devlikepro 6515fac16d [core] WEBJS - add message._data.receipts in group/status messages 2025-05-05 13:08:44 +07:00
devlikepro fdaa7e9f48 [core] Up Dashboard 2025-05-05 13:08:44 +07:00
devlikepro cd779850de [core] GOWS - fix missing DEVICE ack for groups 2025-05-05 13:08:44 +07:00
devlikepro ef939f4357 [core] Add "message.ack" event for groups and status
fix #495
fix #900
2025-05-05 13:08:41 +07:00
devlikepro bfb9dd7009 [core] WEBJS - Update chrome 2025-05-05 13:08:39 +07:00
devlikepro f282f14eff [core] WEBJS - Update puppeter 2025-05-05 13:08:38 +07:00
devlikepro 992b289500 [core] WEBJS - catch puppeter ProtocolError and "Promise was collected" and restart the browser
fix #244 - Promise was collected
fix #714 - Promise was collected
fix #685 - ProtocolError
fix #936 - ProtocolError
2025-05-05 13:08:37 +07:00
devlikepro 9dcdebcab2 [core] Add /api/chats/messages/read
fix #783
2025-05-05 13:08:36 +07:00
devlikepro c0b8bacab0 [core] Fix missing first message - GOWS, NOWEB 2025-05-05 13:08:35 +07:00
devlikepro f96e6420fc [core] Get message by id - filter by 'all' 2025-05-05 13:08:35 +07:00
devlikepro d7f97480cc [core] Do not send seen on empty keys 2025-05-05 13:08:35 +07:00
devlikepro 26df41b571 [core] Add isJidCus method 2025-05-05 13:08:34 +07:00
devlikepro 4ca336017d [core] GOWS - /api/sendSeen messageIds 2025-05-05 13:08:33 +07:00
devlikepro 94252ea54c [core] NOWEB - /api/sendSeen messageIds 2025-05-05 13:08:32 +07:00
devlikepro f8220fec1f [core] GOWS - send seen use messageIds 2025-05-05 13:08:32 +07:00
devlikepro a5ded396c9 [core] Add "audio-decode" to dependencies 2025-05-05 13:08:31 +07:00
devlikepro c457cf8064 [core] Add "node-cache" to dependencies 2025-05-05 13:08:30 +07:00
devlikepro 3079f90118 [core] Up NOWEB 2025-05-05 13:08:29 +07:00
devlikepro 4fbb331015 [core] Copy-paste in-memory store
Got removed from the upstream
2025-05-05 13:08:28 +07:00
devlikepro 640c0d305c [core] - Add 'filter.ack' to /messages 2025-05-05 13:08:26 +07:00
devlikepro 0b044c67bb [core] NOWEB fix ListMessages "Cannot read properties of undefined (reading 'protocolMessage')"
fix #932
2025-05-05 13:08:26 +07:00
devlikepro 7a37ba8145 [core] WEBJS - stop processing if we reach out the earliest message we need 2025-05-05 13:08:25 +07:00
devlikepro b447863d9f [core] NOWEB - update message status on /api/sendSeen API call
fix #635
2025-05-05 13:08:25 +07:00
devlikepro 6dd7e14f6f [core] NOWEB - set DEVICE message status for incoming messages if no status found 2025-05-05 13:08:24 +07:00
devlikepro b09ef98dce [core] default limit to 10 in messages 2025-05-05 13:08:24 +07:00
devlikepro ff1cf8a288 [core] gpt 2025-05-05 13:08:24 +07:00
devlikepro 603793662a [core] Wait 60 times in STARTING, approximately 2 minutes
fix #904
2025-05-05 13:08:24 +07:00
devlikepro 4cdab0dbc7 [core] admin/admin 2025-04-16 09:26:32 +07:00
devlikepro d745279fec [core] 2025.4.1
Release / WEBJS - chrome - amd64 - chrome (push) Waiting to run
Release / WEBJS - chromium - amd64 - latest (push) Waiting to run
Release / WEBJS - chromium - linux/arm64 - arm (push) Waiting to run
Release / GOWS - none - amd64 - gows (push) Waiting to run
Release / GOWS - none - linux/arm64 - gows-arm (push) Waiting to run
Release / NOWEB - none - amd64 - noweb (push) Waiting to run
Release / NOWEB - none - linux/arm64 - noweb-arm (push) Waiting to run
2025-04-09 16:05:03 +07:00
devlikepro df34c9f2fd [core] Up GOWS 2025-04-09 16:05:03 +07:00
devlikepro 37da9aabe3 [core] Add POST /api/send/link-review
fix #880
fix #596
2025-04-09 16:05:02 +07:00
devlikepro 3a2205147c [core] API - deprecate /sendLinkPreview 2025-04-09 16:05:02 +07:00
devlikepro 5c6acaf10a [core] NOWEB - filter out sender key distribution msg
fix #891
2025-04-09 16:05:02 +07:00
devlikepro d916f2dea5 [core] NOWEB - return error in case of malformed xml 2025-04-09 16:05:01 +07:00
devlikepro 254a1aedcb [core] NOWEB - send reaction to channel
fix #889
2025-04-09 16:05:01 +07:00
devlikepro dce3175388 [core] GOWS - fix sending reaction to DM/Groups for other participants 2025-04-09 16:05:01 +07:00
devlikepro e2b6b45418 [core] GOWS - send reaction to channel
fix #889
2025-04-09 16:05:01 +07:00
devlikepro e210d1e9fc [core] 2025.3.3
Release / WEBJS - chrome - amd64 - chrome (push) Waiting to run
Release / NOWEB - none - linux/arm64 - noweb-arm (push) Waiting to run
Release / WEBJS - chromium - amd64 - latest (push) Waiting to run
Release / WEBJS - chromium - linux/arm64 - arm (push) Waiting to run
Release / GOWS - none - amd64 - gows (push) Waiting to run
Release / GOWS - none - linux/arm64 - gows-arm (push) Waiting to run
Release / NOWEB - none - amd64 - noweb (push) Waiting to run
2025-03-31 15:39:56 +07:00
devlikepro df0f449e13 [core] use isJidBroadcast 2025-03-31 15:39:56 +07:00
devlikepro 895105c900 [core] Ignore @broadcast for picture
fix #878
fix #881
2025-03-31 15:39:56 +07:00
devlikepro 209538709b [core] Up Dashboard 2025-03-31 15:39:56 +07:00
devlikepro ef7c35286b [core] NOWEB - revert presence optimizations 2025-03-31 15:39:56 +07:00
devlikepro 70af0828cb [core] NOWEB - cache presence for 1 minute 2025-03-31 15:39:56 +07:00
devlikepro ef18e311b1 [core] 2025.3.2
Release / WEBJS - chrome - amd64 - chrome (push) Waiting to run
Release / WEBJS - chromium - amd64 - latest (push) Waiting to run
Release / WEBJS - chromium - linux/arm64 - arm (push) Waiting to run
Release / GOWS - none - amd64 - gows (push) Waiting to run
Release / GOWS - none - linux/arm64 - gows-arm (push) Waiting to run
Release / NOWEB - none - amd64 - noweb (push) Waiting to run
Release / NOWEB - none - linux/arm64 - noweb-arm (push) Waiting to run
2025-03-27 14:36:38 +07:00
devlikepro 68de84973a [core] GOWS - extract replyTo
fix #876
2025-03-27 14:36:38 +07:00
devlikepro ef6d4f9998 [core] Add _data (original message) to replyTo field 2025-03-27 14:36:37 +07:00
devlikepro 9f5413963b [core] NOWEB - log errors on profile picture change
fix #873
2025-03-27 14:36:36 +07:00
devlikepro ec7c0a5cb7 [core] check session name in waitUntilStatus 2025-03-27 14:36:36 +07:00
devlikepro f82bd9eadf [core] no browser for non webjs based engines 2025-03-27 14:36:35 +07:00
devlikepro 843c064ebe [core] Up GOWS 2025-03-27 14:36:35 +07:00
devlikepro 8a52e1d1d0 [core] Up Dashboard 2025-03-27 14:36:34 +07:00
devlikepro 8ec220dc9a [core] Status to 50K and more contacts
fix #858
2025-03-27 14:36:33 +07:00
devlikepro 9c4b9f4433 [core] NOWEB - fix scan qr issue 2025-03-27 14:36:33 +07:00
devlikepro 6059a29660 [core] fix message reactions empty for channels preview 2025-03-27 14:36:32 +07:00
devlikepro e65e6764fa [core] NOWEB - presence optimizations
Apply 1 hour ttl for presence information
2025-03-27 14:36:32 +07:00
devlikepro 1e77cb234a [core] NOWEB - use {} for presence 2025-03-27 14:36:31 +07:00
devlikepro 4a357087d1 [core] NOWEB - do not restart after SCAN_QR_CODE expires 2025-03-27 14:36:31 +07:00
devlikepro 6a3823cdff [core] NOWEB - fix the case when STARTING session fails the container
fix #821
fix #860
fix #869
2025-03-27 14:36:30 +07:00
devlikepro c2372ce96e [core] GOWS - fix get contact issue
fix #866
2025-03-27 14:36:30 +07:00
devlikepro 0f5e79b163 [core] Enable all features in Global Webhook
fix #855
2025-03-27 14:36:30 +07:00
devlikepro fa4026c83f [core] Allow only http/https for webhooks 2025-03-27 14:36:30 +07:00
devlikepro 3e4d66ca27 [core] Reply to button
fix #848
2025-03-27 14:36:30 +07:00
devlikepro 2ec94dc13c [core] 2025.3.1
Release / WEBJS - chrome - amd64 - chrome (push) Waiting to run
Release / WEBJS - chromium - amd64 - latest (push) Waiting to run
Release / WEBJS - chromium - linux/arm64 - arm (push) Waiting to run
Release / GOWS - none - amd64 - gows (push) Waiting to run
Release / GOWS - none - linux/arm64 - gows-arm (push) Waiting to run
Release / NOWEB - none - amd64 - noweb (push) Waiting to run
Release / NOWEB - none - linux/arm64 - noweb-arm (push) Waiting to run
2025-03-16 14:05:33 +07:00
devlikepro 6b11e08f3d [core] UP GOWS - v1.0.1 2025-03-16 14:05:33 +07:00
devlikepro e884e56b47 [core] Up Dashboard 2025-03-16 14:05:32 +07:00
devlikepro 2d74affed4 [core] NOWEB - send message save ids 2025-03-16 14:05:31 +07:00
devlikepro 3572b17049 [core] NOWEB up engine 2025-03-16 14:05:30 +07:00
devlikepro 4ee7091cd1 [core] Add /api/groups/count
fix #752
2025-03-16 14:05:29 +07:00
devlikepro ff5e89d97c [core] GOWS - no sender key dis message 2025-03-16 14:05:29 +07:00
devlikepro c4e82afc0c [core] log as debug engine info errors (not working perfectly in WEBJS)
fix #850
2025-03-16 14:05:29 +07:00
devlikepro 52c5d85487 [core] source: api|app for messages.
fix #787
2025-03-16 14:05:28 +07:00
devlikepro fd0438f80b [core] GOWS send _data with full message 2025-03-16 14:05:26 +07:00
devlikepro cb5781ca65 [core] NOWEB disappearing messages
fix #736
2025-03-16 14:05:25 +07:00
devlikepro 4d8aad9dfe [core] Remove calls from message events
fix #840
2025-03-16 14:05:24 +07:00
devlikepro 39429a2e44 [core] Add event.timestamp
fix #755
2025-03-16 14:05:23 +07:00
devlikepro 0a3514d81e [core] GOWS reply on message
fix #799
2025-03-16 14:05:22 +07:00
devlikepro 4e5e0a7c34 [core] GOWS fix filename in sendFile
fix #853
2025-03-16 14:05:22 +07:00
devlikepro f6cc6a9bef [core] GOWS edit message
fix #797
2025-03-16 14:05:20 +07:00
devlikepro 2e545e9fb9 [core] GOWS delete status message
fix #754
2025-03-16 14:05:19 +07:00
devlikepro d49ef64e7e [core] API - description for message id 2025-03-16 14:05:18 +07:00
devlikepro b1e6eaef90 [core] GOWS - delete message
fix #796
2025-03-16 14:05:16 +07:00
devlikepro 3420d0199b [core] NOWEB - fix getting message by id 2025-03-16 14:05:16 +07:00
devlikepro 671db35571 [core] NOWEB - link preview HQ 2025-03-16 14:05:16 +07:00
devlikepro d724f87726 [core] GOWS - link preview 2025-03-16 14:05:14 +07:00
devlikepro 5ab8565205 [core] NOWEB - link preview in status 2025-03-16 14:05:14 +07:00
devlikepro 660c567d1b [core] GOWS - link preview
fix #763
2025-03-16 14:05:14 +07:00
devlikepro 2c68d13c56 [core] Send session name back
fix #835
2025-03-16 14:05:14 +07:00
devlikeapro b9fc155b4f Update feature_request.md 2025-02-28 10:55:19 +07:00
devlikeapro 8dd5aefc81 Update feature_request.md 2025-02-28 10:55:05 +07:00
devlikeapro f9aaf5dcef Update bug_report.md 2025-02-28 10:53:52 +07:00
devlikepro 34a6e773df [core] 2025.2.8
Release / WEBJS - chrome - amd64 - chrome (push) Waiting to run
Release / WEBJS - chromium - amd64 - latest (push) Waiting to run
Release / WEBJS - chromium - linux/arm64 - arm (push) Waiting to run
Release / GOWS - none - amd64 - gows (push) Waiting to run
Release / GOWS - none - linux/arm64 - gows-arm (push) Waiting to run
Release / NOWEB - none - amd64 - noweb (push) Waiting to run
Release / NOWEB - none - linux/arm64 - noweb-arm (push) Waiting to run
2025-02-28 10:22:33 +07:00
devlikepro f0beb03344 [core] Link preview on edit
fix #833
2025-02-28 10:22:33 +07:00
devlikepro 1ff9a81ab3 [core] Fix channels API
fix #832
2025-02-28 10:22:33 +07:00
devlikepro 9b1771c693 [core] 2025.2.7
Release / WEBJS - chrome - amd64 - chrome (push) Waiting to run
Release / WEBJS - chromium - amd64 - latest (push) Waiting to run
Release / WEBJS - chromium - linux/arm64 - arm (push) Waiting to run
Release / GOWS - none - amd64 - gows (push) Waiting to run
Release / GOWS - none - linux/arm64 - gows-arm (push) Waiting to run
Release / NOWEB - none - amd64 - noweb (push) Waiting to run
Release / NOWEB - none - linux/arm64 - noweb-arm (push) Waiting to run
2025-02-27 14:34:23 +07:00
devlikepro 823e604737 [core] GOWS v1.0.0 2025-02-27 14:34:23 +07:00
devlikepro 97a31bdf17 [core] NOWEB - ignore EPHEMERAL_SYNC_RESPONSE 2025-02-27 14:34:22 +07:00
devlikepro ee7a198ae6 [core] GOWS - startTyping/stopTyping 2025-02-27 14:34:22 +07:00
devlikepro f143a51010 [core] group id in picture 2025-02-27 14:34:21 +07:00
devlikepro e483185b2c [core] fields.exclude optional 2025-02-27 14:34:21 +07:00
devlikepro 40e1c75746 [core] GOWS - v1.0.0-dev.1 2025-02-27 14:34:21 +07:00
devlikepro 456bd4693c [core] GOWS - fetch proto from ref with no v 2025-02-27 14:34:21 +07:00
devlikepro 5ac9c0f760 [core] NOWEB - no auto restart 2025-02-27 14:34:20 +07:00
devlikepro f621d84b83 [core] Exclude groups participants
fix #242
2025-02-27 14:34:20 +07:00
devlikepro 0dd001e95a [core] GOWS Groups API 2025-02-27 14:34:19 +07:00
devlikepro 69b5235db9 [core] Groups API - return 200 on POST in not create API 2025-02-27 14:34:19 +07:00
devlikepro a673d63b33 [core] Groups Picture API - Get, Update, Remove
fix #802
2025-02-27 14:34:18 +07:00
devlikepro 36485e10f2 [core] NOWEB add admin only / messages admin settings API
fix #695
fix #808
2025-02-27 14:34:18 +07:00
devlikepro 8a1a062127 [core] Place Channels Search API at the end 2025-02-27 14:34:18 +07:00
devlikepro 5961e0de08 [core] Fix group.participants undefined
fix #810
2025-02-27 14:34:18 +07:00
devlikepro 60bda521a0 [core] Wait until session is running and check if exist after 2025-02-27 14:34:18 +07:00
devlikepro 49f63c0f0d [core] 2025.2.6
Release / WEBJS - chrome - amd64 - chrome (push) Waiting to run
Release / WEBJS - chromium - amd64 - latest (push) Waiting to run
Release / WEBJS - chromium - linux/arm64 - arm (push) Waiting to run
Release / GOWS - none - amd64 - gows (push) Waiting to run
Release / GOWS - none - linux/arm64 - gows-arm (push) Waiting to run
Release / NOWEB - none - amd64 - noweb (push) Waiting to run
Release / NOWEB - none - linux/arm64 - noweb-arm (push) Waiting to run
fix #820
2025-02-21 10:06:27 +07:00
devlikepro 33910aedb8 [core] 2025.2.5
Release / WEBJS - chrome - amd64 - chrome (push) Waiting to run
Release / WEBJS - chromium - amd64 - latest (push) Waiting to run
Release / WEBJS - chromium - linux/arm64 - arm (push) Waiting to run
Release / GOWS - none - amd64 - gows (push) Waiting to run
Release / GOWS - none - linux/arm64 - gows-arm (push) Waiting to run
Release / NOWEB - none - amd64 - noweb (push) Waiting to run
Release / NOWEB - none - linux/arm64 - noweb-arm (push) Waiting to run
2025-02-19 09:20:27 +07:00
devlikepro 3359a81a14 [core] NOWEB add message.revoked event
fix #813
2025-02-19 09:20:26 +07:00
devlikepro e27f850e68 [core] NOWEB Fix body extract from messages
fix #813
2025-02-19 09:20:26 +07:00
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
311 changed files with 57432 additions and 5244 deletions

No files matched your search

+2
View File
@@ -1,4 +1,6 @@
Dockerfile
docker-compose
src/core/engines/gows/proto
.*sessions
sessions
files
+102 -158
View File
@@ -1,176 +1,120 @@
# WAHA Configuration
# WAHA - WhatsApp HTTP API
#
# https://waha.devlike.pro/docs/how-to/config/
#
# Common
#
# 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
#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
# https://en.wikipedia.org/wiki/List_of_tz_database_time_zones
#TZ=Europe/Dublin
#
# Security
#
# https://waha.devlike.pro/docs/how-to/security/
#
WHATSAPP_API_KEY=321
WAHA_DASHBOARD_USERNAME=waha
WAHA_DASHBOARD_PASSWORD=waha
#WHATSAPP_SWAGGER_USERNAME=admin
#WHATSAPP_SWAGGER_PASSWORD=admin
# ==================
# ===== COMMON =====
# ==================
# Base URL for the API (used for webhooks, file URLs, etc.)
WAHA_BASE_URL=http://localhost:3000
#
# Dashboard
#
# 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
# Server configuration (if you need to customize hostname/port)
# WHATSAPP_API_SCHEMA=http
# WHATSAPP_API_PORT=3000
# WHATSAPP_API_HOSTNAME=localhost
#
# Logging
# https://waha.devlike.pro/docs/how-to/observability/#logging
#
# Set log format to JSON if you consume logs with log management systems
WAHA_LOG_FORMAT=JSON
WAHA_LOG_LEVEL=info
# WAHA_LOG_FORMAT=PRETTY
# If you want to see more logs, you can set the log level to debug
#WAHA_LOG_LEVEL=debug
# DEBUG=1 # shortcut for setting log level to debug
#
# Engine
# https://waha.devlike.pro/docs/how-to/engines/
#
# Choose the right engine for your needs (WEBJS by default)
WHATSAPP_DEFAULT_ENGINE=WEBJS
# Use NOWEB engine for the fastest performance
#WHATSAPP_DEFAULT_ENGINE=NOWEB
# Worker
# WAHA_WORKER_ID=waha1
#
# Sessions
# https://waha.devlike.pro/docs/how-to/config/#sessions
#
# Do not print QR codes in logs
WAHA_PRINT_QR=False
#
# OR you can specify sessions by name, if you have handful amount of sessions
#WHATSAPP_START_SESSION=session1,session2
#
# Restart all sessions when container starts
# !!! Do not use it with multiple workers against one database !!!
# WHATSAPP_RESTART_ALL_SESSIONS=True
#
# Swagger
# https://waha.devlike.pro/docs/how-to/swagger/
#
# ====================
# ===== SECURITY =====
# ====================
# "sha512:{SHA}" format - below is "admin" api key
WAHA_API_KEY=sha512:c7ad44cbad762a5da0a452f9e854fdc1e0e7a52a38015f23f3eab1d80b931dd472634dfac71cd34ebc35d16ab7fb8a90c81f975113d6c7538dc69dd8de9077ec
WAHA_DASHBOARD_ENABLED=True
WAHA_DASHBOARD_USERNAME=admin
WAHA_DASHBOARD_PASSWORD=admin
WHATSAPP_SWAGGER_ENABLED=True
WHATSAPP_SWAGGER_USERNAME=admin
WHATSAPP_SWAGGER_PASSWORD=admin
# Disable Swagger
#WHATSAPP_SWAGGER_ENABLED=false
# Enable advanced Swagger configuration
#WHATSAPP_SWAGGER_CONFIG_ADVANCED=true
#
# Swagger White Label customization
# https://waha.devlike.pro/docs/how-to/swagger/#white-label
#WHATSAPP_SWAGGER_TITLE=WAHA API
#
#
# Global Proxy Settings
# https://waha.devlike.pro/docs/how-to/config/#global-proxy-configuration
#
# You can also set proxy settings for each session later when starting it
# https://waha.devlike.pro/docs/how-to/sessions/#configure-proxy
#WHATSAPP_PROXY_SERVER=proxy.example.com:3128
#WHATSAPP_PROXY_SERVER_USERNAME=user
#WHATSAPP_PROXY_SERVER_PASSWORD=pass
#
# WhatsApp engine (WEBJS is default, GOWS or NOWEB for better performance)
WHATSAPP_DEFAULT_ENGINE=WEBJS
#
# 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
#
# ===================
# ===== LOGGING =====
# ===================
# Log format: JSON (for log management systems) or PRETTY (for development)
WAHA_LOG_FORMAT=JSON
#
# Media Configuration
#
# https://waha.devlike.pro/docs/how-to/config/#files
#
# Disable media (images, videos, files) download for incoming messages
#WHATSAPP_DOWNLOAD_MEDIA=false
#
# Download only specific media types
#WHATSAPP_FILES_MIMETYPES=image/jpeg,image/png
#
# Log level: info, debug, error, warn
WAHA_LOG_LEVEL=info
# Media - Local Storage
# https://waha.devlike.pro/docs/how-to/storages/#media---local
#
# Set to 0 to keep media files forever
# https://waha.devlike.pro/docs/how-to/storages/#save-media-files-between-the-container-restarts
# Don't print QR codes in logs
WAHA_PRINT_QR=False
# =========================
# ===== MEDIA STORAGE =====
# =========================
# Local storage (default)
WAHA_MEDIA_STORAGE=LOCAL
WHATSAPP_FILES_LIFETIME=0
WHATSAPP_FILES_FOLDER=/app/.media
# Keep media files for 180 seconds (3 minutes)
#WHATSAPP_FILES_LIFETIME=180
#
# Media - S3 Storage
# https://waha.devlike.pro/docs/how-to/storages/#media---s3
#WAHA_MEDIA_STORAGE=S3
#WAHA_S3_REGION=eu-west-2
#WAHA_S3_BUCKET=waha
#WAHA_S3_ACCESS_KEY_ID=minioadmin
#WAHA_S3_SECRET_ACCESS_KEY=minioadmin
#WAHA_S3_ENDPOINT=http://minio:9000 # Not required if you're using AWS S3
#WAHA_S3_FORCE_PATH_STYLE=True # Required for Minio
#WAHA_S3_PROXY_FILES=True # Required for docker-compose setup
# Media download settings
# WHATSAPP_DOWNLOAD_MEDIA=true
# WHATSAPP_FILES_MIMETYPES=image/jpeg,image/png
#
# Global Webhooks
#
# https://waha.devlike.pro/docs/how-to/config/#webhooks
# https://waha.devlike.pro/docs/how-to/webhooks/
#WHATSAPP_HOOK_URL=https://webhook.site/11111111-1111-1111-1111-11111111
#WHATSAPP_HOOK_EVENTS=session.status,message,message.reaction
#
# S3 storage (uncomment to use)
# WAHA_MEDIA_STORAGE=S3
# WAHA_S3_REGION=eu-west-2
# WAHA_S3_BUCKET=waha
# WAHA_S3_ACCESS_KEY_ID=minioadmin
# WAHA_S3_SECRET_ACCESS_KEY=minioadmin
# WAHA_S3_ENDPOINT=http://minio:9000
# WAHA_S3_FORCE_PATH_STYLE=True
# WAHA_S3_PROXY_FILES=True
#
# Local Configuration
#
# WAHA_LOCAL_STORE_BASE_DIR=/app/sessions
# Remember to map the volume to the host machine to the right direction in "volumes" field in docker-compose.yml
# volumes:
# - './.sessions:/app/sessions'
# PostgreSQL storage (uncomment to use)
# WAHA_MEDIA_STORAGE=POSTGRESQL
# WAHA_MEDIA_POSTGRESQL_URL=postgres://postgres:postgres@postgres:5432/postgres?sslmode=disable
#
# MongoDB Configuration
# https://waha.devlike.pro/docs/how-to/storages/#sessions---mongodb
#WHATSAPP_SESSIONS_MONGO_URL=mongodb://mongouser:mongopassword@mongodb:27017
#
# ===========================
# ===== SESSION STORAGE =====
# ===========================
# PostgreSQL for sessions (uncomment to use)
# WHATSAPP_SESSIONS_POSTGRESQL_URL=postgres://postgres:postgres@postgres:5432/postgres?sslmode=disable
# MongoDB for sessions (uncomment to use)
# WHATSAPP_SESSIONS_MONGO_URL=mongodb://mongouser:mongopassword@mongodb:27017
# =================
# ===== APPS ======
# =================
# WAHA_APPS_ENABLED=True
# REDIS_URL=redis://:redis@redis:6379
# Apps Jobs configuration - *_AGE in seconds
# WAHA_APPS_JOBS_REMOVE_ON_COMPLETE_AGE=
# WAHA_APPS_JOBS_REMOVE_ON_COMPLETE_COUNT=
# WAHA_APPS_JOBS_REMOVE_ON_FAIL_AGE=
# WAHA_APPS_JOBS_REMOVE_ON_FAIL_COUNT
# ==================================
# ===== ADVANCED CONFIGURATION =====
# ==================================
# Timezone for screenshots and logs
# TZ=Europe/Dublin
# Session management
# WHATSAPP_START_SESSION=session1,session2
# WHATSAPP_RESTART_ALL_SESSIONS=False
# Webhooks
# WHATSAPP_HOOK_URL=https://webhook.site/11111111-1111-1111-1111-11111111
# WHATSAPP_HOOK_CUSTOM_HEADERS=custom_header_key:custom_header_value
# WHATSAPP_HOOK_EVENTS=session.status,message,message.reaction
# Proxy configuration
# WHATSAPP_PROXY_SERVER=proxy.example.com:3128
# WHATSAPP_PROXY_SERVER_USERNAME=user
# WHATSAPP_PROXY_SERVER_PASSWORD=pass
# HTTPS configuration
# !DEPRECATED!
# Setup nginx reverse proxy to handle TLS connection
# using Let's encrypt or self-issued certificate
# 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
+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
+23 -3
View File
@@ -45,7 +45,7 @@ jobs:
# browser: "chrome"
# goss: 'goss-linux-arm'
# No browser - x86
# NOWEB - x86
- runner: 'buildjet-4vcpu-ubuntu-2204'
tag: 'noweb'
platform: 'amd64'
@@ -53,7 +53,7 @@ jobs:
engine: 'NOWEB'
goss: 'goss-linux-amd64'
# No browser - ARM
# NOWEB - ARM
- runner: 'buildjet-4vcpu-ubuntu-2204-arm'
tag: 'noweb-arm'
platform: 'linux/arm64'
@@ -61,6 +61,22 @@ jobs:
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:
- name: Checkout
uses: actions/checkout@v3
@@ -110,7 +126,11 @@ jobs:
working-directory: tests/smoke
timeout-minutes: 2
run: |
docker run -d --name smoke --rm -p3000:3000 ${{ vars.DOCKER_IMAGE }}:${{ matrix.tag }}
docker run -d \
--name smoke \
--rm -p3000:3000 \
${{ vars.DOCKER_IMAGE }}:${{ matrix.tag }}
sleep 3
docker logs smoke
goss validate --retry-timeout 30s --sleep 1s
+1
View File
@@ -1,3 +1,4 @@
src/core/engines/gows/proto
.env
*.heapsnapshot
.yarn
+1 -1
View File
@@ -1 +1 @@
v22.8
v22.16
+5
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/.*
)$
@@ -37,6 +39,7 @@ repos:
(?x)^(
docs/.*|
README.md|
tests/smoke/goss.yaml|
)$
- repo: https://github.com/Lucas-C/pre-commit-hooks-nodejs
rev: v1.1.2
@@ -73,4 +76,6 @@ repos:
.pre-commit-config.yaml|
^src/core/engines/webjs/session.webjs.core.ts|
^src/core/engines/webjs/_lodash.js|
^tests/perf/.*|
^scripts/.*|
)$
+105 -14
View File
@@ -1,44 +1,88 @@
ARG NODE_IMAGE_TAG=22.16-bookworm-slim
ARG GOLANG_IMAGE_TAG=1.23-bookworm
#
# Build
#
ARG NODE_VERSION=22.8-bullseye
FROM node:${NODE_VERSION} AS build
FROM node:${NODE_IMAGE_TAG} AS build
ENV PUPPETEER_SKIP_DOWNLOAD=True
# npm packages
WORKDIR /src
WORKDIR /git
COPY package.json .
COPY yarn.lock .
ENV YARN_CHECKSUM_BEHAVIOR=update
# git
RUN apt-get update && apt-get install -y git
RUN npm install -g corepack && corepack enable
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
#
# Dashboard
#
FROM node:${NODE_VERSION} AS dashboard
FROM node:${NODE_IMAGE_TAG} 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/*
# wget, unzip
RUN apt-get update && apt-get install -y wget unzip && 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:${GOLANG_IMAGE_TAG} 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
#
FROM node:${NODE_VERSION} AS release
FROM node:${NODE_IMAGE_TAG} AS release
ENV PUPPETEER_SKIP_DOWNLOAD=True
# Quick fix for memory potential memory leaks
# https://github.com/devlikeapro/waha/issues/347
@@ -51,6 +95,11 @@ 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 \
@@ -58,6 +107,13 @@ RUN if [ "$USE_BROWSER" = "chromium" ] || [ "$USE_BROWSER" = "chrome" ]; then \
&& rm -rf /var/lib/apt/lists/*; \
fi
# Install wget - either for chromium or chrome
RUN if [ "$USE_BROWSER" = "chromium" ] || [ "$USE_BROWSER" = "chrome" ]; then \
apt-get update \
&& apt-get install -y wget \
&& rm -rf /var/lib/apt/lists/*; \
fi
# Install fonts if using either chromium or chrome
RUN if [ "$USE_BROWSER" = "chromium" ] || [ "$USE_BROWSER" = "chrome" ]; then \
apt-get update \
@@ -79,6 +135,21 @@ RUN if [ "$USE_BROWSER" = "chromium" ] || [ "$USE_BROWSER" = "chrome" ]; then \
&& rm -rf /var/lib/apt/lists/*; \
fi
# Install xvfb, xauth
RUN if [ "$USE_BROWSER" = "chromium" ] || [ "$USE_BROWSER" = "chrome" ]; then \
apt-get update && apt-get install -y --no-install-recommends \
xvfb \
xauth \
libnss3 \
libxss1 \
libasound2 \
libatk-bridge2.0-0 \
libgtk-3-0 \
libdrm2 \
ca-certificates \
&& rm -rf /var/lib/apt/lists/*; \
fi
# Install Chromium
RUN if [ "$USE_BROWSER" = "chromium" ]; then \
apt-get update \
@@ -91,7 +162,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="137.0.7151.103-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 \
@@ -100,15 +171,33 @@ RUN if [ "$USE_BROWSER" = "chrome" ]; then \
&& rm -rf /var/lib/apt/lists/*; \
fi
# Set the ENV for NOWEB docker image
# curl
RUN apt-get update \
&& apt-get install -y curl \
&& rm -rf /var/lib/apt/lists/*
# GOWS requirements
# libc6
RUN apt-get update \
&& apt-get install -y libc6 \
&& rm -rf /var/lib/apt/lists/*
# Install tini for proper init process
RUN apt-get update && apt-get install -y tini && 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
@@ -120,4 +209,6 @@ ENV WAHA_ZIPPER=ZIPUNZIP
# Run command, etc
EXPOSE 3000
# Use tini as init system to handle zombie processes properly
ENTRYPOINT ["/usr/bin/tini", "--"]
CMD ["/entrypoint.sh"]
+26 -7
View File
@@ -1,16 +1,19 @@
build-all: build-plus build-chrome build-gows build-noweb
build:
docker build . -t devlikeapro/waha
build-plus:
docker build . -t devlikeapro/waha-plus
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 --build-arg WHATSAPP_DEFAULT_ENGINE=NOWEB
docker build . -t devlikeapro/waha-plus:noweb --build-arg USE_BROWSER=none --build-arg WHATSAPP_DEFAULT_ENGINE=NOWEB
build-all: build build-chrome build-noweb
build-plus:
docker build . -t devlikeapro/waha-plus
build-gows:
docker build . -t devlikeapro/waha-plus:gows --build-arg USE_BROWSER=none --build-arg WHATSAPP_DEFAULT_ENGINE=GOWS
build-ssh:
# check IMAGE provided
@@ -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-07-04
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
. ${NVM_DIR}/nvm.sh && nvm exec yarn up whatsapp-web.js@github:devlikeapro/whatsapp-web.js#fork-main-2025-06-09
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 (check [.nvmrc](/.nvmrc) to get the version)
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
```
+49 -5
View File
@@ -8,6 +8,17 @@ services:
# WAHA Core
# image: devlikeapro/waha:latest
# Add "dns" if you have a problem with resolving "web.whatsapp.com"
dns:
- 1.1.1.1
- 8.8.8.8
logging:
driver: 'json-file'
options:
max-size: '100m'
max-file: '10'
ports:
- '127.0.0.1:3000:3000/tcp'
@@ -17,14 +28,37 @@ services:
# Save media files
# https://waha.devlike.pro/docs/how-to/storages/#save-media-files-between-the-container-restarts
- './.media:/app/.media'
- './media:/app/.media'
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:
# - "127.0.0.1:5432:5432"
# volumes:
# - pg_data:/var/lib/postgresql/data
# command:
# - postgres
# - "-c"
# - "max_connections=3000"
# logging:
# driver: "json-file"
# options:
# max-size: "100m"
# max-file: "10"
# 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
@@ -35,6 +69,11 @@ services:
# environment:
# - MONGO_INITDB_ROOT_USERNAME=mongouser
# - MONGO_INITDB_ROOT_PASSWORD=mongopassword
# logging:
# driver: "json-file"
# options:
# max-size: "100m"
# max-file: "10"
# NOTE: Only if you're using S3 to save media files
# https://waha.devlike.pro/docs/how-to/storages/#media---s3
@@ -53,8 +92,13 @@ services:
# volumes:
# - minio_data:/data
# command: server /data --console-address ":9001"
# logging:
# driver: "json-file"
# options:
# max-size: "100m"
# max-file: "10"
volumes:
# NOTE: Only if you're using MongoDB
mongodb_data: {}
minio_data: {}
pg_data: {}
+260
View File
@@ -0,0 +1,260 @@
# Learn about the various environment variables at
# https://www.chatwoot.com/docs/self-hosted/configuration/environment-variables/#rails-production-variables
# Used to verify the integrity of signed cookies. so ensure a secure value is set
# SECRET_KEY_BASE should be alphanumeric. Avoid special characters or symbols.
# Use `rake secret` to generate this variable
SECRET_KEY_BASE=replace_with_lengthy_secure_hex
# Replace with the URL you are planning to use for your app
FRONTEND_URL=http://chatwoot:3009
# To use a dedicated URL for help center pages
# HELPCENTER_URL=http://0.0.0.0:3000
# If the variable is set, all non-authenticated pages would fallback to the default locale.
# Whenever a new account is created, the default language will be DEFAULT_LOCALE instead of en
# DEFAULT_LOCALE=en
# If you plan to use CDN for your assets, set Asset CDN Host
ASSET_CDN_HOST=
# Force all access to the app over SSL, default is set to false
FORCE_SSL=false
# This lets you control new sign ups on your chatwoot installation
# true : default option, allows sign ups
# false : disables all the end points related to sign ups
# api_only: disables the UI for signup, but you can create sign ups via the account apis
ENABLE_ACCOUNT_SIGNUP=false
# Redis config
# specify the configs via single URL or individual variables
# ref: https://www.iana.org/assignments/uri-schemes/prov/redis
# You can also use the following format for the URL: redis://:password@host:port/db_number
REDIS_URL=redis://redis:6379
# If you are using docker-compose, set this variable's value to be any string,
# which will be the password for the redis service running inside the docker-compose
# to make it secure
REDIS_PASSWORD=redis
# Redis Sentinel can be used by passing list of sentinel host and ports e,g. sentinel_host1:port1,sentinel_host2:port2
REDIS_SENTINELS=
# Redis sentinel master name is required when using sentinel, default value is "mymaster".
# You can find list of master using "SENTINEL masters" command
REDIS_SENTINEL_MASTER_NAME=
# By default Chatwoot will pass REDIS_PASSWORD as the password value for sentinels
# Use the following environment variable to customize passwords for sentinels.
# Use empty string if sentinels are configured with out passwords
# REDIS_SENTINEL_PASSWORD=
# Redis premium breakage in heroku fix
# enable the following configuration
# ref: https://github.com/chatwoot/chatwoot/issues/2420
# REDIS_OPENSSL_VERIFY_MODE=none
# Postgres Database config variables
# You can leave POSTGRES_DATABASE blank. The default name of
# the database in the production environment is chatwoot_production
POSTGRES_DATABASE=chatwoot
POSTGRES_HOST=postgres
POSTGRES_USERNAME=postgres
POSTGRES_PASSWORD=postgres
RAILS_ENV=development
# Changes the Postgres query timeout limit. The default is 14 seconds. Modify only when required.
# POSTGRES_STATEMENT_TIMEOUT=14s
RAILS_MAX_THREADS=5
# The email from which all outgoing emails are sent
# could user either `email@yourdomain.com` or `BrandName <email@yourdomain.com>`
MAILER_SENDER_EMAIL=Chatwoot <accounts@chatwoot.com>
#SMTP domain key is set up for HELO checking
SMTP_DOMAIN=chatwoot.com
# Set the value to "mailhog" if using docker-compose for development environments,
# Set the value as "localhost" or your SMTP address in other environments
# If SMTP_ADDRESS is empty, Chatwoot would try to use sendmail(postfix)
SMTP_ADDRESS=
SMTP_PORT=1025
SMTP_USERNAME=
SMTP_PASSWORD=
# plain,login,cram_md5
SMTP_AUTHENTICATION=
SMTP_ENABLE_STARTTLS_AUTO=true
# Can be: 'none', 'peer', 'client_once', 'fail_if_no_peer_cert', see http://api.rubyonrails.org/classes/ActionMailer/Base.html
SMTP_OPENSSL_VERIFY_MODE=peer
# Comment out the following environment variables if required by your SMTP server
# SMTP_TLS=
# SMTP_SSL=
# SMTP_OPEN_TIMEOUT
# SMTP_READ_TIMEOUT
# Mail Incoming
# This is the domain set for the reply emails when conversation continuity is enabled
MAILER_INBOUND_EMAIL_DOMAIN=
# Set this to the appropriate ingress channel with regards to incoming emails
# Possible values are :
# relay for Exim, Postfix, Qmail
# mailgun for Mailgun
# mandrill for Mandrill
# postmark for Postmark
# sendgrid for Sendgrid
RAILS_INBOUND_EMAIL_SERVICE=
# Use one of the following based on the email ingress service
# Ref: https://edgeguides.rubyonrails.org/action_mailbox_basics.html
# Set this to a password of your choice and use it in the Inbound webhook
RAILS_INBOUND_EMAIL_PASSWORD=
MAILGUN_INGRESS_SIGNING_KEY=
MANDRILL_INGRESS_API_KEY=
# Creating Your Inbound Webhook Instructions for Postmark and Sendgrid:
# Inbound webhook URL format:
# https://actionmailbox:[YOUR_RAILS_INBOUND_EMAIL_PASSWORD]@[YOUR_CHATWOOT_DOMAIN.COM]/rails/action_mailbox/[RAILS_INBOUND_EMAIL_SERVICE]/inbound_emails
# Note: Replace the values inside the brackets; do not include the brackets themselves.
# Example: https://actionmailbox:mYRandomPassword3@chatwoot.example.com/rails/action_mailbox/postmark/inbound_emails
# For Postmark
# Ensure the 'Include raw email content in JSON payload' checkbox is selected in the inbound webhook section.
# Storage
ACTIVE_STORAGE_SERVICE=local
# Amazon S3
# documentation: https://www.chatwoot.com/docs/configuring-s3-bucket-as-cloud-storage
S3_BUCKET_NAME=
AWS_ACCESS_KEY_ID=
AWS_SECRET_ACCESS_KEY=
AWS_REGION=
# Log settings
# Disable if you want to write logs to a file
RAILS_LOG_TO_STDOUT=true
LOG_LEVEL=info
LOG_SIZE=500
# Configure this environment variable if you want to use lograge instead of rails logger
#LOGRAGE_ENABLED=true
### This environment variables are only required if you are setting up social media channels
# Facebook
# documentation: https://www.chatwoot.com/docs/facebook-setup
FB_VERIFY_TOKEN=
FB_APP_SECRET=
FB_APP_ID=
# https://developers.facebook.com/docs/messenger-platform/instagram/get-started#app-dashboard
IG_VERIFY_TOKEN=
# Twitter
# documentation: https://www.chatwoot.com/docs/twitter-app-setup
TWITTER_APP_ID=
TWITTER_CONSUMER_KEY=
TWITTER_CONSUMER_SECRET=
TWITTER_ENVIRONMENT=
#slack integration
SLACK_CLIENT_ID=
SLACK_CLIENT_SECRET=
# Google OAuth
GOOGLE_OAUTH_CLIENT_ID=
GOOGLE_OAUTH_CLIENT_SECRET=
GOOGLE_OAUTH_CALLBACK_URL=
### Change this env variable only if you are using a custom build mobile app
## Mobile app env variables
IOS_APP_ID=L7YLMN4634.com.chatwoot.app
ANDROID_BUNDLE_ID=com.chatwoot.app
# https://developers.google.com/android/guides/client-auth (use keytool to print the fingerprint in the first section)
ANDROID_SHA256_CERT_FINGERPRINT=AC:73:8E:DE:EB:56:EA:CC:10:87:02:A7:65:37:7B:38:D4:5D:D4:53:F8:3B:FB:D3:C6:28:64:1D:AA:08:1E:D8
### Smart App Banner
# https://developer.apple.com/library/archive/documentation/AppleApplications/Reference/SafariWebContent/PromotingAppswithAppBanners/PromotingAppswithAppBanners.html
# You can find your app-id in https://itunesconnect.apple.com
#IOS_APP_IDENTIFIER=1495796682
## Push Notification
## generate a new key value here : https://d3v.one/vapid-key-generator/
# VAPID_PUBLIC_KEY=
# VAPID_PRIVATE_KEY=
#
# for mobile apps
# FCM_SERVER_KEY=
### APM and Error Monitoring configurations
## Elastic APM
## https://www.elastic.co/guide/en/apm/agent/ruby/current/getting-started-rails.html
# ELASTIC_APM_SERVER_URL=
# ELASTIC_APM_SECRET_TOKEN=
## Sentry
# SENTRY_DSN=
## Scout
## https://scoutapm.com/docs/ruby/configuration
# SCOUT_KEY=YOURKEY
# SCOUT_NAME=YOURAPPNAME (Production)
# SCOUT_MONITOR=true
## NewRelic
# https://docs.newrelic.com/docs/agents/ruby-agent/configuration/ruby-agent-configuration/
# NEW_RELIC_LICENSE_KEY=
# Set this to true to allow newrelic apm to send logs.
# This is turned off by default.
# NEW_RELIC_APPLICATION_LOGGING_ENABLED=
## Datadog
## https://github.com/DataDog/dd-trace-rb/blob/master/docs/GettingStarted.md#environment-variables
# DD_TRACE_AGENT_URL=
# MaxMindDB API key to download GeoLite2 City database
# IP_LOOKUP_API_KEY=
## Rack Attack configuration
## To prevent and throttle abusive requests
# ENABLE_RACK_ATTACK=true
# RACK_ATTACK_LIMIT=300
# ENABLE_RACK_ATTACK_WIDGET_API=true
## Running chatwoot as an API only server
## setting this value to true will disable the frontend dashboard endpoints
# CW_API_ONLY_SERVER=false
## Development Only Config
# if you want to use letter_opener for local emails
# LETTER_OPENER=true
# meant to be used in github codespaces
# WEBPACKER_DEV_SERVER_PUBLIC=
# If you want to use official mobile app,
# the notifications would be relayed via a Chatwoot server
ENABLE_PUSH_RELAY_SERVER=true
# Stripe API key
STRIPE_SECRET_KEY=
STRIPE_WEBHOOK_SECRET=
# Set to true if you want to upload files to cloud storage using the signed url
# Make sure to follow https://edgeguides.rubyonrails.org/active_storage_overview.html#cross-origin-resource-sharing-cors-configuration on the cloud storage after setting this to true.
DIRECT_UPLOADS_ENABLED=
#MS OAUTH creds
AZURE_APP_ID=
AZURE_APP_SECRET=
## Advanced configurations
## Change these values to fine tune performance
# control the concurrency setting of sidekiq
# SIDEKIQ_CONCURRENCY=10
# AI powered features
## OpenAI key
# OPENAI_API_KEY=
# Housekeeping/Performance related configurations
# Set to true if you want to remove stale contact inboxes
# contact_inboxes with no conversation older than 90 days will be removed
# REMOVE_STALE_CONTACT_INBOX_JOB_STATUS=false
+115
View File
@@ -0,0 +1,115 @@
# WAHA - WhatsApp HTTP API
#
# https://waha.devlike.pro/docs/how-to/config/
#
# ==================
# ===== COMMON =====
# ==================
# Base URL for the API (used for webhooks, file URLs, etc.)
WAHA_BASE_URL=http://waha:3000
# Server configuration (if you need to customize hostname/port)
# WHATSAPP_API_SCHEMA=http
# WHATSAPP_API_PORT=3000
# WHATSAPP_API_HOSTNAME=localhost
# ====================
# ===== SECURITY =====
# ====================
# WAHA Apps requires both sha and plain version
WAHA_API_KEY_PLAIN=00000000000000000000000000000000
WAHA_API_KEY=sha512:98b6d128682e280b74b324ca82a6bae6e8a3f7174e0605bfd52eb9948fad8984854ec08f7652f32055c4a9f12b69add4850481d9503a7f2225501671d6124648
WAHA_DASHBOARD_USERNAME=admin
WAHA_DASHBOARD_PASSWORD=11111111111111111111111111111111
WHATSAPP_SWAGGER_USERNAME=admin
WHATSAPP_SWAGGER_PASSWORD=11111111111111111111111111111111
WAHA_DASHBOARD_ENABLED=True
WHATSAPP_SWAGGER_ENABLED=True
# WhatsApp engine (WEBJS is default, GOWS or NOWEB for better performance)
WHATSAPP_DEFAULT_ENGINE=GOWS
# =================
# ===== APPS ======
# =================
WAHA_APPS_ENABLED=True
REDIS_URL=redis://:redis@redis:6379
# ===================
# ===== LOGGING =====
# ===================
# Log format: JSON (for log management systems) or PRETTY (for development)
WAHA_LOG_FORMAT=JSON
# Log level: info, debug, error, warn
WAHA_LOG_LEVEL=info
# Don't print QR codes in logs
WAHA_PRINT_QR=False
# =========================
# ===== MEDIA STORAGE =====
# =========================
# Local storage (default)
WAHA_MEDIA_STORAGE=LOCAL
WHATSAPP_FILES_LIFETIME=1800
WHATSAPP_FILES_FOLDER=/app/.media
# Media download settings
# WHATSAPP_DOWNLOAD_MEDIA=true
# WHATSAPP_FILES_MIMETYPES=image/jpeg,image/png
# S3 storage (uncomment to use)
# WAHA_MEDIA_STORAGE=S3
# WAHA_S3_REGION=eu-west-2
# WAHA_S3_BUCKET=waha
# WAHA_S3_ACCESS_KEY_ID=minioadmin
# WAHA_S3_SECRET_ACCESS_KEY=minioadmin
# WAHA_S3_ENDPOINT=http://minio:9000
# WAHA_S3_FORCE_PATH_STYLE=True
# WAHA_S3_PROXY_FILES=True
# PostgreSQL storage (uncomment to use)
# WAHA_MEDIA_STORAGE=POSTGRESQL
# WAHA_MEDIA_POSTGRESQL_URL=postgres://postgres:postgres@postgres:5432/postgres?sslmode=disable
# ===========================
# ===== SESSION STORAGE =====
# ===========================
# PostgreSQL for sessions (uncomment to use)
WHATSAPP_SESSIONS_POSTGRESQL_URL=postgres://postgres:postgres@postgres:5432/postgres?sslmode=disable
# MongoDB for sessions (uncomment to use)
# WHATSAPP_SESSIONS_MONGO_URL=mongodb://mongouser:mongopassword@mongodb:27017
# ==================================
# ===== ADVANCED CONFIGURATION =====
# ==================================
# Timezone for screenshots and logs
# TZ=Europe/Dublin
# Session management
# WHATSAPP_START_SESSION=session1,session2
# WHATSAPP_RESTART_ALL_SESSIONS=False
# Webhooks
# WHATSAPP_HOOK_URL=https://webhook.site/11111111-1111-1111-1111-11111111
# WHATSAPP_HOOK_EVENTS=session.status,message,message.reaction
# Proxy configuration
# WHATSAPP_PROXY_SERVER=proxy.example.com:3128
# WHATSAPP_PROXY_SERVER_USERNAME=user
# WHATSAPP_PROXY_SERVER_PASSWORD=pass
# HTTPS configuration
# !DEPRECATED!
# Setup nginx reverse proxy to handle TLS connection
# using Let's encrypt or self-issued certificate
# 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
@@ -0,0 +1,97 @@
services:
waha:
restart: always
image: devlikeapro/waha-plus
ports:
- '127.0.0.1:3000:3000/tcp'
volumes:
- './sessions:/app/.sessions' # required for WAHA Core only
- './media:/app/.media'
env_file:
- .waha.env
depends_on:
- postgres
- redis
- chatwoot
- chatwoot-sidekiq
logging:
driver: 'json-file'
options:
max-size: '100m'
max-file: '10'
dns:
- 1.1.1.1
- 8.8.8.8
base: &base
image: chatwoot/chatwoot:v4.3.0
env_file: .chatwoot.env ## Change this file for customized env variables
volumes:
- chatwoot_storage:/app/storage
logging:
driver: 'json-file'
options:
max-size: '100m'
max-file: '10'
chatwoot:
<<: *base
depends_on:
- postgres
- redis
ports:
- '127.0.0.1:3009:3009'
environment:
- NODE_ENV=production
- RAILS_ENV=production
- INSTALLATION_ENV=docker
entrypoint: docker/entrypoints/rails.sh
command: ['bundle', 'exec', 'rails', 's', '-p', '3009', '-b', '0.0.0.0']
restart: always
chatwoot-sidekiq:
<<: *base
depends_on:
- postgres
- redis
environment:
- NODE_ENV=production
- RAILS_ENV=production
- INSTALLATION_ENV=docker
command: ['bundle', 'exec', 'sidekiq', '-C', 'config/sidekiq.yml']
restart: always
postgres:
image: pgvector/pgvector:pg16
restart: always
ports:
- '127.0.0.1:5432:5432'
volumes:
- chatwoot_pg_data:/var/lib/postgresql/data
environment:
- POSTGRES_DB=chatwoot
- POSTGRES_USER=postgres
# Please provide your own password.
- POSTGRES_PASSWORD=postgres
- POSTGRES_HOST_AUTH_METHOD=trust
redis:
image: redis:alpine
restart: always
command:
- 'sh'
- '-c'
- >
redis-server --bind 0.0.0.0 --port 6379 --requirepass
${REDIS_PASSWORD:-redis} --timeout 300 --tcp-keepalive 60 --save 900 1
--save 300 10 --save 60 10000 --appendonly yes --appendfsync everysec
env_file: .chatwoot.env
volumes:
- chatwoot_redis:/data
ports:
- '127.0.0.1:6379:6379'
volumes:
chatwoot_pg_data: {}
chatwoot_redis: {}
chatwoot_storage: {}
+2 -2
View File
@@ -17,7 +17,7 @@ services:
ports:
- '127.0.0.1:3001:3000/tcp'
volumes:
- './.media:/app/.media'
- './media:/app/.media'
env_file:
- .env
environment:
@@ -42,7 +42,7 @@ services:
ports:
- '127.0.0.1:3002:3000/tcp'
volumes:
- './.media:/app/.media'
- './media:/app/.media'
env_file:
- .env
environment:
+5 -5
View File
@@ -7,12 +7,12 @@ services:
ports:
- '3000:3000/tcp'
volumes:
- './.sessions:/app/.sessions'
- './.media:/app/.media'
- './sessions:/app/.sessions'
- './media:/app/.media'
environment:
- WHATSAPP_API_KEY=321
- WAHA_DASHBOARD_USERNAME=waha
- WAHA_DASHBOARD_PASSWORD=waha
- WAHA_API_KEY=321
- WAHA_DASHBOARD_USERNAME=admin
- WAHA_DASHBOARD_PASSWORD=admin
- WHATSAPP_DEFAULT_ENGINE=WEBJS
- WAHA_PRINT_QR=False
- WAHA_MEDIA_STORAGE=LOCAL
+4 -4
View File
@@ -29,15 +29,15 @@ services:
# - WHATSAPP_HOOK_EVENTS=message
# - WHATSAPP_API_HOSTNAME=localhost
- WHATSAPP_DEFAULT_ENGINE=WEBJS
- WHATSAPP_API_KEY=321
- WAHA_API_KEY=321
# Username and password for Swagger
- WHATSAPP_SWAGGER_USERNAME=swagger
- WHATSAPP_SWAGGER_PASSWORD=admin
# 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
@@ -55,7 +55,7 @@ services:
# Environment variables from https://waha.devlike.pro/docs/how-to/config/
# - WHATSAPP_HOOK_URL=https://webhook.site/11111111-1111-1111-1111-11111111
# - WHATSAPP_HOOK_EVENTS=message
# - WHATSAPP_API_KEY=321
# - WAHA_API_KEY=321
# - WHATSAPP_SWAGGER_USERNAME=admin
# - WHATSAPP_SWAGGER_PASSWORD=123
# - WAHA_DASHBOARD_USERNAME=admin
+79 -3
View File
@@ -1,11 +1,87 @@
#!/bin/sh
#
# Calculate UV_THREADPOOL_SIZE based on number of CPUs
cpus=$(node -e "const os = require('os'); console.log(os.cpus().length);")
#
# Try to get CPU count using Node.js, fallback to 1 if it fails
cpus=$(node -e "const os = require('os'); console.log(os.cpus().length);" 2>/dev/null) || cpus=1
# Make sure cpus is a number, default to 1 if not
case $cpus in
''|*[!0-9]*) cpus=1 ;;
esac
uv_threadpool_size=$(($cpus * 2))
# Check if uv_threadpool_size is less than 4 (default), set it to 4 if it is
if [ "$uv_threadpool_size" -lt 4 ]; then
uv_threadpool_size=4
fi
# Set UV_THREADPOOL_SIZE as an environment variable
export UV_THREADPOOL_SIZE="${UV_THREADPOOL_SIZE:-$uv_threadpool_size}"
# Start your application using yarn
exec node dist/main
#
# Handle API key hashing
#
# Save WHATSAPP_API_KEY or WAHA_API_KEY in a variable (WHATSAPP_API_KEY has priority)
if [ -n "$WHATSAPP_API_KEY" ]; then
key="$WHATSAPP_API_KEY"
elif [ -n "$WAHA_API_KEY" ]; then
key="$WAHA_API_KEY"
fi
# Unset both environment variables
unset WHATSAPP_API_KEY
unset WAHA_API_KEY
# Process the key if it exists
if [ -n "$key" ]; then
# Check if key is already hashed
if echo "$key" | grep -q "^sha512:"; then
# If already hashed, use it as is
export WAHA_API_KEY="$key"
else
# Display warning about using plain text API key
echo "⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️"
echo "WARNING: Plain text API key detected. Converting to hashed format for security."
echo "For better security, use WAHA_API_KEY=sha512:{SHA512_HASH_FOR_YOUR_API_KEY}"
echo "⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️"
# Hash the key using sha512sum
HASHED_KEY=$(echo -n "$key" | sha512sum | awk '{print $1}')
export WAHA_API_KEY="sha512:$HASHED_KEY"
fi
fi
#
# xvfb-run
#
USE_XVFB=false
# Check WAHA_RUN_XVFB parameter - only test for "false" case, treat all others as True
if [ "$WAHA_RUN_XVFB" = "false" ] || [ "$WAHA_RUN_XVFB" = "False" ] || [ "$WAHA_RUN_XVFB" = "0" ]; then
# Explicitly disabled by user
echo "WAHA_RUN_XVFB value: $WAHA_RUN_XVFB - xvfb is disabled"
USE_XVFB=false
else
# Check engine and run test if it's WEBJS or not specified
if [ -z "$WHATSAPP_DEFAULT_ENGINE" ] || [ "$WHATSAPP_DEFAULT_ENGINE" = "WEBJS" ]; then
# Try to run xvfb-run with a test command
if xvfb-run --auto-servernum echo "xvfb-run is working!"; then
USE_XVFB=true
else
echo "xvfb-run test failed, do not run it"
USE_XVFB=false
fi
fi
fi
#
# Start your application using node with exec to ensure proper signal handling
#
if [ "$USE_XVFB" = "true" ]; then
echo "Executing node with xvfb-run..."
exec xvfb-run --auto-servernum node dist/main
else
echo "Executing node without xvfb-run..."
exec node dist/main
fi
Binary file not shown.
Binary file not shown.
+6 -1
View File
@@ -3,7 +3,12 @@
"sourceRoot": "src",
"compilerOptions": {
"plugins": ["@nestjs/swagger"],
"assets": ["dashboard/**", "core/engines/webjs/*", "plus/engines/webjs/*"],
"assets": [
"dashboard/**",
"core/engines/webjs/*",
"plus/engines/webjs/*",
"apps/chatwoot/i18n/*.yaml"
],
"watchAssets": true
}
}
+51 -21
View File
@@ -20,66 +20,89 @@
"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-07-04",
"@adiwajshing/keyed-db": "^0.2.4",
"@aws-sdk/client-s3": "^3.633.0",
"@aws-sdk/s3-request-presigner": "^3.633.0",
"@bull-board/api": "^6.9.1",
"@bull-board/express": "^6.9.1",
"@bull-board/nestjs": "^6.9.1",
"@figuro/chatwoot-sdk": "^1.1.17",
"@liaoliaots/nestjs-redis": "^9",
"@nestjs/axios": "^3.0.2",
"@nestjs/common": "^9.0.9",
"@nestjs/bullmq": "^11.0.2",
"@nestjs/common": "^11.0.0",
"@nestjs/config": "^3.2.3",
"@nestjs/core": "^9.0.9",
"@nestjs/passport": "^9.0.0",
"@nestjs/platform-express": "^9.0.9",
"@nestjs/platform-ws": "^9.0.9",
"@nestjs/serve-static": "^2.1.3",
"@nestjs/core": "^11.0.0",
"@nestjs/passport": "^11.0.0",
"@nestjs/platform-express": "^11.1.5",
"@nestjs/platform-ws": "^11.0.0",
"@nestjs/serve-static": "^5.0.0",
"@nestjs/swagger": "^7.1.11",
"@nestjs/terminus": "^10.2.3",
"@nestjs/websockets": "^9.0.9",
"@nestjs/websockets": "^11.0.0",
"@types/better-sqlite3": "^7.6.10",
"@types/lodash": "^4.14.194",
"@types/mustache": "^4.2.5",
"@types/passport": "^1.0.17",
"@types/sqlite3": "^5.1.0",
"@types/ws": "^8.5.4",
"adm-zip": "0.5.10",
"agentkeepalive": "^4.5.0",
"async-lock": "^1.4.1",
"axios": "^1.7.7",
"audio-decode": "^2.2.2",
"axios": "^1.9.0",
"axios-retry": "^4.5.0",
"better-sqlite3": "11.3.0",
"bullmq": "^5.48.1",
"check-disk-space": "^3.4.0",
"chokidar": "^3.6.0",
"class-transformer": "^0.5.1",
"class-validator": "0.14.0",
"del": "^6.0.0",
"del": "^8.0.0",
"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",
"ioredis": "^5",
"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",
"mustache": "^4.2.0",
"nestjs-pino": "^4.1.0",
"passport": "^0.6.0",
"node-cache": "5.1.2",
"passport": "^0.7.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",
"promise-retry": "^2.0.1",
"puppeteer": "^23.6.0",
"puppeteer": "^24.10.0",
"qrcode": "^1.5.1",
"qrcode-terminal": "^0.12.0",
"reflect-metadata": "^0.1.13",
"rimraf": "^3.0.2",
"rimraf": "^4.3.0",
"rxjs": "^7.8.1",
"sharp": "^0.33.4",
"sqlite3": "^5.1.7",
"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-2025-06-09",
"write-file-atomic": "^6.0.0",
"yaml": "^2.7.1"
},
"optionalDependencies": {
"bufferutil": "^4.0.8"
@@ -87,13 +110,17 @@
"resolutions": {
"bufferutil": "^4.0.8",
"ws": "^8.18.0",
"puppeteer": "^23.6.0",
"whatwg-url": "13.0.0"
"puppeteer": "^24.10.0",
"whatwg-url": "13.0.0",
"libsignal": "github:devlikeapro/libsignal-node#fork-master",
"axios": "^1.9.0"
},
"devDependencies": {
"@nestjs/cli": "^9.0.0",
"@nestjs/schematics": "^9.0.1",
"@nestjs/testing": "^9.0.9",
"@grpc/grpc-js": "^1.13.4",
"@grpc/proto-loader": "^0.7.13",
"@nestjs/cli": "^11.0.0",
"@nestjs/schematics": "^11.0.0",
"@nestjs/testing": "^11.0.0",
"@types/express": "^4.17.3",
"@types/jest": "26.0.10",
"@types/node": "^18.14.2",
@@ -104,14 +131,17 @@
"eslint-config-prettier": "^6.10.0",
"eslint-plugin-import": "^2.20.1",
"eslint-plugin-simple-import-sort": "^10.0.0",
"grpc-tools": "^1.13.0",
"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 "\n-------" >> "$OUTPUT_FILE"
echo "File: $file" >> "$OUTPUT_FILE"
echo "-------\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
+2 -41
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,
@@ -39,8 +36,8 @@ class AuthController {
summary: 'Get QR code for pairing WhatsApp API.',
})
@SessionApiParam
@ApiFileAcceptHeader(Base64File, QRCodeValue)
@UseInterceptors(new BufferResponseInterceptor())
@ApiFileAcceptHeader('image/png', Base64File, QRCodeValue)
@UseInterceptors(new BufferResponseInterceptor('image/png'))
async getQR(
@QRCodeSessionParam session: WhatsappSession,
@Query() query: QRCodeQuery,
@@ -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();
}
}
+66 -1
View File
@@ -1,3 +1,4 @@
import { isJidGroup } from '@adiwajshing/baileys';
import {
Body,
Controller,
@@ -21,13 +22,21 @@ 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,
OverviewBodyRequest,
OverviewFilter,
OverviewPaginationParams,
PinMessageRequest,
ReadChatMessagesQuery,
ReadChatMessagesResponse,
transformAck,
} from '../structures/chats.dto';
import { EditMessageRequest } from '../structures/chatting.dto';
@@ -48,6 +57,35 @@ 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,
@Query() filter: OverviewFilter,
): Promise<ChatSummary[]> {
return session.getChatsOverview(pagination, filter);
}
@Post('overview')
@SessionApiParam
@ApiOperation({
summary:
'Get chats overview. Use POST if you have too many "ids" params - GET can limit it',
})
@UsePipes(new ValidationPipe({ transform: true, whitelist: true }))
postChatsOverview(
@WorkingSessionParam session: WhatsappSession,
@Body() body: OverviewBodyRequest,
): Promise<ChatSummary[]> {
return session.getChatsOverview(body.pagination, body.filter);
}
@Delete(':chatId')
@SessionApiParam
@ApiOperation({ summary: 'Deletes the chat' })
@@ -59,6 +97,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 +121,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')
+50
View File
@@ -0,0 +1,50 @@
import {
Body,
Controller,
Param,
Post,
UsePipes,
ValidationPipe,
} from '@nestjs/common';
import { ApiOperation, ApiSecurity, ApiTags } from '@nestjs/swagger';
import { WhatsappSession } from '@waha/core/abc/session.abc';
import {
SessionApiParam,
WorkingSessionParam,
} from '@waha/nestjs/params/SessionApiParam';
import { SessionManager } from '../core/abc/manager.abc';
import {
EventCancelRequest,
EventMessageRequest,
} from '../structures/events.dto';
import { WAMessage } from '../structures/responses.dto';
@ApiSecurity('api_key')
@Controller('api/:session/events')
@ApiTags('📅 Events')
export class EventsController {
constructor(private manager: SessionManager) {}
@Post()
@ApiOperation({ summary: 'Send an event message' })
@UsePipes(new ValidationPipe())
@SessionApiParam
async sendEvent(
@WorkingSessionParam session: WhatsappSession,
@Body() request: EventMessageRequest,
): Promise<WAMessage> {
return session.sendEvent(request);
}
// @Post(':id/cancel')
// @ApiOperation({ summary: 'Cancel an event by ID' })
// @UsePipes(new ValidationPipe())
// @SessionApiParam
// async cancelEvent(
// @WorkingSessionParam session: WhatsappSession,
// @Param('id') id: string,
// ): Promise<WAMessage> {
// return session.cancelEvent(id);
// }
}
+133 -61
View File
@@ -3,6 +3,8 @@ import {
Controller,
Delete,
Get,
HttpCode,
HttpStatus,
Param,
Post,
Put,
@@ -16,12 +18,20 @@ import {
SessionApiParam,
WorkingSessionParam,
} from '@waha/nestjs/params/SessionApiParam';
import { CountResponse, Result } from '@waha/structures/base.dto';
import {
ChatPictureQuery,
ChatPictureResponse,
} from '@waha/structures/chats.dto';
import { ProfilePictureRequest } from '@waha/structures/profile.dto';
import { SessionManager } from '../core/abc/manager.abc';
import { WhatsappSession } from '../core/abc/session.abc';
import { parseGroupInviteLink, WhatsappSession } from '../core/abc/session.abc';
import {
CreateGroupRequest,
DescriptionRequest,
GroupField,
GroupsListFields,
GroupsPaginationParams,
JoinGroupRequest,
JoinGroupResponse,
@@ -53,20 +63,19 @@ export class GroupsController {
@WorkingSessionParam session: WhatsappSession,
@Query() query: JoinGroupRequest,
): Promise<any> {
// https://chat.whatsapp.com/123 => 123
const code = query.code.split('/').pop();
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> {
// https://chat.whatsapp.com/123 => 123
const code = request.code.split('/').pop();
const code = parseGroupInviteLink(request.code);
const id = await session.joinGroup(code);
return { id: id };
}
@@ -75,14 +84,32 @@ export class GroupsController {
@SessionApiParam
@ApiOperation({ summary: 'Get all groups.' })
@UsePipes(new ValidationPipe({ transform: true, whitelist: true }))
getGroups(
async getGroups(
@WorkingSessionParam session: WhatsappSession,
@Query() pagination: GroupsPaginationParams,
@Query() fields: GroupsListFields,
) {
return session.getGroups(pagination);
let groups: any = await session.getGroups(pagination);
groups = session.filterGroupsFields(groups, fields);
return groups;
}
@Get('/count')
@SessionApiParam
@ApiOperation({ summary: 'Get the number of groups.' })
@UsePipes(new ValidationPipe({ transform: true, whitelist: true }))
async getGroupsCount(
@WorkingSessionParam session: WhatsappSession,
): Promise<CountResponse> {
const data: any = await session.getGroups({});
const groups: any[] = Array.isArray(data) ? data : Object.values(data);
return {
count: groups.length,
};
}
@Post('refresh')
@HttpCode(HttpStatus.OK)
@SessionApiParam
@ApiOperation({ summary: 'Refresh groups from the server.' })
@UsePipes(new ValidationPipe({ transform: true, whitelist: true }))
@@ -101,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
@@ -162,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
@@ -228,6 +295,7 @@ export class GroupsController {
}
@Post(':id/invite-code/revoke')
@HttpCode(HttpStatus.OK)
@SessionApiParam
@GroupIdApiParam
@ApiOperation({
@@ -253,6 +321,7 @@ export class GroupsController {
}
@Post(':id/participants/add')
@HttpCode(HttpStatus.OK)
@SessionApiParam
@GroupIdApiParam
@ApiOperation({ summary: 'Add participants' })
@@ -265,6 +334,7 @@ export class GroupsController {
}
@Post(':id/participants/remove')
@HttpCode(HttpStatus.OK)
@SessionApiParam
@GroupIdApiParam
@ApiOperation({
@@ -279,6 +349,7 @@ export class GroupsController {
}
@Post(':id/admin/promote')
@HttpCode(HttpStatus.OK)
@SessionApiParam
@GroupIdApiParam
@ApiOperation({ summary: 'Promote participants to admin users.' })
@@ -291,6 +362,7 @@ export class GroupsController {
}
@Post(':id/admin/demote')
@HttpCode(HttpStatus.OK)
@SessionApiParam
@GroupIdApiParam
@ApiOperation({ summary: 'Demotes participants to regular users.' })
+102
View File
@@ -0,0 +1,102 @@
import { isLidUser } from '@adiwajshing/baileys/lib/WABinary/jid-utils';
import {
Controller,
Get,
Param,
Query,
UnprocessableEntityException,
UsePipes,
ValidationPipe,
} from '@nestjs/common';
import { ApiOperation, ApiSecurity, ApiTags } from '@nestjs/swagger';
import { WhatsappSession } from '@waha/core/abc/session.abc';
import {
SessionApiParam,
WorkingSessionParam,
} from '@waha/nestjs/params/SessionApiParam';
import { CountResponse } from '@waha/structures/base.dto';
import {
LidsListQueryParams,
LidToPhoneNumber,
} from '@waha/structures/lids.dto';
import { PaginationParams, SortOrder } from '@waha/structures/pagination.dto';
import { SessionManager } from '../core/abc/manager.abc';
@ApiSecurity('api_key')
@Controller('api/:session/lids')
@ApiTags('👤 Contacts')
export class LidsController {
constructor(private manager: SessionManager) {}
@Get('/')
@SessionApiParam
@ApiOperation({ summary: 'Get all known lids to phone number mapping' })
@UsePipes(new ValidationPipe({ transform: true, whitelist: true }))
async getAll(
@WorkingSessionParam session: WhatsappSession,
@Query() params: LidsListQueryParams,
): Promise<Array<LidToPhoneNumber>> {
// Always lid
const pagination: PaginationParams = params;
pagination.sortBy = 'lid';
pagination.sortOrder = SortOrder.ASC;
const lids = await session.getAllLids(pagination);
return lids;
}
@Get('/count')
@SessionApiParam
@ApiOperation({ summary: 'Get the number of known lids' })
@UsePipes(new ValidationPipe({ transform: true, whitelist: true }))
async getLidsCount(
@WorkingSessionParam session: WhatsappSession,
): Promise<CountResponse> {
const count = await session.getLidsCount();
return {
count: count,
};
}
@Get('/:lid')
@SessionApiParam
@ApiOperation({ summary: 'Get phone number by lid' })
@UsePipes(new ValidationPipe({ transform: true, whitelist: true }))
async findPNByLid(
@WorkingSessionParam session: WhatsappSession,
@Param('lid') lid: string,
): Promise<LidToPhoneNumber> {
if (!lid.includes('@')) {
lid = lid + '@lid';
}
if (!isLidUser(lid)) {
throw new UnprocessableEntityException(
'Invalid LID - it must end with @lid',
);
}
const result = await session.findPNByLid(lid);
result.pn = result.pn || null;
return result;
}
@Get('/pn/:phoneNumber')
@SessionApiParam
@ApiOperation({ summary: 'Get lid by phone number (chat id)' })
@UsePipes(new ValidationPipe({ transform: true, whitelist: true }))
async findLIDByPhoneNumber(
@WorkingSessionParam session: WhatsappSession,
@Param('phoneNumber') phoneNumber: string,
): Promise<LidToPhoneNumber> {
if (isLidUser(phoneNumber)) {
return {
lid: phoneNumber,
pn: null,
};
}
const result = await session.findLIDByPhoneNumber(phoneNumber);
result.lid = result.lid || null;
return result;
}
}
+83
View File
@@ -0,0 +1,83 @@
import {
Body,
Controller,
Post,
UnprocessableEntityException,
UseInterceptors,
} from '@nestjs/common';
import { ApiOperation, ApiSecurity, ApiTags } from '@nestjs/swagger';
import { SessionManager } from '@waha/core/abc/manager.abc';
import { WAMimeType } from '@waha/core/media/WAMimeType';
import { ApiFileAcceptHeader } from '@waha/nestjs/ApiFileAcceptHeader';
import {
SessionApiParam,
WorkingSessionParam,
} from '@waha/nestjs/params/SessionApiParam';
import {
FileDTO,
VideoFileDTO,
VoiceFileDTO,
} from '@waha/structures/media.dto';
import { WhatsappSession } from '../core/abc/session.abc';
import { BufferResponseInterceptor } from '../nestjs/BufferResponseInterceptor';
@ApiSecurity('api_key')
@Controller('api/:session/media')
@ApiTags('🖼️ Media')
class MediaController {
constructor(private manager: SessionManager) {}
@Post('convert/voice')
@ApiOperation({
summary: 'Convert voice to WhatsApp format (opus)',
})
@SessionApiParam
@UseInterceptors(
new BufferResponseInterceptor(WAMimeType.VOICE, 'output.opus'),
)
@ApiFileAcceptHeader(WAMimeType.VOICE)
async convertVoice(
@WorkingSessionParam session: WhatsappSession,
@Body() file: VoiceFileDTO,
): Promise<Buffer> {
const data = await this.buffer(session, file);
const content = await session.mediaConverter.voice(data);
return content;
}
@Post('convert/video')
@ApiOperation({
summary: 'Convert video to WhatsApp format (mp4)',
})
@SessionApiParam
@UseInterceptors(
new BufferResponseInterceptor(WAMimeType.VIDEO, 'output.mp4'),
)
@ApiFileAcceptHeader(WAMimeType.VIDEO)
async convertVideo(
@WorkingSessionParam session: WhatsappSession,
@Body() file: VideoFileDTO,
): Promise<Buffer> {
const data = await this.buffer(session, file);
const content = await session.mediaConverter.video(data);
return content;
}
private async buffer(
session: WhatsappSession,
file: FileDTO,
): Promise<Buffer> {
if ('url' in file) {
return session.fetch(file.url);
} else if ('data' in file) {
return Buffer.from(file.data, 'base64');
} else {
throw new UnprocessableEntityException(
'Either "url" or "data" must be specified.',
);
}
}
}
export { MediaController };
+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 };
}
}
+2 -2
View File
@@ -21,8 +21,8 @@ export class ScreenshotController {
constructor(private manager: SessionManager) {}
@Get('/screenshot')
@UseInterceptors(new BufferResponseInterceptor())
@ApiFileAcceptHeader()
@UseInterceptors(new BufferResponseInterceptor('image/jpeg'))
@ApiFileAcceptHeader('image/jpeg')
async screenshot(
@Res({ passthrough: true }) res: Response,
@Query() sessionQuery: SessionQuery,
-34
View File
@@ -1,15 +1,12 @@
import * as process from 'node:process';
import * as v8 from 'node:v8';
import {
Body,
Controller,
Get,
Logger,
NotFoundException,
Post,
Query,
StreamableFile,
UsePipes,
} from '@nestjs/common';
import { ApiOperation, ApiSecurity, ApiTags } from '@nestjs/swagger';
@@ -112,34 +109,3 @@ export class ServerController {
return { stopping: true };
}
}
@ApiSecurity('api_key')
@Controller('api/server/debug')
@ApiTags('🔍 Observability')
export class ServerDebugController {
private logger: Logger;
private readonly enabled: boolean;
constructor(protected config: WhatsappConfigService) {
this.logger = new Logger('ServerDebugController');
this.enabled = this.config.debugModeEnabled;
}
@Get('heapsnapshot')
@ApiOperation({
summary: 'Return a heapsnapshot',
description: "Return a heapsnapshot of the server's memory",
})
async heapsnapshot() {
if (!this.enabled) {
throw new NotFoundException('WAHA_DEBUG_MODE is disabled');
}
this.logger.log('Creating a heap snapshot...');
const heap = v8.getHeapSnapshot();
const fileName = `${Date.now()}.heapsnapshot`;
return new StreamableFile(heap, {
type: 'application/octet-stream',
disposition: `attachment; filename=${fileName}`,
});
}
}
+79
View File
@@ -0,0 +1,79 @@
import * as v8 from 'node:v8';
import {
Controller,
Get,
Logger,
NotFoundException,
Query,
StreamableFile,
UsePipes,
} from '@nestjs/common';
import { ApiOperation, ApiSecurity, ApiTags } from '@nestjs/swagger';
import { WhatsappConfigService } from '@waha/config.service';
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 { BrowserTraceQuery } from '@waha/structures/server.debug.dto';
import { createReadStream } from 'fs';
@ApiSecurity('api_key')
@Controller('api/server/debug')
@ApiTags('🔍 Observability')
export class ServerDebugController {
private logger: Logger;
private readonly enabled: boolean;
constructor(
private config: WhatsappConfigService,
private manager: SessionManager,
) {
this.logger = new Logger('ServerDebugController');
this.enabled = this.config.debugModeEnabled;
}
@Get('heapsnapshot')
@ApiOperation({
summary: 'Return a heapsnapshot for the current nodejs process',
description: "Return a heapsnapshot of the server's memory",
})
async heapsnapshot() {
if (!this.enabled) {
throw new NotFoundException('WAHA_DEBUG_MODE is disabled');
}
this.logger.log('Creating a heap snapshot...');
const heap = v8.getHeapSnapshot();
const fileName = `${Date.now()}.heapsnapshot`;
return new StreamableFile(heap, {
type: 'application/octet-stream',
disposition: `attachment; filename=${fileName}`,
});
}
@Get('browser/trace/:session')
@ApiOperation({
summary: 'Collect and get a trace.json for Chrome DevTools ',
description: 'Uses https://pptr.dev/api/puppeteer.tracing',
})
@SessionApiParam
@UsePipes(new WAHAValidationPipe())
async browserTrace(
@WorkingSessionParam session: WhatsappSession,
@Query() query: BrowserTraceQuery,
) {
if (!this.enabled) {
throw new NotFoundException('WAHA_DEBUG_MODE is disabled');
}
const filepath = await session.browserTrace(query);
const stream = createReadStream(filepath);
const filename = `trace - ${session.name} - ${new Date()}.json`;
return new StreamableFile(stream, {
type: 'application/octet-stream',
disposition: `attachment; filename=${filename}`,
});
}
}
+10 -10
View File
@@ -209,16 +209,7 @@ class SessionsController {
})
@UsePipes(new WAHAValidationPipe())
async restart(@Param('session') name: string): Promise<SessionDTO> {
await this.withLock(name, async () => {
const exists = await this.manager.exists(name);
if (!exists) {
throw new NotFoundException('Session not found');
}
await this.manager.assign(name);
await this.manager.stop(name, true);
await this.manager.start(name);
});
await this.manager.restart(name);
return await this.manager.getSessionInfo(name);
}
@@ -233,6 +224,9 @@ class SessionsController {
@Body() request: SessionStartDeprecatedRequest,
): Promise<SessionDTO> {
const name = request.name;
if (!request.name) {
throw new UnprocessableEntityException('Session name is required');
}
if (this.manager.isRunning(name)) {
const msg = `Session '${name}' is already started.`;
throw new UnprocessableEntityException(msg);
@@ -255,6 +249,9 @@ class SessionsController {
async DEPRECATED_stop(
@Body() request: SessionStopDeprecatedRequest,
): Promise<void> {
if (!request.name) {
throw new UnprocessableEntityException('Session name is required');
}
const name = request.name;
if (request.logout) {
// Old API did remove the session complete
@@ -283,6 +280,9 @@ class SessionsController {
async DEPRECATED_logout(
@Body() request: SessionLogoutDeprecatedRequest,
): Promise<void> {
if (!request.name) {
throw new UnprocessableEntityException('Session name is required');
}
const name = request.name;
await this.withLock(name, async () => {
await this.manager.unassign(name);
+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 };
@@ -12,6 +12,7 @@ import {
WebSocketServer,
} from '@nestjs/websockets';
import { SessionManager } from '@waha/core/abc/manager.abc';
import { WebSocketAuth } from '@waha/core/auth/WebSocketAuth';
import { WebsocketHeartbeatJob } from '@waha/nestjs/ws/WebsocketHeartbeatJob';
import { WebSocket } from '@waha/nestjs/ws/ws';
import { WAHAEvents, WAHAEventsWild } from '@waha/structures/enums.dto';
@@ -21,6 +22,15 @@ import { IncomingMessage } from 'http';
import * as url from 'url';
import { Server } from 'ws';
export enum WebSocketCloseCode {
NORMAL = 1000,
GOING_AWAY = 1001,
PROTOCOL_ERROR = 1002,
UNSUPPORTED_DATA = 1003,
POLICY_VIOLATION = 1008,
INTERNAL_ERROR = 1011,
}
@WebSocketGateway({
path: '/ws',
cors: true,
@@ -32,7 +42,7 @@ export class WebsocketGatewayCore
OnGatewayDisconnect,
BeforeApplicationShutdown
{
HEARTBEAT_INTERVAL = 10_000;
HEARTBEAT_INTERVAL = 60_000;
@WebSocketServer()
server: Server;
@@ -41,7 +51,10 @@ export class WebsocketGatewayCore
private heartbeat: WebsocketHeartbeatJob;
private eventUnmask = new EventWildUnmask(WAHAEvents, WAHAEventsWild);
constructor(private manager: SessionManager) {
constructor(
private manager: SessionManager,
private auth: WebSocketAuth,
) {
this.logger = new Logger('WebsocketGateway');
this.heartbeat = new WebsocketHeartbeatJob(
this.logger,
@@ -51,21 +64,35 @@ export class WebsocketGatewayCore
handleConnection(socket: WebSocket, request: IncomingMessage, ...args): any {
// wsc - websocket client
const id = generatePrefixedId('wsc');
socket.id = id;
this.logger.debug(`New client connected: ${request.url}`);
socket.id = generatePrefixedId('wsc');
if (!this.auth.validateRequest(request)) {
// Not authorized - close connection
socket.close(WebSocketCloseCode.POLICY_VIOLATION, 'Unauthorized');
this.logger.debug(
`Unauthorized websocket connection attempt: ${request.url} - ${socket.id}`,
);
return;
}
this.logger.debug(`New client connected: ${request.url} - ${socket.id}`);
const params = this.getParams(request);
const session: string = params.session;
const events: WAHAEvents[] = params.events;
this.logger.debug(
`Client connected to session: '${session}', events: ${events}, ${id}`,
`Client connected to session: '${session}', events: ${events}, ${socket.id}`,
);
const sub = this.manager
.getSessionEvents(session, events)
.subscribe((data) => {
this.logger.debug(`Sending data to client, event.id: ${data.id}`, data);
socket.send(JSON.stringify(data));
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 +118,10 @@ 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
this.logger.log('Websocket server is down');
this.heartbeat?.stop();
}
afterInit(server: Server) {
+60
View File
@@ -0,0 +1,60 @@
import { JOB_DELAY, JOB_LOCK_TTL } from '@waha/apps/app_sdk/constants';
import { JobLoggerWrapper } from '@waha/apps/app_sdk/JobLoggerWrapper';
import { RMutexService } from '@waha/modules/rmutex/rmutex.service';
import { BaseWorkerHost } from '@waha/utils/bull/BaseWorkerHost';
import { Job } from 'bullmq';
import { DelayedError } from 'bullmq';
import { PinoLogger } from 'nestjs-pino';
import { Logger } from 'pino';
/**
* Base class for app consumers that provides common functionality
* like mutex locking, logging setup, and error handling.
*/
export abstract class AppConsumer extends BaseWorkerHost {
protected readonly logger: Logger;
constructor(
appName: string,
componentName: string,
log: PinoLogger,
protected readonly rmutex: RMutexService,
) {
super();
this.logger = log.logger.child({
app: appName,
component: componentName,
});
}
/**
* Executes a job with mutex locking to ensure only one job processes a specific resource at a time.
* @param job The job to process
* @param mutexKey The key to use for mutex locking
* @param processor The function that processes the job
* @returns The result of the processor function
*/
protected async withMutex<T, R>(
job: Job<T, R, any>,
mutexKey: string,
processor: () => Promise<R>,
): Promise<R> {
const mutex = this.rmutex.get(mutexKey, JOB_LOCK_TTL);
const lock = await mutex.lock();
if (!lock) {
const logger = new JobLoggerWrapper(job, this.logger);
logger.debug(
`Postponing job '${job.id}' for ${JOB_DELAY}ms, another job is already running the mutex.key='${mutexKey}'`,
);
await job.moveToDelayed(Date.now() + JOB_DELAY);
throw new DelayedError();
}
try {
return await processor();
} finally {
await lock.release();
}
}
}
+58
View File
@@ -0,0 +1,58 @@
import { AxiosInstance, AxiosRequestConfig, AxiosResponse } from 'axios';
import { ILogger } from './ILogger';
export class AxiosLogging {
constructor(private readonly logger: ILogger) {}
applyTo(instance: AxiosInstance) {
instance.interceptors.request.use(this.onRequest.bind(this));
instance.interceptors.response.use(
this.onResponse.bind(this),
this.onError.bind(this),
);
}
private onRequest(config: AxiosRequestConfig) {
const method = config.method?.toUpperCase() ?? 'GET';
const url = config.url ?? '';
const query = config.params
? `?${new URLSearchParams(config.params).toString()}`
: '';
const body = config.data ? JSON.stringify(config.data) : '';
this.logger.info(`${method} ${url}${query} ${body}`);
return config;
}
private onResponse(response: AxiosResponse) {
const { method, url } = response.config;
const status = response.status;
const methodStr = method?.toUpperCase() ?? 'GET';
const responseData = JSON.stringify(response.data);
if (status >= 200 && status < 400) {
this.logger.info(`${methodStr} ${status}:OK ${url}`);
this.logger.debug(`${methodStr} ${status}:OK ${url} ${responseData}`);
} else {
this.logger.warn(`${methodStr} ${status} ${url} ${responseData}`);
}
return response;
}
private onError(error: any) {
if (error.response) {
const { method, url } = error.config;
const status = error.response.status;
const message = `${
method?.toUpperCase() ?? 'GET'
} ${status} ${url} ${JSON.stringify(error.response.data)}`;
this.logger.error(message);
} else {
this.logger.error(`Axios error: ${error.message}`);
}
return Promise.reject(error);
}
}
+26
View File
@@ -0,0 +1,26 @@
import { BullMQAdapter } from '@bull-board/api/bullMQAdapter';
import { BullBoardModule } from '@bull-board/nestjs';
import { BullModule } from '@nestjs/bullmq';
import { RegisterQueueOptions } from '@nestjs/bullmq/dist/interfaces/register-queue-options.interface';
import { DynamicModule } from '@nestjs/common';
/**
* Registers a queue with both BullModule and BullBoardModule
* This ensures that whenever a queue is registered, it's also added to the Bull Board
*
* @param options Queue options including name
* @returns An array of dynamic modules for both BullModule and BullBoardModule
*/
export function RegisterAppQueue(
options: RegisterQueueOptions,
): DynamicModule[] {
return [
BullModule.registerQueue({
...options,
}),
BullBoardModule.forFeature({
name: options.name,
adapter: BullMQAdapter,
}),
];
}
+8
View File
@@ -0,0 +1,8 @@
export interface ILogger {
fatal(log: string): void;
error(log: string): void;
warn(log: string): void;
info(log: string): void;
debug(log: string): void;
trace(log: string): void;
}
+58
View File
@@ -0,0 +1,58 @@
import { Job } from 'bullmq';
import { Logger } from 'pino';
import { ILogger } from './ILogger';
/**
* JobLoggerWrapper is a wrapper around the BullMQ Job class that provides
* a logger with the job ID as a child context.
* It allows logging messages to the job's log
* and also to the logger itself.
*/
export class JobLoggerWrapper implements ILogger {
private readonly logger: Logger;
constructor(
private job: Job,
logger: Logger,
) {
this.logger = logger.child({ jobId: job.id });
}
fatal(log: string): void {
this.log(log, 'fatal');
}
error(log: string): void {
this.log(log, 'error');
}
warn(log: string): void {
this.log(log, 'warn');
}
info(log: string): void {
this.log(log, 'info');
}
debug(log: string): void {
this.log(log, 'debug');
}
trace(log: string): void {
this.log(log, 'trace');
}
private log(msg: string, level: string) {
if (!this.logger.isLevelEnabled(level)) {
return;
}
this.logger[level](msg);
const timestamp = new Date().toISOString();
this.job
.log(`[${timestamp}] ${level.toUpperCase()}: ${msg}`)
.catch((err) => {
this.logger.error('Error logging message to job', err);
});
}
}
+52
View File
@@ -0,0 +1,52 @@
import { Job } from 'bullmq';
import { Backoffs } from 'bullmq';
/**
* Calculates the delay until the next attempt for a job.
*
* @param job The job to calculate the next attempt delay for
* @returns The delay in milliseconds, or null if there is no next attempt or backoff is not set
*/
export function NextAttemptDelayInMs(job: Job): number | null {
const attemptsMade = job.attemptsMade + 1;
const maxAttempts = job.opts?.attempts || 1;
// If this is the last attempt, return null
if (attemptsMade >= maxAttempts) {
return null;
}
// Normalize the backoff options
const backoff = Backoffs.normalize(job.opts.backoff);
if (!backoff) {
return null;
}
// Calculate the delay using Backoffs.calculate
const delay = Backoffs.calculate(backoff, attemptsMade, new Error(), job);
// The delay can be a Promise, a number, or undefined
if (delay === undefined) {
return null;
}
// If it's a Promise, we can't handle it synchronously, so return null
if (delay instanceof Promise) {
return null;
}
return delay;
}
export function NextAttemptDelayInWholeSeconds(job: Job) {
const delay = NextAttemptDelayInMs(job);
if (delay == null) {
return null;
}
return Math.round(delay / 1000);
}
/**
* Checks if a job has been retried (not on its first attempt).
*
* @param job The job to check
* @returns True if the job has been retried (attemptsMade > 1), false otherwise
*/
export function HasBeenRetried(job: Job): boolean {
const attemptsMade = job.attemptsMade + 1;
return attemptsMade > 1;
}
+72
View File
@@ -0,0 +1,72 @@
import {
Body,
Controller,
Delete,
Get,
Inject,
NotFoundException,
Param,
Post,
Put,
Query,
UsePipes,
} from '@nestjs/common';
import { ApiOperation, ApiSecurity, ApiTags } from '@nestjs/swagger';
import {
AppsService,
IAppsService,
} from '@waha/apps/app_sdk/services/IAppsService';
import { SessionManager } from '@waha/core/abc/manager.abc';
import { WAHAValidationPipe } from '@waha/nestjs/pipes/WAHAValidationPipe';
import { App } from '../dto/app.dto';
import { ListAppsQuery } from '../dto/query.dto';
@ApiSecurity('api_key')
@Controller('api/apps')
@ApiTags('🧩 Apps')
export class AppsController {
constructor(
@Inject(AppsService)
private appsService: IAppsService,
private manager: SessionManager,
) {}
@Get('/')
@ApiOperation({ summary: 'List all apps for a session' })
@UsePipes(new WAHAValidationPipe())
async list(
@Query(new WAHAValidationPipe()) query: ListAppsQuery,
): Promise<App[]> {
return this.appsService.list(this.manager, query.session);
}
@Post('/')
@ApiOperation({ summary: 'Create a new app' })
@UsePipes(new WAHAValidationPipe())
async create(@Body() app: App): Promise<App> {
return await this.appsService.create(this.manager, app);
}
@Put('/:id')
@ApiOperation({ summary: 'Update an existing app' })
@UsePipes(new WAHAValidationPipe())
async update(@Param('id') id: string, @Body() app: App): Promise<void> {
if (!app.id) {
app.id = id;
} else if (app.id !== id) {
throw new NotFoundException(
`App ID in path (${id}) does not match ID in body (${app.id})`,
);
}
await this.appsService.update(this.manager, app);
}
@Delete('/:id')
@ApiOperation({ summary: 'Delete an app' })
@UsePipes(new WAHAValidationPipe())
async delete(@Param('id') id: string): Promise<void> {
await this.appsService.delete(this.manager, id);
}
}
+22
View File
@@ -0,0 +1,22 @@
import * as process from 'node:process';
import { parseBool } from '@waha/helpers';
import * as basicAuth from 'express-basic-auth';
export function BullAuthMiddleware() {
let username = process.env.WAHA_DASHBOARD_USERNAME;
let password = process.env.WAHA_DASHBOARD_PASSWORD;
const enabled = parseBool(process.env.WAHA_DASHBOARD_ENABLED);
if (!enabled) {
// Generate a random uuid4 username /password to prevent access
username = 'admin';
password = crypto.randomUUID();
}
return basicAuth({
challenge: true,
users: {
[username]: password,
},
});
}
+42
View File
@@ -0,0 +1,42 @@
import { DefaultJobOptions } from 'bullmq';
export const JOB_DELAY = 1_000;
export const JOB_LOCK_TTL = 20_000;
export const JOB_CONCURRENCY =
parseInt(process.env.WAHA_APPS_JOBS_CONCURRENCY) || 50;
function jobRemoveOptions() {
// in seconds
const HOUR = 60 * 60;
const DAY = 24 * HOUR;
return {
removeOnComplete: {
age:
parseInt(process.env.WAHA_APPS_JOBS_REMOVE_ON_COMPLETE_AGE) || 3 * DAY,
count:
parseInt(process.env.WAHA_APPS_JOBS_REMOVE_ON_COMPLETE_COUNT) || 1000,
},
removeOnFail: {
age: parseInt(process.env.WAHA_APPS_JOBS_REMOVE_ON_FAIL_AGE) || 31 * DAY,
count: parseInt(process.env.WAHA_APPS_JOBS_REMOVE_ON_FAIL_COUNT) || 1000,
},
};
}
export const JobRemoveOptions = jobRemoveOptions();
export const ExponentialRetriesJobOptions: DefaultJobOptions = {
attempts: 3,
backoff: {
type: 'exponential',
delay: 1000,
},
};
export const NoRetriesJobOptions: DefaultJobOptions = {
attempts: 1,
};
export function merge(...args: any[]) {
return Object.assign({}, ...args);
}
+42
View File
@@ -0,0 +1,42 @@
import { ChatWootAppConfig } from '@waha/apps/chatwoot/dto/config.dto';
import { Type } from 'class-transformer';
import { IsEnum, IsString, ValidateNested } from 'class-validator';
export type AllowedAppConfig = ChatWootAppConfig;
export enum AppName {
chatwoot = 'chatwoot',
}
export class App<T extends AllowedAppConfig = any> {
@IsString()
id: string;
@IsString()
session: string;
// App name (aka type)
@IsEnum(AppName)
app: AppName;
@ValidateNested()
@Type((options) => {
if (options && options.object && options.object.app) {
switch (options.object.app) {
case AppName.chatwoot:
return ChatWootAppConfig;
default:
return Object;
}
}
return Object;
})
config: T;
}
export class ChatWootAppDto extends App<ChatWootAppConfig> {
@Type(() => ChatWootAppConfig)
config: ChatWootAppConfig;
}
export type AppDto = ChatWootAppDto;
+13
View File
@@ -0,0 +1,13 @@
import { ApiProperty } from '@nestjs/swagger';
import { IsNotEmpty, IsString } from 'class-validator';
export class ListAppsQuery {
@ApiProperty({
example: 'default',
required: true,
description: 'Session name to list apps for',
})
@IsString()
@IsNotEmpty()
session: string;
}
+29
View File
@@ -0,0 +1,29 @@
import * as path from 'node:path';
import { Knex } from 'knex';
function migrateSDK(knex: Knex): Promise<any> {
const config = {
directory: `${__dirname}/migrations`,
tableName: 'apps_migrations',
loadExtensions: ['.js'],
};
return knex.migrate.latest(config);
}
function migrateApp(knex: Knex, app: string): Promise<any> {
// one level upp
const appsDirectory = path.join(__dirname, '..', '..', 'apps');
const directory = path.join(appsDirectory, app, 'migrations');
const config = {
directory: directory,
tableName: `app_${app}_migrations`,
loadExtensions: ['.js'],
};
return knex.migrate.latest(config);
}
export async function migrate(knex: Knex) {
await migrateSDK(knex);
await migrateApp(knex, 'chatwoot');
}
@@ -0,0 +1,22 @@
import { Knex } from 'knex';
exports.up = function (knex: Knex) {
return knex.schema
.createTable('apps', function (table) {
table.increments('pk');
table.string('id', 64);
table.string('session', 64);
table.string('app', 64);
table.json('config');
})
.table('apps', function (table) {
table.unique(['pk']);
table.index(['session']);
table.index(['id']);
table.unique(['id']);
});
};
exports.down = function (knex: Knex) {
return knex.schema.dropTable('apps');
};
@@ -0,0 +1,43 @@
import { Injectable, UnprocessableEntityException } from '@nestjs/common';
import { IAppsService } from '@waha/apps/app_sdk/services/IAppsService';
import { DataStore } from '@waha/core/abc/DataStore';
import { SessionManager } from '@waha/core/abc/manager.abc';
import { WhatsappSession } from '@waha/core/abc/session.abc';
import { Knex } from 'knex';
import { App } from '../dto/app.dto';
class AppsIsDisabledError extends UnprocessableEntityException {
constructor() {
super(
"Apps are disabled. Enable it by setting 'WAHA_APPS_ENABLED=True' environment variable.",
);
}
}
@Injectable()
export class AppsDisabledService implements IAppsService {
async list(manager: SessionManager, session: string): Promise<App[]> {
throw new AppsIsDisabledError();
}
async create(manager: SessionManager, app: App): Promise<App> {
throw new AppsIsDisabledError();
}
async update(manager: SessionManager, app: App) {
throw new AppsIsDisabledError();
}
async delete(manager: SessionManager, appId: string) {
throw new AppsIsDisabledError();
}
migrate(knex: Knex<any, any[]>): Promise<void> {
return;
}
async beforeSessionStart(session: WhatsappSession, store: DataStore) {
return;
}
}
@@ -0,0 +1,150 @@
import { Injectable, NotFoundException } from '@nestjs/common';
import { migrate } from '@waha/apps/app_sdk/migrations';
import { IAppService } from '@waha/apps/app_sdk/services/IAppService';
import { IAppsService } from '@waha/apps/app_sdk/services/IAppsService';
import { ChatWootAppService } from '@waha/apps/chatwoot/services/ChatWootAppService';
import { DataStore } from '@waha/core/abc/DataStore';
import { SessionManager } from '@waha/core/abc/manager.abc';
import { WhatsappSession } from '@waha/core/abc/session.abc';
import { generatePrefixedId } from '@waha/utils/ids';
import { Knex } from 'knex';
import { InjectPinoLogger, PinoLogger } from 'nestjs-pino';
import { App, AppName } from '../dto/app.dto';
import { AppRepository } from '../storage/AppRepository';
@Injectable()
export class AppsEnabledService implements IAppsService {
constructor(
protected readonly chatwootService: ChatWootAppService,
@InjectPinoLogger('AppsService')
protected logger: PinoLogger,
) {}
async list(manager: SessionManager, session: string): Promise<App[]> {
const knex = manager.store.getWAHADatabase();
const repo = new AppRepository(knex);
const apps = await repo.getAllBySession(session);
apps.forEach((app) => {
delete app.pk;
});
return apps;
}
async create(manager: SessionManager, app: App): Promise<App> {
await this.checkSessionExists(manager, app.session);
app.id = app.id || generatePrefixedId('app');
const knex = manager.store.getWAHADatabase();
const repo = new AppRepository(knex);
const existingApp = await repo.getById(app.id);
if (existingApp) {
throw new Error(`App with ID '${app.id}' already exists.`);
}
// Validate only one Chatwoot app per session
if (app.app === AppName.chatwoot) {
const existingApps = await repo.getAllBySession(app.session);
const existingChatwootApp = existingApps.find(
(existingApp) => existingApp.app === AppName.chatwoot,
);
if (existingChatwootApp) {
throw new Error(
`Only one Chatwoot app is allowed per session. Session '${app.session}' already has a Chatwoot app with ID '${existingChatwootApp.id}'.`,
);
}
}
const service = this.getAppService(app);
await service.beforeCreated(app);
const result = await repo.save(app);
await this.restartIfRunning(manager, app.session);
delete result.pk;
return result;
}
async update(manager: SessionManager, app: App) {
await this.checkSessionExists(manager, app.session);
const knex = manager.store.getWAHADatabase();
const repo = new AppRepository(knex);
const savedApp = await repo.getById(app.id);
if (!savedApp) {
throw new NotFoundException(`App '${app.id}' not found`);
}
if (savedApp.app != app.app) {
throw new Error(
`Can not change app type. Delete and create a new app. Before type: '${savedApp.app}' After type: '${app.app}'`,
);
}
if (savedApp.session != app.session) {
throw new Error(
`Can not change session. Delete and create a new app. Before session: '${savedApp.session}' After session: '${app.session}'`,
);
}
const service = this.getAppService(app);
await service.beforeUpdated(savedApp, app);
await repo.update(app.id, app);
await this.restartIfRunning(manager, app.session);
return;
}
async delete(manager: SessionManager, appId: string) {
const knex = manager.store.getWAHADatabase();
const repo = new AppRepository(knex);
const app = await repo.getById(appId);
if (!app) {
throw new NotFoundException(`App '${appId}' not found`);
}
const service = this.getAppService(app);
await service.beforeDeleted(app);
await repo.delete(app.id);
await this.restartIfRunning(manager, app.session);
return;
}
async beforeSessionStart(session: WhatsappSession, store: DataStore) {
const knex = store.getWAHADatabase();
const repo = new AppRepository(knex);
const apps = await repo.getAllBySession(session.name);
for (const app of apps) {
const service = this.getAppService(app);
await service.beforeSessionStart(app, session);
}
}
async migrate(knex: Knex): Promise<void> {
await migrate(knex);
}
private restartIfRunning(manager: SessionManager, session: string) {
const isRunning = manager.isRunning(session);
if (!isRunning) {
return;
}
return manager.restart(session);
}
private getAppService(app: App): IAppService {
switch (app.app) {
case AppName.chatwoot:
return this.chatwootService;
default:
throw new Error(`App '${app.app}' not supported`);
}
}
private async checkSessionExists(
manager: SessionManager,
sessionName: string,
) {
const session = await manager.exists(sessionName);
if (session === null) {
throw new NotFoundException('Session not found');
}
}
}
+15
View File
@@ -0,0 +1,15 @@
import { App } from '@waha/apps/app_sdk/dto/app.dto';
import { WhatsappSession } from '@waha/core/abc/session.abc';
/**
* Exact App service
*/
export interface IAppService {
beforeCreated(app: App): Promise<void>;
beforeUpdated(savedApp: App, newApp: App): Promise<void>;
beforeDeleted(app: App): Promise<void>;
beforeSessionStart(app: App, session: WhatsappSession): Promise<void>;
}
+21
View File
@@ -0,0 +1,21 @@
import { App } from '@waha/apps/app_sdk/dto/app.dto';
import { DataStore } from '@waha/core/abc/DataStore';
import { SessionManager } from '@waha/core/abc/manager.abc';
import { WhatsappSession } from '@waha/core/abc/session.abc';
import { Knex } from 'knex';
export interface IAppsService {
list(manager: SessionManager, session: string): Promise<App[]>;
create(manager: SessionManager, app: App): Promise<App>;
update(manager: SessionManager, app: App): Promise<void>;
delete(manager: SessionManager, appId: string): Promise<void>;
beforeSessionStart(session: WhatsappSession, store: DataStore): Promise<void>;
migrate(knex: Knex): Promise<void>;
}
export const AppsService = Symbol('AppsService');
+79
View File
@@ -0,0 +1,79 @@
import { Knex } from 'knex';
import { App } from '../dto/app.dto';
import { AppDB } from './types';
export class AppRepository {
static tableName = 'apps';
constructor(private readonly knex: Knex) {}
get tableName() {
return AppRepository.tableName;
}
/**
* Saves an app to the database
*/
async save(app: Omit<App, 'pk'>): Promise<AppDB> {
const appToSave = { ...app };
if (appToSave.config && typeof appToSave.config === 'object') {
appToSave.config = JSON.stringify(appToSave.config);
}
const [pk] = await this.knex(this.tableName)
.insert(appToSave)
.returning('pk');
return { ...app, pk: pk };
}
private deserialize(app: AppDB): AppDB {
const parsedConfig =
typeof app.config === 'string' ? JSON.parse(app.config) : app.config;
return { ...app, config: parsedConfig };
}
private serialize<T extends Partial<App>>(app: T): T {
const appCopy = { ...app };
if (appCopy.config && typeof appCopy.config === 'object') {
appCopy.config = JSON.stringify(appCopy.config);
}
return appCopy;
}
/**
* Gets an app by its id
*/
async getById(id: string): Promise<AppDB | null> {
const app = await this.knex(this.tableName).where('id', id).first();
if (!app) {
return null;
}
return this.deserialize(app);
}
/**
* Gets all apps
*/
async getAllBySession(session: string): Promise<AppDB[]> {
return this.knex(this.tableName)
.select('*')
.where('session', session)
.orderBy('id', 'asc')
.then((apps) => apps.map((app) => this.deserialize(app)));
}
/**
* Updates an app
*/
async update(id: string, app: Partial<Omit<App, 'id'>>): Promise<void> {
const appToUpdate = this.serialize(app);
await this.knex(this.tableName).where('id', id).update(appToUpdate);
}
/**
* Deletes an app
*/
async delete(id: string): Promise<void> {
await this.knex(this.tableName).where('id', id).delete();
}
}
+6
View File
@@ -0,0 +1,6 @@
import { App } from '@waha/apps/app_sdk/dto/app.dto';
export interface AppDB extends App {
// Internal database number
pk: number;
}
+112
View File
@@ -0,0 +1,112 @@
import * as process from 'node:process';
import { ExpressAdapter } from '@bull-board/express';
import { BullBoardModule } from '@bull-board/nestjs';
import { RedisModule } from '@liaoliaots/nestjs-redis';
import { BullModule } from '@nestjs/bullmq';
import { AppsController } from '@waha/apps/app_sdk/api/apps.controller';
import { BullAuthMiddleware } from '@waha/apps/app_sdk/auth';
import { AppsDisabledService } from '@waha/apps/app_sdk/services/AppsDisabledService';
import { AppsEnabledService } from '@waha/apps/app_sdk/services/AppsEnabledService';
import { AppsService } from '@waha/apps/app_sdk/services/IAppsService';
import { ChatwootLocalesController } from '@waha/apps/chatwoot/api/chatwoot.locales.controller';
import { ChatWootExports } from '@waha/apps/chatwoot/chatwoot.module';
import { parseBool } from '@waha/helpers';
import { RMutexModule } from '@waha/modules/rmutex';
const IMPORTS = [
BullModule.forRoot({
connection: {
url: process.env.REDIS_URL || 'redis://:redis@localhost:6379',
maxRetriesPerRequest: null,
},
prefix: `waha-${process.env.WAHA_WORKER_ID}`,
}),
RedisModule.forRoot({
closeClient: true,
config: {
url: process.env.REDIS_URL || 'redis://:redis@localhost:6379',
onClientCreated: async (client) => {
try {
await client.ping();
} catch (err) {
console.error('[Redis] Connection failed:', err);
process.exit(1); // Stop the app if Redis is unavailable
}
},
},
}),
RMutexModule,
BullBoardModule.forRoot({
route: '/jobs',
adapter: ExpressAdapter,
middleware: BullAuthMiddleware(),
boardOptions: {
uiConfig: {
boardTitle: 'Jobs | WAHA',
boardLogo: {
path: '/dashboard/layout/images/logo-white.svg',
width: 35,
height: 35,
},
favIcon: {
default: '/dashboard/favicon.ico',
alternative: '/dashboard/favicon.ico',
},
miscLinks: [
{
text: '📊 Dashboard',
url: '/dashboard',
},
{
text: '📚 Swagger (OpenAPI)',
url: '/',
},
],
},
},
}),
...ChatWootExports.imports,
];
const AppsEnabled = {
imports: IMPORTS,
controllers: [AppsController, ...ChatWootExports.controllers],
providers: [
{
provide: AppsService,
useClass: AppsEnabledService,
},
...ChatWootExports.providers,
],
};
const AppsDisabled = {
providers: [
{
provide: AppsService,
useClass: AppsDisabledService,
},
],
imports: [],
controllers: [AppsController, ChatwootLocalesController],
};
function checkApiKey() {
const key = process.env.WHATSAPP_API_KEY || process.env.WAHA_API_KEY;
if (!key) {
return;
}
const plain = process.env.WAHA_API_KEY_PLAIN;
if (!plain) {
throw Error(
'WAHA_API_KEY set, please provide WAHA_API_KEY_PLAIN when WAHA_APPS_ENABLED',
);
}
}
const enabled = parseBool(process.env.WAHA_APPS_ENABLED);
if (enabled) {
checkApiKey();
}
export const AppsModuleExports = enabled ? AppsEnabled : AppsDisabled;
+18
View File
@@ -0,0 +1,18 @@
import { WAHASessionStatus } from '@waha/structures/enums.dto';
export function SessionStatusEmoji(status: WAHASessionStatus): string {
switch (status) {
case WAHASessionStatus.STOPPED:
return '⚠️';
case WAHASessionStatus.STARTING:
return '⏳';
case WAHASessionStatus.SCAN_QR_CODE:
return '⚠️';
case WAHASessionStatus.WORKING:
return '🟢';
case WAHASessionStatus.FAILED:
return '🛑';
default:
return '❓';
}
}
@@ -0,0 +1,25 @@
import { Controller, Get } from '@nestjs/common';
import { ApiOperation, ApiSecurity, ApiTags } from '@nestjs/swagger';
import { LOCALE_NAMES } from '@waha/apps/chatwoot/locale';
interface LanguageResponse {
name: string;
locale: string;
}
@Controller('api/apps/chatwoot')
@ApiSecurity('api_key')
@ApiTags('🧩 Apps')
export class ChatwootLocalesController {
@Get('locales')
@ApiOperation({
summary: 'Get available languages for Chatwoot app',
description: 'Get available languages for Chatwoot app',
})
getLanguages(): LanguageResponse[] {
return Array.from(LOCALE_NAMES.entries()).map(([locale, name]) => ({
name,
locale,
}));
}
}
@@ -0,0 +1,76 @@
import { Body, Controller, Param, Post } from '@nestjs/common';
import { ApiOperation, ApiTags } from '@nestjs/swagger';
import { FindChatID } from '@waha/apps/chatwoot/client/ids';
import { EventName, MessageType } from '@waha/apps/chatwoot/client/types';
import { INBOX_CONTACT_CHAT_ID } from '@waha/apps/chatwoot/const';
import { InboxData } from '@waha/apps/chatwoot/consumers/types';
import { ChatWootQueueService } from '@waha/apps/chatwoot/services/ChatWootQueueService';
@Controller('webhooks/chatwoot/')
@ApiTags('🧩 Apps')
export class ChatwootWebhookController {
constructor(private readonly chatWootQueueService: ChatWootQueueService) {}
@Post(':session/:id')
@ApiOperation({
summary: 'Chatwoot Webhook',
description: 'Chatwoot Webhook',
})
async webhook(
@Param('session') session: string,
@Param('id') id: string,
@Body() body: any,
) {
if (!body || !body?.event) {
return { success: true };
}
// Ignore all private notes
if (body.private) {
return { success: true };
}
// Ignore all incoming messages
if (body.message_type == MessageType.INCOMING) {
return { success: true };
}
const data: InboxData = {
session: session,
app: id,
body: body,
};
// Check if it's a command message (sent to the special inbox contact)
const sender = body?.conversation?.meta?.sender;
const chatId = FindChatID(sender);
const isCommandsChat = chatId === INBOX_CONTACT_CHAT_ID;
// Check if it's a deleted message
if (body.content_attributes?.deleted && !isCommandsChat) {
await this.chatWootQueueService.addMessageDeletedJob(data);
return { success: true };
}
// Route to specific queues based on an event type
switch (body.event) {
case EventName.MESSAGE_CREATED:
if (!isCommandsChat) {
await this.chatWootQueueService.addMessageCreatedJob(data);
} else {
await this.chatWootQueueService.addCommandsJob(body.event, data);
}
return { success: true };
case EventName.MESSAGE_UPDATED:
if (!isCommandsChat) {
await this.chatWootQueueService.addMessageUpdatedJob(data);
} else {
await this.chatWootQueueService.addCommandsJob(body.event, data);
}
return { success: true };
default:
// Ignore other events
return { success: true };
}
}
}
+53
View File
@@ -0,0 +1,53 @@
import { public_conversation } from '@figuro/chatwoot-sdk';
import { ChatWootAPIConfig } from '@waha/apps/chatwoot/client/interfaces';
import * as NodeCache from 'node-cache';
import { IConversationCache } from './IConversationCache';
const cache: NodeCache = new NodeCache({
stdTTL: 24 * 60 * 60, // 1 day
useClones: false,
});
export function CacheForConfig(config: ChatWootAPIConfig): ConversationCache {
return new ConversationCache(`${config.url}+${config.inboxId}`);
}
class ConversationCache implements IConversationCache {
constructor(private prefix: string) {}
fullKey(key: string) {
return `${this.prefix}.${key}`;
}
delete(key: string): void {
const fullKey = this.fullKey(key);
cache.del(fullKey);
}
get(key: string): public_conversation | null {
const fullKey = this.fullKey(key);
return cache.get(fullKey) || null;
}
has(key: string): boolean {
const fullKey = this.fullKey(key);
return cache.has(fullKey);
}
set(key: string, value: public_conversation): void {
const fullKey = this.fullKey(key);
cache.set(fullKey, value);
}
/**
* Completely clean the cache with the prefix
*/
clean() {
cache.keys().forEach((key) => {
if (key.startsWith(this.prefix)) {
cache.del(key);
}
});
}
}
+9
View File
@@ -0,0 +1,9 @@
import { public_conversation } from '@figuro/chatwoot-sdk';
export interface IConversationCache {
get(key: string): public_conversation | null;
set(key: string, value: public_conversation): void;
has(key: string): boolean;
delete(key: string): void;
clean(): void;
}
+101
View File
@@ -0,0 +1,101 @@
import { Module } from '@nestjs/common';
import { RegisterAppQueue } from '@waha/apps/app_sdk/BullUtils';
import {
ExponentialRetriesJobOptions,
JobRemoveOptions,
merge,
NoRetriesJobOptions,
} from '@waha/apps/app_sdk/constants';
import { MessageCleanupConsumer } from '@waha/apps/chatwoot/consumers/scheduled/message.cleanup';
import { ChatWootAppService } from '@waha/apps/chatwoot/services/ChatWootAppService';
import * as lodash from 'lodash';
import { ChatwootLocalesController } from './api/chatwoot.locales.controller';
import { ChatwootWebhookController } from './api/chatwoot.webhook.controller';
import { ChatWootInboxCommandsConsumer } from './consumers/inbox/commands';
import { ChatWootInboxMessageCreatedConsumer } from './consumers/inbox/message_created';
import { ChatWootInboxMessageDeletedConsumer } from './consumers/inbox/message_deleted';
import { ChatWootInboxMessageUpdatedConsumer } from './consumers/inbox/message_updated';
import { QueueName } from './consumers/QueueName';
import { CheckVersionConsumer } from './consumers/scheduled/check.version';
import { WAHAMessageAnyConsumer } from './consumers/waha/message.any';
import { WAHAMessageEditedConsumer } from './consumers/waha/message.edited';
import { WAHAMessageReactionConsumer } from './consumers/waha/message.reaction';
import { WAHAMessageRevokedConsumer } from './consumers/waha/message.revoked';
import { WAHASessionStatusConsumer } from './consumers/waha/session.status';
import { ChatWootQueueService } from './services/ChatWootQueueService';
import { ChatWootScheduleService } from './services/ChatWootScheduleService';
import { ChatWootWAHAQueueService } from './services/ChatWootWAHAQueueService';
const CONTROLLERS = [ChatwootWebhookController, ChatwootLocalesController];
const IMPORTS = lodash.flatten([
RegisterAppQueue({
name: QueueName.SCHEDULED_MESSAGE_CLEANUP,
defaultJobOptions: merge(ExponentialRetriesJobOptions, JobRemoveOptions),
}),
RegisterAppQueue({
name: QueueName.SCHEDULED_CHECK_VERSION,
defaultJobOptions: merge(NoRetriesJobOptions, JobRemoveOptions),
}),
RegisterAppQueue({
name: QueueName.WAHA_MESSAGE_ANY,
defaultJobOptions: merge(ExponentialRetriesJobOptions, JobRemoveOptions),
}),
RegisterAppQueue({
name: QueueName.WAHA_MESSAGE_REACTION,
defaultJobOptions: merge(ExponentialRetriesJobOptions, JobRemoveOptions),
}),
RegisterAppQueue({
name: QueueName.WAHA_MESSAGE_EDITED,
defaultJobOptions: merge(ExponentialRetriesJobOptions, JobRemoveOptions),
}),
RegisterAppQueue({
name: QueueName.WAHA_MESSAGE_REVOKED,
defaultJobOptions: merge(ExponentialRetriesJobOptions, JobRemoveOptions),
}),
RegisterAppQueue({
name: QueueName.WAHA_SESSION_STATUS,
defaultJobOptions: merge(ExponentialRetriesJobOptions, JobRemoveOptions),
}),
RegisterAppQueue({
name: QueueName.INBOX_MESSAGE_CREATED,
defaultJobOptions: merge(ExponentialRetriesJobOptions, JobRemoveOptions),
}),
RegisterAppQueue({
name: QueueName.INBOX_MESSAGE_UPDATED,
defaultJobOptions: merge(ExponentialRetriesJobOptions, JobRemoveOptions),
}),
RegisterAppQueue({
name: QueueName.INBOX_MESSAGE_DELETED,
defaultJobOptions: merge(ExponentialRetriesJobOptions, JobRemoveOptions),
}),
RegisterAppQueue({
name: QueueName.INBOX_COMMANDS,
defaultJobOptions: merge(NoRetriesJobOptions, JobRemoveOptions),
}),
]);
const PROVIDERS = [
ChatWootInboxMessageCreatedConsumer,
ChatWootInboxMessageUpdatedConsumer,
ChatWootInboxMessageDeletedConsumer,
ChatWootInboxCommandsConsumer,
WAHASessionStatusConsumer,
WAHAMessageAnyConsumer,
WAHAMessageReactionConsumer,
WAHAMessageEditedConsumer,
WAHAMessageRevokedConsumer,
MessageCleanupConsumer,
CheckVersionConsumer,
ChatWootWAHAQueueService,
ChatWootQueueService,
ChatWootScheduleService,
ChatWootAppService,
];
export const ChatWootExports = {
providers: PROVIDERS,
imports: IMPORTS,
controllers: CONTROLLERS,
};
+150
View File
@@ -0,0 +1,150 @@
import ChatwootClient, {
contact_update,
public_contact_create_update_payload,
} from '@figuro/chatwoot-sdk';
import type { contact } from '@figuro/chatwoot-sdk/dist/models/contact';
import type { generic_id } from '@figuro/chatwoot-sdk/dist/models/generic_id';
import { ILogger } from '@waha/apps/app_sdk/ILogger';
import {
ChatWootAPIConfig,
ChatWootInboxAPI,
} from '@waha/apps/chatwoot/client/interfaces';
import { isJidCusFormat } from '@waha/utils/wa';
import * as lodash from 'lodash';
import { AttributeKey } from '../const';
export interface ContactResponse {
data: generic_id & contact;
sourceId: string;
}
export class ContactAPI {
constructor(
private config: ChatWootAPIConfig,
private accountAPI: ChatwootClient,
protected inboxAPI: ChatWootInboxAPI,
private logger: ILogger,
) {}
async searchByAnyID(chatId: string): Promise<ContactResponse | null> {
const payload: any[] = [
{
attribute_key: AttributeKey.WA_CHAT_ID,
filter_operator: 'equal_to',
values: [chatId],
attribute_model: 'standard',
custom_attribute_type: '',
query_operator: 'OR',
},
{
attribute_key: AttributeKey.WA_JID,
filter_operator: 'equal_to',
values: [chatId],
attribute_model: 'standard',
custom_attribute_type: '',
query_operator: 'OR',
},
{
attribute_key: AttributeKey.WA_LID,
filter_operator: 'equal_to',
values: [chatId],
attribute_model: 'standard',
custom_attribute_type: '',
query_operator: 'OR',
},
{
attribute_key: 'identifier',
filter_operator: 'equal_to',
values: [chatId],
attribute_model: 'standard',
custom_attribute_type: '',
},
];
if (isJidCusFormat(chatId)) {
// Search by phone
let phone_number = chatId.split('@')[0];
phone_number = phone_number.replace('+', '');
payload[payload.length - 1].query_operator = 'OR';
payload.push({
attribute_key: 'phone_number',
filter_operator: 'equal_to',
values: [phone_number],
});
}
const response: any = await this.accountAPI.contacts.filter({
accountId: this.config.accountId,
payload: payload as any,
});
const contacts = response.payload;
if (contacts.length == 0) {
return null;
}
const contact = contacts[0];
const inboxes = lodash.filter(contact.contact_inboxes, {
inbox: { id: this.config.inboxId },
});
if (inboxes.length == 0) {
return null;
}
return {
data: contact,
sourceId: inboxes[0].source_id,
};
}
public updateCustomAttributes(
contact: generic_id & contact,
attributes: any,
) {
const update: contact_update = {
custom_attributes: { ...contact.custom_attributes, ...attributes },
};
return this.accountAPI.contacts.update({
id: contact.id,
accountId: this.config.accountId,
data: update,
});
}
public async create(
chatId: string,
payload: public_contact_create_update_payload,
): Promise<ContactResponse> {
const contact = await this.inboxAPI.contacts.create({
inboxIdentifier: this.config.inboxIdentifier,
data: payload,
});
this.logger.info(
`Created contact for chat.id: ${chatId}, contact.id: ${contact.source_id}`,
);
const response: any = await this.accountAPI.contacts.get({
accountId: this.config.accountId,
id: contact.id,
});
return {
data: response.payload,
sourceId: contact.source_id,
};
}
public updateAvatarUrlSafe(contactId, avatarUrl: string) {
this.accountAPI.contacts
.update({
accountId: this.config.accountId,
id: contactId,
data: {
avatar_url: avatarUrl,
},
})
.catch((e) => {
this.logger.warn(
'Error updating avatar_url for contact.id: ' + contactId,
);
this.logger.warn(e);
});
}
}
@@ -0,0 +1,130 @@
import ChatwootClient, {
ApiError,
public_contact_create_update_payload,
public_conversation,
} from '@figuro/chatwoot-sdk';
import { ILogger } from '@waha/apps/app_sdk/ILogger';
import { ContactAPI } from '@waha/apps/chatwoot/client/ContactAPI';
import { Conversation } from '@waha/apps/chatwoot/client/Conversation';
import { ConversationAPI } from '@waha/apps/chatwoot/client/ConversationAPI';
import { ChatWootAPIConfig } from '@waha/apps/chatwoot/client/interfaces';
import { InboxContactInfo } from '@waha/apps/chatwoot/contacts/InboxContactInfo';
import { Locale } from '@waha/apps/chatwoot/locale';
import { CacheForConfig } from '../cache/ConversationCache';
import { IConversationCache } from '../cache/IConversationCache';
export interface ContactInfo {
ChatId(): string;
AvatarUrl(): Promise<string | null>;
Attributes(): Promise<any>;
PublicContactCreate(): Promise<public_contact_create_update_payload>;
}
export class ContactConversationService {
private cache: IConversationCache;
constructor(
private config: ChatWootAPIConfig,
private contactAPI: ContactAPI,
private conversationAPI: ConversationAPI,
private accountAPI: ChatwootClient,
private logger: ILogger,
private l: Locale,
) {
this.cache = CacheForConfig(config);
}
private async upsertByContactInfo(
contactInfo: ContactInfo,
): Promise<public_conversation> {
const chatId = contactInfo.ChatId();
// Check cache for chat id
if (this.cache.has(chatId)) {
return this.cache.get(chatId);
}
//
// Find or create contact
//
let contact = await this.contactAPI.searchByAnyID(chatId);
if (!contact) {
const request = await contactInfo.PublicContactCreate();
contact = await this.contactAPI.create(chatId, request);
}
// Update custom attributes - always
const attributes = await contactInfo.Attributes();
this.logger.info(
`Updating contact custom attributes for chat.id: ${chatId}, contact.id: ${contact.data.id}`,
);
await this.contactAPI.updateCustomAttributes(contact.data, attributes);
// Update Avatar if nothing, but keep the original one if any
if (!contact.data.thumbnail) {
const avatarUrl = await contactInfo.AvatarUrl();
if (avatarUrl) {
this.contactAPI.updateAvatarUrlSafe(contact.data.id, avatarUrl);
}
}
this.logger.info(
`Using contact for chat.id: ${chatId}, contact.id: ${contact.sourceId}`,
);
//
// Get or create a conversation for this inbox
//
const conversation = await this.conversationAPI.upsert(contact.sourceId);
this.logger.info(
`Using conversation for chat.id: ${chatId}, conversation.id: ${conversation.id}, contact.id: ${contact.sourceId}`,
);
// Save to cache
this.cache.set(chatId, conversation);
return conversation;
}
public async ConversationByContact(
contactInfo: ContactInfo,
): Promise<Conversation> {
const chatId = contactInfo.ChatId();
const publicConversation = await this.upsertByContactInfo(contactInfo);
const conversation = new Conversation(
this.accountAPI,
this.config.accountId,
publicConversation.id,
);
conversation.onError = (err) => {
if (err instanceof ApiError) {
// invalidate cache
this.cache.delete(chatId);
this.logger.error(`ApiError: ${err.message}`);
this.logger.error(
`ApiError occurred, invalidating cache for chat.id: ${chatId}, conversation.id: ${publicConversation.id}`,
);
}
};
return conversation;
}
public ConversationById(conversationId: number): Conversation {
return new Conversation(
this.accountAPI,
this.config.accountId,
conversationId,
);
}
/**
* Build specific contact for inbox notifications
* @constructor
*/
public async InboxNotifications() {
return this.ConversationByContact(new InboxContactInfo(this.l));
}
}
+53
View File
@@ -0,0 +1,53 @@
import ChatwootClient from '@figuro/chatwoot-sdk';
import type { conversation_message_create } from '@figuro/chatwoot-sdk/dist/models/conversation_message_create';
import { MessageType } from '@waha/apps/chatwoot/client/types';
export class Conversation {
public onError: (e: any) => void;
constructor(
private accountAPI: ChatwootClient,
private accountId: number,
public conversationId: number,
) {}
public async send(data: conversation_message_create) {
try {
const message = await this.accountAPI.messages.create({
accountId: this.accountId,
conversationId: this.conversationId,
data: data,
});
return message;
} catch (err) {
if (this.onError) {
this.onError(err);
}
throw err;
}
}
public async incoming(text: string) {
const data: conversation_message_create = {
content: text,
message_type: MessageType.INCOMING as any,
};
return this.send(data);
}
public async activity(text: string) {
const data: conversation_message_create = {
content: text,
message_type: MessageType.ACTIVITY as any,
};
return this.send(data);
}
public async outgoing(text: string) {
const data: conversation_message_create = {
content: text,
message_type: MessageType.OUTGOING as any,
};
return this.send(data);
}
}
@@ -0,0 +1,52 @@
import { public_conversation } from '@figuro/chatwoot-sdk';
import { ILogger } from '@waha/apps/app_sdk/ILogger';
import {
ChatWootAPIConfig,
ChatWootInboxAPI,
} from '@waha/apps/chatwoot/client/interfaces';
export class ConversationAPI {
constructor(
private config: ChatWootAPIConfig,
private inboxAPI: ChatWootInboxAPI,
private logger: ILogger,
) {}
private async find(
contactIdentifier: string,
): Promise<public_conversation | null> {
const request = {
inboxIdentifier: this.config.inboxIdentifier,
contactIdentifier: contactIdentifier,
};
const conversations = await this.inboxAPI.conversations.list(request);
if (conversations.length != 0) {
return conversations[0];
}
return null;
}
private async create(
contactIdentifier: string,
): Promise<public_conversation> {
const conversation = await this.inboxAPI.conversations.create({
inboxIdentifier: this.config.inboxIdentifier,
contactIdentifier: contactIdentifier,
});
this.logger.info(
`Created conversation.id: ${conversation.id} for contact.id: ${contactIdentifier}`,
);
return conversation;
}
async upsert(contactIdentifier: string): Promise<public_conversation> {
let conversation = await this.find(contactIdentifier);
if (!conversation) {
conversation = await this.create(contactIdentifier);
}
this.logger.info(
`Using conversation.id: ${conversation.id} for contact.id: ${contactIdentifier}`,
);
return conversation;
}
}
@@ -0,0 +1,53 @@
import ChatwootClient from '@figuro/chatwoot-sdk';
import { ChatWootAccountAPIConfig } from '@waha/apps/chatwoot/client/interfaces';
import {
CustomAttributeModel,
CustomAttributeType,
} from '@waha/apps/chatwoot/client/types';
export interface CustomAttribute {
key: string;
name: string;
type: CustomAttributeType;
description: string;
model: CustomAttributeModel;
}
export class CustomAttributesAPI {
constructor(
private config: ChatWootAccountAPIConfig,
private accountAPI: ChatwootClient,
) {}
async upsert(attribute: CustomAttribute): Promise<void> {
const attributes = await this.accountAPI.customAttributes.list({
accountId: this.config.accountId,
attributeModel: String(attribute.model) as '0' | '1',
});
const existing = attributes.find((a) => a.attribute_key === attribute.key);
if (existing) {
await this.accountAPI.customAttributes.update({
accountId: this.config.accountId,
id: existing.id,
data: {
attribute_key: attribute.key,
attribute_display_name: attribute.name,
attribute_display_type: attribute.type,
attribute_description: attribute.description,
},
});
} else {
await this.accountAPI.customAttributes.create({
accountId: this.config.accountId,
data: {
attribute_key: attribute.key,
attribute_display_name: attribute.name,
attribute_display_type: attribute.type,
attribute_description: attribute.description,
attribute_model: attribute.model,
},
});
}
}
}
+47
View File
@@ -0,0 +1,47 @@
import { isLidUser } from '@adiwajshing/baileys/lib/WABinary/jid-utils';
import { AttributeKey } from '@waha/apps/chatwoot/const';
import { WhatsAppMessage } from '@waha/apps/chatwoot/storage';
import { buildMessageId } from '@waha/core/engines/noweb/session.noweb.core';
export function GetJID(contact: any): string | null {
return contact?.custom_attributes?.[AttributeKey.WA_JID];
}
export function GetLID(contact: any): string | null {
return contact?.custom_attributes?.[AttributeKey.WA_LID];
}
export function GetChatID(contact: any): string | null {
return contact?.custom_attributes?.[AttributeKey.WA_CHAT_ID];
}
export function FindChatID(contact: any): string | null {
if (GetJID(contact)) {
return GetJID(contact);
}
if (GetLID(contact)) {
return GetLID(contact);
}
if (GetChatID(contact)) {
return GetChatID(contact);
}
return null;
}
export function SerializeWhatsAppKey(message: WhatsAppMessage): string {
const key = {
id: message.message_id,
remoteJid: message.chat_id,
fromMe: Boolean(message.from_me),
participant: message.participant,
};
return buildMessageId(key);
}
export function ContactAttr(chatId: string): AttributeKey {
if (isLidUser(chatId)) {
return AttributeKey.WA_LID;
} else {
return AttributeKey.WA_JID;
}
}
+28
View File
@@ -0,0 +1,28 @@
// Original '@figuro/chatwoot-sdk'
// Doesn't have the right types for client.client
// So we defined it here
import { ContactsApi } from '@figuro/chatwoot-sdk/dist/services/client/Contacts';
import { ConversationsApi } from '@figuro/chatwoot-sdk/dist/services/client/Conversations';
import { MessagesApi } from '@figuro/chatwoot-sdk/dist/services/client/Messages';
export interface ChatWootInboxAPI {
contacts: ContactsApi;
conversations: ConversationsApi;
messages: MessagesApi;
}
export interface ChatWootAccountAPIConfig {
url: string;
accountId: number;
accountToken: string;
}
export interface ChatWootInboxAPIConfig {
url: string;
inboxId: number;
inboxIdentifier: string;
}
export interface ChatWootAPIConfig
extends ChatWootAccountAPIConfig,
ChatWootInboxAPIConfig {}
+21
View File
@@ -0,0 +1,21 @@
import { conversation_message_create } from '@figuro/chatwoot-sdk/dist/models/conversation_message_create';
import { SendAttachment } from '@waha/apps/chatwoot/client/types';
export function AttachmentFromBuffer(
buffer: Buffer,
filename: string,
): conversation_message_create {
const content = buffer.toString('base64');
const attachments: SendAttachment[] = [
{
content: content,
filename: filename,
encoding: 'base64',
},
];
const message: conversation_message_create = {
content: '',
attachments: attachments as any,
};
return message;
}
+53
View File
@@ -0,0 +1,53 @@
export enum EventName {
CONVERSATION_CREATED = 'conversation_created',
CONVERSATION_STATUS_CHANGED = 'conversation_status_changed',
CONVERSATION_UPDATED = 'conversation_updated',
CONTACT_CREATED = 'contact_created',
CONTACT_UPDATED = 'contact_updated',
MESSAGE_CREATED = 'message_created',
MESSAGE_UPDATED = 'message_updated',
WEBWIDGET_TRIGGERED = 'webwidget_triggered',
CONVERSATION_TYPING_ON = 'conversation_typing_on',
CONVERSATION_TYPING_OFF = 'conversation_typing_off',
}
export interface MessageAttachment {
id: number;
account_id: number;
message_id: number;
file_type: 'image' | 'video' | 'audio' | string;
file_size: number;
data_url?: string;
thumb_url?: string;
extension?: string;
width?: number;
height?: number;
}
export interface SendAttachment {
content: string;
filename?: string;
encoding: 'base64';
}
export enum MessageType {
OUTGOING = 'outgoing',
INCOMING = 'incoming',
ACTIVITY = 'activity',
}
export enum CustomAttributeType {
TEXT = 0,
NUMBER = 1,
CURRENCY = 2,
PERCENT = 3,
LINK = 4,
DATE = 5,
LIST = 6,
CHECKBOX = 7,
}
export enum CustomAttributeModel {
CONVERSATION = 0,
CONTACT = 1,
}
+35
View File
@@ -0,0 +1,35 @@
import { CustomAttribute } from '@waha/apps/chatwoot/client/CustomAttributesAPI';
import { CustomAttributeModel, CustomAttributeType } from './client/types';
export enum AttributeKey {
WA_CHAT_ID = 'waha_whatsapp_chat_id',
WA_JID = 'waha_whatsapp_jid',
WA_LID = 'waha_whatsapp_lid',
}
export const CHATWOOT_CUSTOM_ATTRIBUTES: CustomAttribute[] = [
{
key: AttributeKey.WA_CHAT_ID,
name: 'WhatsApp Chat ID',
description: 'WhatsApp Chat ID',
type: CustomAttributeType.TEXT,
model: CustomAttributeModel.CONTACT,
},
{
key: AttributeKey.WA_JID,
name: 'WhatsApp JID',
description: 'WhatsApp Phone Number ID',
type: CustomAttributeType.TEXT,
model: CustomAttributeModel.CONTACT,
},
{
key: AttributeKey.WA_LID,
name: 'WhatsApp LID',
description: 'WhatsApp Linked ID (Anonymous ID)',
type: CustomAttributeType.TEXT,
model: CustomAttributeModel.CONTACT,
},
];
export const INBOX_CONTACT_CHAT_ID = 'whatsapp.integration';
+26
View File
@@ -0,0 +1,26 @@
export enum QueueName {
//
// Scheduled
//
SCHEDULED_MESSAGE_CLEANUP = 'chatwoot.scheduled | message.cleanup',
SCHEDULED_CHECK_VERSION = 'chatwoot.scheduled | check.version',
//
// WAHA Events
//
WAHA_SESSION_STATUS = 'chatwoot.waha | session.status',
WAHA_MESSAGE_ANY = 'chatwoot.waha | message.any',
WAHA_MESSAGE_REACTION = 'chatwoot.waha | message.reaction',
WAHA_MESSAGE_EDITED = 'chatwoot.waha | message.edited',
WAHA_MESSAGE_REVOKED = 'chatwoot.waha | message.revoked',
//
// ChatWoot Events - Real
//
INBOX_MESSAGE_CREATED = 'chatwoot.inbox | message_created',
INBOX_MESSAGE_UPDATED = 'chatwoot.inbox | message_updated',
//
// ChatWoot Events - Artificial
//
INBOX_MESSAGE_DELETED = 'chatwoot.inbox | message_deleted',
INBOX_COMMANDS = 'chatwoot.inbox | commands',
}
+139
View File
@@ -0,0 +1,139 @@
import { AppConsumer } from '@waha/apps/app_sdk/AppConsumer';
import { JobLoggerWrapper } from '@waha/apps/app_sdk/JobLoggerWrapper';
import { HasBeenRetried } from '@waha/apps/app_sdk/JobUtils';
import { FindChatID } from '@waha/apps/chatwoot/client/ids';
import { EventName } from '@waha/apps/chatwoot/client/types';
import { ChatWootConversationKey } from '@waha/apps/chatwoot/consumers/mutex';
import { InboxData } from '@waha/apps/chatwoot/consumers/types';
import { DIContainer } from '@waha/apps/chatwoot/di/DIContainer';
import {
ChatIDNotFoundForContactError,
PhoneNumberNotFoundInWhatsAppError,
} from '@waha/apps/chatwoot/errors';
import { TKey } from '@waha/apps/chatwoot/locale';
import { WAHASessionAPI } from '@waha/apps/chatwoot/session/WAHASelf';
import { SessionManager } from '@waha/core/abc/manager.abc';
import { RMutexService } from '@waha/modules/rmutex/rmutex.service';
import { Job } from 'bullmq';
import { PinoLogger } from 'nestjs-pino';
import { AppRepository } from '../../storage';
/**
* Base class for ChatWoot inbox consumers
* Contains common logic for all inbox consumers
*/
export abstract class ChatWootInboxMessageConsumer extends AppConsumer {
protected appRepository: AppRepository;
constructor(
protected readonly manager: SessionManager,
log: PinoLogger,
rmutex: RMutexService,
protected readonly consumerName: string,
) {
super('ChatWoot', consumerName, log, rmutex);
}
/**
* Gets the DIContainer for the specified app.
*/
protected async DIContainer(job: Job, appId: string): Promise<DIContainer> {
const knex = this.manager.store.getWAHADatabase();
this.appRepository = new AppRepository(knex);
const logger = new JobLoggerWrapper(job, this.logger);
const app = await this.appRepository.getById(appId);
return new DIContainer(app.pk, app.config, logger, knex);
}
protected abstract ErrorHeaderKey(): TKey | null;
/**
* Get the function that processes the message
* This method must be implemented by subclasses
*/
protected abstract Process(
container: DIContainer,
body: any,
job: Job,
): Promise<any>;
/**
* Process the job
* This method is called by the queue processor
*/
async processJob(job: Job<InboxData, any, EventName>): Promise<any> {
const body = job.data.body;
const key = ChatWootConversationKey(job.data.app, body.conversation.id);
return await this.withMutex(job, key, () =>
this.ProcessAndReportStatus(job),
);
}
private async ProcessAndReportStatus(job) {
const body = job.data.body;
try {
const container = await this.DIContainer(job, job.data.app);
const result = await this.Process(container, body, job);
await this.ReportErrorRecovered(job, body);
return result;
} catch (err) {
await this.ReportErrorForMessage(job, err, body);
throw err;
}
}
/**
* Report an error for a message
*/
protected async ReportErrorForMessage(job: Job, err: any, body: any) {
const container = await this.DIContainer(job, job.data.app);
const header: string = this.ErrorHeaderKey()
? container.Locale().key(this.ErrorHeaderKey()).render()
: err.message || `${err}`;
const conversation = container
.ContactConversationService()
.ConversationById(body.conversation.id);
const reporter = container.ChatWootErrorReporter(job);
await reporter.ReportError(
conversation,
header,
body.message_type,
err,
body.id,
);
throw err;
}
protected async ReportErrorRecovered(job: Job, body: any) {
if (!HasBeenRetried(job)) {
return;
}
const container = await this.DIContainer(job, job.data.app);
const conversation = container
.ContactConversationService()
.ConversationById(body.conversation.id);
const reporter = container.ChatWootErrorReporter(job);
await reporter.ReportSucceeded(conversation, body.message_type, body.id);
}
}
export async function LookupAndCheckChatId(
session: WAHASessionAPI,
body: any,
): Promise<string> {
const sender = body.conversation.meta.sender;
let chatId = FindChatID(sender);
if (!chatId && sender.phone_number) {
const existResult = await session.contactCheckExists(sender.phone_number);
if (!existResult.numberExists) {
throw new PhoneNumberNotFoundInWhatsAppError(sender.phone_number);
}
chatId = existResult.chatId;
}
if (!chatId) {
throw new ChatIDNotFoundForContactError(sender);
}
return chatId;
}
@@ -0,0 +1,208 @@
import { Processor } from '@nestjs/bullmq';
import { JOB_CONCURRENCY } from '@waha/apps/app_sdk/constants';
import { ILogger } from '@waha/apps/app_sdk/ILogger';
import { ContactConversationService } from '@waha/apps/chatwoot/client/ContactConversationService';
import { AttachmentFromBuffer } from '@waha/apps/chatwoot/client/messages';
import { MessageType } from '@waha/apps/chatwoot/client/types';
import { ChatWootInboxMessageConsumer } from '@waha/apps/chatwoot/consumers/inbox/base';
import { QueueName } from '@waha/apps/chatwoot/consumers/QueueName';
import { DIContainer } from '@waha/apps/chatwoot/di/DIContainer';
import { Locale, TKey } from '@waha/apps/chatwoot/locale';
import { WAHASelf } from '@waha/apps/chatwoot/session/WAHASelf';
import { SessionStatusEmoji } from '@waha/apps/chatwoot/SessionStatusEmoji';
import { SessionManager } from '@waha/core/abc/manager.abc';
import { RMutexService } from '@waha/modules/rmutex/rmutex.service';
import { Job } from 'bullmq';
import { PinoLogger } from 'nestjs-pino';
@Processor(QueueName.INBOX_COMMANDS, { concurrency: JOB_CONCURRENCY })
export class ChatWootInboxCommandsConsumer extends ChatWootInboxMessageConsumer {
constructor(
protected readonly manager: SessionManager,
log: PinoLogger,
rmutex: RMutexService,
) {
super(manager, log, rmutex, 'ChatWootInboxCommandsConsumer');
}
ErrorHeaderKey(): TKey | null {
return null;
}
protected async Process(container: DIContainer, body, job: Job) {
const handler = new SessionCommandHandler(
container.Logger(),
container.ContactConversationService(),
job.data.session,
container.Locale(),
container.WAHASelf(),
);
return await handler.handle(body);
}
}
enum Command {
STATUS = 'status',
RESTART = 'restart',
START = 'start',
STOP = 'stop',
LOGOUT = 'logout',
QR = 'qr',
SCREENSHOT = 'screenshot',
HELP = 'help',
SERVER_STATUS = 'server status',
SERVER_REBOOT = 'server reboot',
SERVER_REBOOT_FORCE = 'server reboot force',
}
const CommandMap: Record<string, Command> = {
// Status
[Command.STATUS]: Command.STATUS,
['1']: Command.STATUS,
// Restart
[Command.RESTART]: Command.RESTART,
['2']: Command.RESTART,
// Start
[Command.START]: Command.START,
['3']: Command.START,
// Stop
[Command.STOP]: Command.STOP,
['4']: Command.STOP,
// Logout
[Command.LOGOUT]: Command.LOGOUT,
['5']: Command.LOGOUT,
// QR
[Command.QR]: Command.QR,
['6']: Command.QR,
// Screenshot
[Command.SCREENSHOT]: Command.SCREENSHOT,
['7']: Command.SCREENSHOT,
// Help
[Command.HELP]: Command.HELP,
['8']: Command.HELP,
// Server commands
[Command.SERVER_STATUS]: Command.SERVER_STATUS,
[Command.SERVER_REBOOT]: Command.SERVER_REBOOT,
[Command.SERVER_REBOOT_FORCE]: Command.SERVER_REBOOT_FORCE,
};
class CommandIsNotImplementedError extends Error {
constructor(private command: string) {
super(`Command is not implemented`);
}
}
class SessionCommandHandler {
constructor(
private logger: ILogger,
private repo: ContactConversationService,
private session: string,
private l: Locale,
private waha: WAHASelf,
) {}
async handle(message: any) {
const cmd = message.content?.toLowerCase();
const command = this.findCommand(cmd) || Command.HELP;
this.logger.info(
`Executing command ${command} for session ${this.session}`,
);
switch (command) {
case Command.RESTART:
await this.waha.restart(this.session);
break;
case Command.START:
await this.waha.restart(this.session);
break;
case Command.LOGOUT:
this.logger.info(`Logging out session ${this.session}`);
await this.waha.logout(this.session);
const conversation = await this.repo.InboxNotifications();
const text = this.l.key(TKey.APP_LOGOUT_SUCCESS).render();
await conversation.incoming(text);
break;
case Command.STOP:
this.logger.info(`Stopping session ${this.session}`);
await this.waha.stop(this.session);
break;
case Command.STATUS: {
this.logger.info(`Getting status for session ${this.session}`);
const session = await this.waha.get(this.session);
const conversation = await this.repo.InboxNotifications();
const emoji = SessionStatusEmoji(session.status);
const text = this.l.key(TKey.APP_SESSION_CURRENT_STATUS).render({
emoji: emoji,
session: session.name,
status: session.status,
name: session.me?.pushName || 'Unknown',
id: session.me?.id || 'No phone number',
});
await conversation.incoming(text);
break;
}
case Command.SERVER_STATUS: {
this.logger.info('Getting server version and status');
const version = await this.waha.serverVersion();
const status = await this.waha.serverStatus();
const conversation = await this.repo.InboxNotifications();
const text = this.l.key(TKey.APP_SERVER_VERSION_AND_STATUS).render({
version: JSON.stringify(version, null, 2),
status: JSON.stringify(status, null, 2),
});
await conversation.incoming(text);
break;
}
case Command.SERVER_REBOOT: {
this.logger.info('Rebooting server (graceful)');
const conversation = await this.repo.InboxNotifications();
const text = this.l.key(TKey.APP_SERVER_REBOOT).render();
await conversation.incoming(text);
await this.waha.serverReboot(false);
break;
}
case Command.SERVER_REBOOT_FORCE: {
this.logger.info('Rebooting server (force)');
const conversation = await this.repo.InboxNotifications();
const text = this.l.key(TKey.APP_SERVER_REBOOT_FORCE).render();
await conversation.incoming(text);
await this.waha.serverReboot(true);
break;
}
case Command.QR: {
const conversation = await this.repo.InboxNotifications();
const content = await this.waha.qr(this.session);
const message = AttachmentFromBuffer(content, 'qr.jpg');
message.message_type = MessageType.INCOMING;
await conversation.send(message);
break;
}
case Command.SCREENSHOT: {
this.logger.info(`Getting screenshot for session ${this.session}`);
const conversation = await this.repo.InboxNotifications();
const content = await this.waha.screenshot(this.session);
const message = AttachmentFromBuffer(content, 'screenshot.jpg');
message.message_type = MessageType.INCOMING;
await conversation.send(message);
break;
}
case Command.HELP: {
const conversation = await this.repo.InboxNotifications();
const text = this.l.key(TKey.APP_COMMANDS_LIST).render();
await conversation.incoming(text);
break;
}
default:
throw new CommandIsNotImplementedError(cmd);
}
}
findCommand(text: string): Command {
// Remove empty from end
text = text.trim();
// Remove empty from start
text = text.trimStart();
// Remove empty from end
text = text.trimEnd();
return CommandMap[text] || null;
}
}
@@ -0,0 +1,242 @@
import { Processor } from '@nestjs/bullmq';
import { JOB_CONCURRENCY } from '@waha/apps/app_sdk/constants';
import { ILogger } from '@waha/apps/app_sdk/ILogger';
import { MessageAttachment } from '@waha/apps/chatwoot/client/types';
import {
ChatWootInboxMessageConsumer,
LookupAndCheckChatId,
} from '@waha/apps/chatwoot/consumers/inbox/base';
import { QueueName } from '@waha/apps/chatwoot/consumers/QueueName';
import { DIContainer } from '@waha/apps/chatwoot/di/DIContainer';
import { TKey } from '@waha/apps/chatwoot/locale';
import { EngineHelper } from '@waha/apps/chatwoot/session';
import { WAHASessionAPI } from '@waha/apps/chatwoot/session/WAHASelf';
import {
ChatwootMessage,
MessageMappingService,
} from '@waha/apps/chatwoot/storage';
import { MarkdownToWhatsApp } from '@waha/apps/chatwoot/text';
import { SessionManager } from '@waha/core/abc/manager.abc';
import { RMutexService } from '@waha/modules/rmutex/rmutex.service';
import {
MessageFileRequest,
MessageImageRequest,
MessageTextRequest,
MessageVideoRequest,
MessageVoiceRequest,
} from '@waha/structures/chatting.dto';
import { sleep } from '@waha/utils/promiseTimeout';
import { Job } from 'bullmq';
import { PinoLogger } from 'nestjs-pino';
import { SerializeWhatsAppKey } from '../../client/ids';
// eslint-disable-next-line @typescript-eslint/no-var-requires
const mime = require('mime-types');
@Processor(QueueName.INBOX_MESSAGE_CREATED, { concurrency: JOB_CONCURRENCY })
export class ChatWootInboxMessageCreatedConsumer extends ChatWootInboxMessageConsumer {
constructor(
protected readonly manager: SessionManager,
log: PinoLogger,
rmutex: RMutexService,
) {
super(manager, log, rmutex, 'ChatWootInboxMessageCreatedConsumer');
}
ErrorHeaderKey(): TKey | null {
return TKey.WHATSAPP_ERROR_SENDING_MESSAGE;
}
protected async Process(container: DIContainer, body, job: Job) {
const waha = container.WAHASelf();
const session = new WAHASessionAPI(job.data.session, waha);
const handler = new MessageHandler(
container.MessageMappingService(),
container.Logger(),
session,
);
return await handler.handle(body);
}
}
export class MessageHandler {
constructor(
private mappingService: MessageMappingService,
private logger: ILogger,
private session: WAHASessionAPI,
) {}
async handle(body: any) {
const chatId = await LookupAndCheckChatId(this.session, body);
const message = body;
if (message.content_type != 'text') {
this.logger.info(
`Message content type not supported. Content type: ${message.content_type}`,
);
return;
}
const content = MarkdownToWhatsApp(message.content);
const attachments = message.attachments || [];
const results = [];
const multipleAttachments = attachments.length > 1;
let part = 1;
const replyTo = await this.getReplyTo(message).catch((err) => {
this.logger.error(`Error getting reply to message ID: ${err}`);
return undefined;
});
// Send text
if (attachments.length == 0 || multipleAttachments) {
const msg = await this.sendTextMessage(chatId, content, replyTo);
results.push(msg);
part = await this.saveMapping(message, msg, part);
this.logger.info(`Text message sent: ${msg.id}`);
}
// Send files
const fileMessageContent = multipleAttachments ? '' : content;
for (const file of attachments) {
const msg = await this.sendFile(
chatId,
fileMessageContent,
file,
replyTo,
);
this.logger.info(
`File message sent: ${msg.id} - ${file.data_url} - ${file.file_type}`,
);
results.push(msg);
part = await this.saveMapping(message, msg, part);
}
return results;
}
private async saveMapping(
chatwootMessage: any,
whatsappMessage: any,
part: number,
): Promise<number> {
const chatwoot: Omit<ChatwootMessage, 'id'> = {
timestamp: new Date(chatwootMessage.created_at),
conversation_id: chatwootMessage.conversation.id,
message_id: chatwootMessage.id,
};
const whatsapp = EngineHelper.WhatsAppMessageKeys(whatsappMessage);
await this.mappingService.map(chatwoot, whatsapp, 1);
return part + 1;
}
async getReplyTo(message): Promise<string | undefined> {
const message_id = message.content_attributes.in_reply_to;
if (!message_id) {
return;
}
const messages = await this.mappingService.getWhatsAppMessage({
conversation_id: message.conversation.id,
message_id: message_id,
});
if (!messages || messages.length == 0) {
return;
}
const whatsapp = messages[0];
return SerializeWhatsAppKey(whatsapp);
}
private async sendTextMessage(
chatId: string,
content: string,
replyTo: string,
) {
const request: MessageTextRequest = {
session: '',
chatId: chatId,
text: content,
reply_to: replyTo,
};
const session = this.session;
await session.readMessages(chatId);
await session.startTyping({ chatId: chatId, session: '' });
await sleep(2000);
await session.stopTyping({ chatId: chatId, session: '' });
const msg = await session.sendText(request);
return msg;
}
private async sendFile(
chatId: string,
content: string,
file: MessageAttachment,
replyTo: string,
) {
const url = file.data_url;
let filename = url.split('/').pop();
if (filename) {
filename = decodeURIComponent(filename);
}
const mimetype = mime.lookup(filename);
const session = this.session;
switch (file.file_type) {
case 'image':
if (mimetype != 'image/jpeg' && mimetype != 'image/png') {
// Send it as a file
break;
}
const imageRequest: MessageImageRequest = {
session: '',
caption: content,
chatId: chatId,
reply_to: replyTo,
file: {
url: url,
mimetype: mimetype,
},
};
return session.sendImage(imageRequest);
case 'video':
if (mimetype != 'video/mp4') {
break;
}
const videoRequest: MessageVideoRequest = {
session: '',
caption: content,
chatId: chatId,
reply_to: replyTo,
file: {
url: url,
mimetype: mimetype,
filename: filename,
},
convert: true,
};
return session.sendVideo(videoRequest);
case 'audio':
const voiceRequest: MessageVoiceRequest = {
session: '',
chatId: chatId,
file: {
url: url,
mimetype: mimetype,
filename: filename,
},
convert: true,
};
return session.sendVoice(voiceRequest);
}
// Fallback and send as file (attachment)
const fileRequest: MessageFileRequest = {
session: '',
caption: content,
chatId: chatId,
reply_to: replyTo,
file: {
filename: filename,
url: url,
mimetype: mimetype,
},
};
return session.sendFile(fileRequest);
}
}
@@ -0,0 +1,76 @@
import { Processor } from '@nestjs/bullmq';
import { JOB_CONCURRENCY } from '@waha/apps/app_sdk/constants';
import { ILogger } from '@waha/apps/app_sdk/ILogger';
import { SerializeWhatsAppKey } from '@waha/apps/chatwoot/client/ids';
import {
ChatWootInboxMessageConsumer,
LookupAndCheckChatId,
} from '@waha/apps/chatwoot/consumers/inbox/base';
import { QueueName } from '@waha/apps/chatwoot/consumers/QueueName';
import { DIContainer } from '@waha/apps/chatwoot/di/DIContainer';
import { TKey } from '@waha/apps/chatwoot/locale';
import { WAHASessionAPI } from '@waha/apps/chatwoot/session/WAHASelf';
import { MessageMappingService } from '@waha/apps/chatwoot/storage';
import { SessionManager } from '@waha/core/abc/manager.abc';
import { RMutexService } from '@waha/modules/rmutex/rmutex.service';
import { Job } from 'bullmq';
import { PinoLogger } from 'nestjs-pino';
@Processor(QueueName.INBOX_MESSAGE_DELETED, { concurrency: JOB_CONCURRENCY })
export class ChatWootInboxMessageDeletedConsumer extends ChatWootInboxMessageConsumer {
constructor(
protected readonly manager: SessionManager,
log: PinoLogger,
rmutex: RMutexService,
) {
super(manager, log, rmutex, 'ChatWootInboxMessageDeletedConsumer');
}
ErrorHeaderKey(): TKey | null {
return TKey.WHATSAPP_ERROR_REMOVING_MESSAGE;
}
protected async Process(container: DIContainer, body, job: Job) {
const waha = container.WAHASelf();
const session = new WAHASessionAPI(job.data.session, waha);
const handler = new MessageDeletedHandler(
container.MessageMappingService(),
container.Logger(),
session,
);
return await handler.handle(body);
}
}
class MessageDeletedHandler {
constructor(
private mappingService: MessageMappingService,
private logger: ILogger,
private session: WAHASessionAPI,
) {}
async handle(body: any) {
const chatId = await LookupAndCheckChatId(this.session, body);
const messages = await this.mappingService.getWhatsAppMessage({
conversation_id: body.conversation.id,
message_id: body.id,
});
if (!messages || messages.length == 0) {
throw Error(
`No WhatsApp message found for Chatwoot message '${body.id}'`,
);
}
const session = this.session;
for (const whatsapp of messages) {
this.logger.debug(
`Deleting message '${whatsapp.message_id}' from '${whatsapp.chat_id}'`,
);
const messageId = SerializeWhatsAppKey(whatsapp);
await session.deleteMessage(chatId, messageId);
this.logger.info(
`Message '${whatsapp.message_id}' deleted from '${whatsapp.chat_id}'`,
);
}
}
}
@@ -0,0 +1,39 @@
import { Processor } from '@nestjs/bullmq';
import { JOB_CONCURRENCY } from '@waha/apps/app_sdk/constants';
import { ChatWootInboxMessageConsumer } from '@waha/apps/chatwoot/consumers/inbox/base';
import { MessageHandler } from '@waha/apps/chatwoot/consumers/inbox/message_created';
import { QueueName } from '@waha/apps/chatwoot/consumers/QueueName';
import { DIContainer } from '@waha/apps/chatwoot/di/DIContainer';
import { TKey } from '@waha/apps/chatwoot/locale';
import { SessionManager } from '@waha/core/abc/manager.abc';
import { RMutexService } from '@waha/modules/rmutex/rmutex.service';
import { Job } from 'bullmq';
import { PinoLogger } from 'nestjs-pino';
import { WAHASessionAPI } from '../../session/WAHASelf';
@Processor(QueueName.INBOX_MESSAGE_UPDATED, { concurrency: JOB_CONCURRENCY })
export class ChatWootInboxMessageUpdatedConsumer extends ChatWootInboxMessageConsumer {
constructor(
protected readonly manager: SessionManager,
log: PinoLogger,
rmutex: RMutexService,
) {
super(manager, log, rmutex, 'ChatWootInboxMessageUpdatedConsumer');
}
ErrorHeaderKey(): TKey | null {
return TKey.WHATSAPP_ERROR_SENDING_MESSAGE;
}
protected async Process(container: DIContainer, body, job: Job) {
const waha = container.WAHASelf();
const session = new WAHASessionAPI(job.data.session, waha);
const handler = new MessageHandler(
container.MessageMappingService(),
container.Logger(),
session,
);
return await handler.handle(body);
}
}
+7
View File
@@ -0,0 +1,7 @@
export function WhatsAppChatIdKey(app: string, chatId: string) {
return `chatwoot.${app}.whatsapp.chat-id-${chatId}`;
}
export function ChatWootConversationKey(app: string, conversationId: string) {
return `chatwoot.${app}.chatwoot.conversation-id-${conversationId}`;
}
@@ -0,0 +1,103 @@
import { AppConsumer } from '@waha/apps/app_sdk/AppConsumer';
import { JobLoggerWrapper } from '@waha/apps/app_sdk/JobLoggerWrapper';
import { HasBeenRetried } from '@waha/apps/app_sdk/JobUtils';
import { MessageType } from '@waha/apps/chatwoot/client/types';
import { ScheduledData } from '@waha/apps/chatwoot/consumers/types';
import { DIContainer } from '@waha/apps/chatwoot/di/DIContainer';
import { ChatWootAppConfig } from '@waha/apps/chatwoot/dto/config.dto';
import { TKey } from '@waha/apps/chatwoot/locale';
import { SessionManager } from '@waha/core/abc/manager.abc';
import { RMutexService } from '@waha/modules/rmutex/rmutex.service';
import { Job } from 'bullmq';
import { PinoLogger } from 'nestjs-pino';
import { AppRepository } from '../../storage';
/**
* Base class for ChatWoot scheduled consumers
* Contains common logic for all scheduled consumers
*/
export abstract class ChatWootScheduledConsumer extends AppConsumer {
protected appRepository: AppRepository;
constructor(
protected readonly manager: SessionManager,
log: PinoLogger,
rmutex: RMutexService,
protected readonly consumerName: string,
) {
super('ChatWoot', consumerName, log, rmutex);
}
/**
* Gets the DIContainer for the specified app.
*/
protected async DIContainer(job: Job, appId: string): Promise<DIContainer> {
const knex = this.manager.store.getWAHADatabase();
this.appRepository = new AppRepository(knex);
const logger = new JobLoggerWrapper(job, this.logger);
const app = await this.appRepository.getById(appId);
return new DIContainer(app.pk, app.config, logger, knex);
}
protected abstract ErrorHeaderKey(): TKey | null;
/**
* Get the function that processes the scheduled job
* This method must be implemented by subclasses
*/
protected abstract Process(container: DIContainer, job: Job): Promise<any>;
/**
* Process the job
* This method is called by the queue processor
*/
async processJob(job: Job<ScheduledData, any, string>): Promise<any> {
return this.ProcessAndReportStatus(job);
}
private async ProcessAndReportStatus(job: Job) {
try {
const container = await this.DIContainer(job, job.data.app);
const result = await this.Process(container, job);
await this.ReportErrorRecovered(job);
return result;
} catch (err) {
await this.ReportErrorForJob(job, err);
throw err;
}
}
/**
* Report an error for a scheduled job
*/
protected async ReportErrorForJob(job: Job, err: any) {
const container = await this.DIContainer(job, job.data.app);
let header = this.ErrorHeaderKey()
? container.Locale().key(this.ErrorHeaderKey()).render()
: err.message || `${err}`;
header = `${job.queueName}: ${header}`;
const conversation = await container
.ContactConversationService()
.InboxNotifications();
const reporter = container.ChatWootErrorReporter(job);
await reporter.ReportError(conversation, header, MessageType.INCOMING, err);
throw err;
}
protected async ReportErrorRecovered(job: Job) {
if (!HasBeenRetried(job)) {
return;
}
const container = await this.DIContainer(job, job.data.app);
const conversation = await container
.ContactConversationService()
.InboxNotifications();
const reporter = container.ChatWootErrorReporter(job);
await reporter.ReportSucceeded(conversation, MessageType.INCOMING);
}
}
@@ -0,0 +1,96 @@
import { Processor } from '@nestjs/bullmq';
import { Injectable } from '@nestjs/common';
import { JOB_CONCURRENCY } from '@waha/apps/app_sdk/constants';
import { ILogger } from '@waha/apps/app_sdk/ILogger';
import { QueueName } from '@waha/apps/chatwoot/consumers/QueueName';
import { DIContainer } from '@waha/apps/chatwoot/di/DIContainer';
import { TKey } from '@waha/apps/chatwoot/locale';
import { SessionManager } from '@waha/core/abc/manager.abc';
import { CHANGELOG_URL, SUPPORT_US_URL } from '@waha/core/constants';
import { RMutexService } from '@waha/modules/rmutex/rmutex.service';
import { VERSION, WAHAVersion } from '@waha/version';
import axios from 'axios';
import { Job } from 'bullmq';
import { PinoLogger } from 'nestjs-pino';
import { ChatWootScheduledConsumer } from './base';
/**
* Scheduled consumer for version checking
* Periodically checks for new versions and notifies if updates are available
*/
@Processor(QueueName.SCHEDULED_CHECK_VERSION, { concurrency: JOB_CONCURRENCY })
export class CheckVersionConsumer extends ChatWootScheduledConsumer {
constructor(manager: SessionManager, log: PinoLogger, rmutex: RMutexService) {
super(manager, log, rmutex, CheckVersionConsumer.name);
}
protected ErrorHeaderKey(): TKey {
return TKey.APP_SCHEDULED_JOB_ERROR;
}
/**
* Process the version check job
* Checks for new versions and notifies if updates are available
*/
protected async Process(container: DIContainer, job: Job): Promise<any> {
await this.CheckWAHACoreVersion(container);
await this.CheckNewVersionAvailable(container);
}
private async CheckWAHACoreVersion(container: DIContainer) {
const logger = container.Logger();
const locale = container.Locale();
const conversation = await container
.ContactConversationService()
.InboxNotifications();
// If using the CORE version, send a message encouraging support
if (VERSION.tier !== WAHAVersion.CORE) {
logger.info('WAHA is not using the CORE version');
return;
}
logger.info('WAHA is using the CORE version');
const supportMessage = locale.key(TKey.APP_CORE_VERSION_USED).render({
supportUrl: SUPPORT_US_URL,
});
await conversation.incoming(supportMessage);
}
private async CheckNewVersionAvailable(container: DIContainer) {
const logger = container.Logger();
logger.info('Processing version check job');
const currentVersion = VERSION.version;
logger.info(`Current WAHA version: ${currentVersion}`);
const latestVersion = await this.fetchLatestVersion(logger);
const isNewVersionAvailable = currentVersion !== latestVersion;
if (!isNewVersionAvailable) {
logger.info('WAHA is up to date');
return;
}
logger.info(
`New version available: ${latestVersion} (current: ${currentVersion})`,
);
// Send a message to Chatwoot conversation about a new version
const locale = container.Locale();
const message = locale.key(TKey.APP_NEW_VERSION_AVAILABLE).render({
currentVersion: currentVersion,
newVersion: latestVersion,
changelogUrl: CHANGELOG_URL,
});
const conversation = await container
.ContactConversationService()
.InboxNotifications();
await conversation.incoming(message);
}
private async fetchLatestVersion(logger: ILogger): Promise<string> {
const response = await axios.get(
'https://api.github.com/repos/devlikeapro/waha/releases/latest',
);
const latestVersion = response.data.name;
logger.info(`Latest WAHA version: ${latestVersion}`);
return latestVersion;
}
}
@@ -0,0 +1,48 @@
import { Processor } from '@nestjs/bullmq';
import { Injectable } from '@nestjs/common';
import { JOB_CONCURRENCY } from '@waha/apps/app_sdk/constants';
import { QueueName } from '@waha/apps/chatwoot/consumers/QueueName';
import { DIContainer } from '@waha/apps/chatwoot/di/DIContainer';
import { TKey } from '@waha/apps/chatwoot/locale';
import { SessionManager } from '@waha/core/abc/manager.abc';
import { RMutexService } from '@waha/modules/rmutex/rmutex.service';
import { Job } from 'bullmq';
import { PinoLogger } from 'nestjs-pino';
import { ChatWootScheduledConsumer } from './base';
/**
* Scheduled consumer for message cleanup tasks
* Handles periodic cleanup of old messages and related data
*/
@Processor(QueueName.SCHEDULED_MESSAGE_CLEANUP, {
concurrency: JOB_CONCURRENCY,
})
export class MessageCleanupConsumer extends ChatWootScheduledConsumer {
REMOVE_AFTER_DAYS = 14;
constructor(manager: SessionManager, log: PinoLogger, rmutex: RMutexService) {
super(manager, log, rmutex, MessageCleanupConsumer.name);
}
protected ErrorHeaderKey(): TKey {
return TKey.APP_SCHEDULED_JOB_ERROR;
}
/**
* Process the message cleanup job
* Performs cleanup of old messages and related data
*/
protected async Process(container: DIContainer, job: Job): Promise<any> {
const logger = container.Logger();
logger.info('Processing message cleanup job');
const removeAfter = new Date(
Date.now() - this.REMOVE_AFTER_DAYS * 24 * 60 * 60 * 1000,
);
logger.info(`Removing mapping for messages older than ${removeAfter}`);
const removed = await container
.MessageMappingService()
.cleanup(removeAfter);
logger.info(`Removed ${removed} mappings for messages`);
}
}
+17
View File
@@ -0,0 +1,17 @@
import { WAHAWebhook } from '@waha/structures/webhooks.dto';
export interface InboxData {
session: string;
app: string;
body: any;
}
export interface EventData {
app: string;
event: WAHAWebhook;
}
export interface ScheduledData {
app: string;
session: string;
}
+330
View File
@@ -0,0 +1,330 @@
import { isJidGroup, isJidStatusBroadcast } from '@adiwajshing/baileys';
import { conversation_message_create } from '@figuro/chatwoot-sdk';
import type { generic_id } from '@figuro/chatwoot-sdk/dist/models/generic_id';
import type { message } from '@figuro/chatwoot-sdk/dist/models/message';
import { AppConsumer } from '@waha/apps/app_sdk/AppConsumer';
import { ILogger } from '@waha/apps/app_sdk/ILogger';
import { JobLoggerWrapper } from '@waha/apps/app_sdk/JobLoggerWrapper';
import { HasBeenRetried } from '@waha/apps/app_sdk/JobUtils';
import { ContactConversationService } from '@waha/apps/chatwoot/client/ContactConversationService';
import { Conversation } from '@waha/apps/chatwoot/client/Conversation';
import { MessageType, SendAttachment } from '@waha/apps/chatwoot/client/types';
import { WhatsAppChatIdKey } from '@waha/apps/chatwoot/consumers/mutex';
import { EventData } from '@waha/apps/chatwoot/consumers/types';
import { WhatsAppContactInfo } from '@waha/apps/chatwoot/contacts/WhatsAppContactInfo';
import { DIContainer } from '@waha/apps/chatwoot/di/DIContainer';
import { Locale, TKey } from '@waha/apps/chatwoot/locale';
import { WAHASelf, WAHASessionAPI } from '@waha/apps/chatwoot/session/WAHASelf';
import {
AppRepository,
ChatwootMessage,
MessageMappingService,
WhatsAppMessage,
} from '@waha/apps/chatwoot/storage';
import { SessionManager } from '@waha/core/abc/manager.abc';
import { toCusFormat } from '@waha/core/engines/noweb/session.noweb.core';
import { parseMessageIdSerialized } from '@waha/core/utils/ids';
import { RMutexService } from '@waha/modules/rmutex/rmutex.service';
import { WAHAEvents } from '@waha/structures/enums.dto';
import { MessageSource, WAMessageBase } from '@waha/structures/responses.dto';
import {
WAHAWebhook,
WAHAWebhookMessageRevoked,
} from '@waha/structures/webhooks.dto';
import { sleep } from '@waha/utils/promiseTimeout';
import { Job } from 'bullmq';
import { PinoLogger } from 'nestjs-pino';
import { ChatWootAppConfig } from '../../dto/config.dto';
export function ListenEventsForChatWoot() {
return [
WAHAEvents.MESSAGE_ANY,
WAHAEvents.MESSAGE_REACTION,
WAHAEvents.MESSAGE_EDITED,
WAHAEvents.MESSAGE_REVOKED,
WAHAEvents.SESSION_STATUS,
];
}
/**
* Base class for ChatWoot WAHA consumers
* Contains common logic for all WAHA consumers
*/
export abstract class ChatWootWAHABaseConsumer extends AppConsumer {
protected appRepository: AppRepository;
constructor(
protected readonly manager: SessionManager,
log: PinoLogger,
rmutex: RMutexService,
protected readonly consumerName: string,
) {
super('ChatWoot', consumerName, log, rmutex);
}
protected ErrorHeaderKey(): TKey | null {
return TKey.WHATSAPP_ERROR_RECEIVING_MESSAGE;
}
private async ProcessAndReportErrors(job) {
const errorReportInfo = new MessageReportInfo();
try {
const result = await this.Process(job, errorReportInfo);
await this.ReportErrorRecovered(errorReportInfo, job);
return result;
} catch (err) {
await this.ReportErrorForMessage(errorReportInfo, job, err);
throw err;
}
}
/**
* Gets the DIContainer for the specified app.
*/
protected async DIContainer(job: Job, appId: string): Promise<DIContainer> {
const knex = this.manager.store.getWAHADatabase();
this.appRepository = new AppRepository(knex);
const logger = new JobLoggerWrapper(job, this.logger);
const app = await this.appRepository.getById(appId);
return new DIContainer(app.pk, app.config, logger, knex);
}
abstract GetChatId(event: any): string;
abstract Process(
job: Job<EventData, any, WAHAEvents>,
messageInfo: IMessageInfo,
): Promise<any>;
async processJob(job: Job<EventData, any, WAHAEvents>): Promise<any> {
const event: WAHAWebhookMessageRevoked = job.data.event as any;
const key = WhatsAppChatIdKey(job.data.app, this.GetChatId(event));
return await this.withMutex(job, key, () =>
this.ProcessAndReportErrors(job),
);
}
protected async ReportErrorForMessage(
info: MessageReportInfo,
job: Job,
err: any,
) {
const container = await this.DIContainer(job, job.data.app);
let conversation: Conversation;
if (info.conversationId) {
conversation = container
.ContactConversationService()
.ConversationById(info.conversationId);
} else {
conversation = await container
.ContactConversationService()
.InboxNotifications();
}
const header: string = this.ErrorHeaderKey()
? container.Locale().key(this.ErrorHeaderKey()).render()
: err.message || `${err}`;
const reporter = container.ChatWootErrorReporter(job);
await reporter.ReportError(
conversation,
header,
info.type || MessageType.INCOMING,
err,
);
throw err;
}
/**
* Report a job as recovered after retries
* This method will only send a report if the job has been retried (not on its first attempt)
*/
protected async ReportErrorRecovered(info: MessageReportInfo, job: Job) {
if (!HasBeenRetried(job)) {
return;
}
const container = await this.DIContainer(job, job.data.app);
let conversation: Conversation;
if (info.conversationId) {
conversation = container
.ContactConversationService()
.ConversationById(info.conversationId);
} else {
const conversation = await container
.ContactConversationService()
.InboxNotifications();
}
const reporter = container.ChatWootErrorReporter(job);
await reporter.ReportSucceeded(
conversation,
info.type || MessageType.INCOMING,
);
}
}
export interface IMessageInfo {
onConversationId(id: number): void;
onMessageType(type: MessageType): void;
}
class MessageReportInfo implements IMessageInfo {
public conversationId: number | null = null;
public type: MessageType | null = null;
onConversationId(id: number) {
this.conversationId = id;
}
onMessageType(type: MessageType) {
this.type = type;
}
}
export abstract class MessageBaseHandler<Payload extends WAMessageBase> {
constructor(
protected mappingService: MessageMappingService,
protected repo: ContactConversationService,
protected logger: ILogger,
protected info: IMessageInfo,
protected session: WAHASessionAPI,
protected l: Locale,
protected waha: WAHASelf,
) {}
abstract getContent(payload: Payload): string;
abstract getReplyToWhatsAppID(payload: Payload): string | undefined;
abstract getAttachments(payload: Payload): Promise<SendAttachment[]>;
async handle(data: WAHAWebhook<Payload>) {
const payload = data.payload;
// Find the type as soon as possible for error reporting
const type = payload.fromMe ? MessageType.OUTGOING : MessageType.INCOMING;
this.info.onMessageType(type);
// Check if we have that message already in ChatWoot to avoid duplication
// It also handles messages fromMe and ChatWoot (but does not if send via API)
if (payload.fromMe && payload.source === MessageSource.API) {
// Sleep a few seconds to save it to a database
await sleep(3_000);
}
const key = parseMessageIdSerialized(payload.id);
const chatwoot = await this.mappingService.getChatWootMessage({
chat_id: toCusFormat(key.remoteJid),
message_id: key.id,
});
if (chatwoot) {
this.logger.info(
`Message '${payload.id}' already in ChatWoot: conversation.id=${chatwoot.conversation_id}, message.id=${chatwoot.message_id}`,
);
return;
}
const contactInfo = WhatsAppContactInfo(this.session, payload.from, this.l);
const conversation = await this.repo.ConversationByContact(contactInfo);
this.info.onConversationId(conversation.conversationId);
const message = await this.buildChatWootMessage(payload);
const response = await conversation.send(message);
this.logger.info(
`Created message as '${message.message_type}' from WhatsApp: ${response.id}`,
);
await this.saveMapping(response, payload);
return message;
}
private async saveMapping(
chatwootMessage: generic_id & message,
whatsappMessage: WAMessageBase,
) {
const chatwoot: Omit<ChatwootMessage, 'id'> = {
timestamp: new Date(chatwootMessage.created_at * 1000),
conversation_id: chatwootMessage.conversation_id,
message_id: chatwootMessage.id,
};
const key = parseMessageIdSerialized(whatsappMessage.id);
const whatsapp: WhatsAppMessage = {
timestamp: new Date(whatsappMessage.timestamp * 1000),
chat_id: toCusFormat(key.remoteJid),
message_id: key.id,
from_me: key.fromMe,
participant: null,
};
await this.mappingService.map(chatwoot, whatsapp, 1);
return;
}
private async buildChatWootMessage(
payload: Payload,
): Promise<conversation_message_create> {
let content: string = this.getContent(payload);
// Format the content if the message from me
if (payload.fromMe && payload.source === MessageSource.APP) {
const key = TKey.MESSAGE_FROM_WHATSAPP;
content = this.l.key(key).render({ text: content });
} else if (payload.fromMe && payload.source === MessageSource.API) {
const key = TKey.MESSAGE_FROM_API;
content = this.l.key(key).render({ text: content });
}
const chatId = payload.from;
// Add participant name to group messages
const manyParticipants = isJidGroup(chatId) || isJidStatusBroadcast(chatId);
if (!payload.fromMe && manyParticipants) {
const key = parseMessageIdSerialized(payload.id, true);
let participant = toCusFormat(key.participant);
const contact: any = await this.session.getContact(key.participant);
const name = contact?.name || contact?.pushName || contact?.pushname;
if (name) {
participant = `${name} (${participant})`;
}
content = this.l.key(TKey.WHATSAPP_GROUP_MESSAGE).render({
text: content,
participant: participant,
});
}
const attachments: SendAttachment[] = await this.getAttachments(payload);
if (!content && attachments.length > 0) {
// With no content ChatWoot render the message as an ugly big attachment
content = ' ';
}
const replyTo = await this.getReplyToChatWootMessageID(payload).catch(
(err) => {
this.logger.error(`Error getting reply to message ID: ${err}`);
return undefined;
},
);
const type = payload.fromMe ? MessageType.OUTGOING : MessageType.INCOMING;
return {
content: content,
message_type: type,
private: payload.fromMe,
attachments: attachments as any,
content_attributes: {
in_reply_to: replyTo,
},
};
}
async getReplyToChatWootMessageID(
payload: Payload,
): Promise<number | undefined> {
const replyToWhatsAppID = this.getReplyToWhatsAppID(payload);
if (!replyToWhatsAppID) {
return;
}
const chatwoot = await this.mappingService.getChatWootMessage({
chat_id: payload.from,
message_id: replyToWhatsAppID,
});
return chatwoot.message_id;
}
}
@@ -0,0 +1,100 @@
import { Processor } from '@nestjs/bullmq';
import { JOB_CONCURRENCY } from '@waha/apps/app_sdk/constants';
import { SendAttachment } from '@waha/apps/chatwoot/client/types';
import { QueueName } from '@waha/apps/chatwoot/consumers/QueueName';
import { EventData } from '@waha/apps/chatwoot/consumers/types';
import {
ChatWootWAHABaseConsumer,
IMessageInfo,
} from '@waha/apps/chatwoot/consumers/waha/base';
import { MessageBaseHandler } from '@waha/apps/chatwoot/consumers/waha/base';
import { WAHASessionAPI } from '@waha/apps/chatwoot/session/WAHASelf';
import { WhatsappToMarkdown } from '@waha/apps/chatwoot/text';
import { SessionManager } from '@waha/core/abc/manager.abc';
import { parseMessageIdSerialized } from '@waha/core/utils/ids';
import { RMutexService } from '@waha/modules/rmutex/rmutex.service';
import { WAHAEvents } from '@waha/structures/enums.dto';
import { WAMessage } from '@waha/structures/responses.dto';
import { WAHAWebhookMessageAny } from '@waha/structures/webhooks.dto';
import { Job } from 'bullmq';
import { PinoLogger } from 'nestjs-pino';
// eslint-disable-next-line @typescript-eslint/no-var-requires
const mime = require('mime-types');
@Processor(QueueName.WAHA_MESSAGE_ANY, { concurrency: JOB_CONCURRENCY })
export class WAHAMessageAnyConsumer extends ChatWootWAHABaseConsumer {
constructor(
protected readonly manager: SessionManager,
log: PinoLogger,
rmutex: RMutexService,
) {
super(manager, log, rmutex, 'WAHAMessageAnyConsumer');
}
GetChatId(event: WAHAWebhookMessageAny): string {
return event.payload.from;
}
async Process(
job: Job<EventData, any, WAHAEvents>,
info: IMessageInfo,
): Promise<any> {
const container = await this.DIContainer(job, job.data.app);
const event: WAHAWebhookMessageAny = job.data.event as any;
const session = new WAHASessionAPI(event.session, container.WAHASelf());
const handler = new MessageAnyHandler(
container.MessageMappingService(),
container.ContactConversationService(),
container.Logger(),
info,
session,
container.Locale(),
container.WAHASelf(),
);
return await handler.handle(event);
}
}
class MessageAnyHandler extends MessageBaseHandler<WAMessage> {
getContent(payload: WAMessage): string {
const body = payload.body;
return WhatsappToMarkdown(body);
}
getReplyToWhatsAppID(payload: WAMessage): string {
const replyTo = payload.replyTo;
if (!replyTo) {
return undefined;
}
const key = parseMessageIdSerialized(replyTo.id, true);
return key.id;
}
async getAttachments(payload: WAMessage): Promise<SendAttachment[]> {
const hasMedia = payload.media?.url;
if (!hasMedia) {
return [];
}
const attachments: SendAttachment[] = [];
const media = payload.media;
this.logger.info(`Downloading media from '${media.url}'...`);
const buffer = await this.waha.fetch(media.url);
const fileContent = buffer.toString('base64');
let filename = media.filename;
if (!filename) {
const extension = mime.extension(media.mimetype);
filename = `no-filename.${extension}`;
}
const attachment: SendAttachment = {
content: fileContent,
filename: filename,
encoding: 'base64',
};
attachments.push(attachment);
this.logger.info(`Downloaded media from '${media.url}' as '${filename}'`);
return attachments;
}
}
@@ -0,0 +1,77 @@
import { Processor } from '@nestjs/bullmq';
import { JOB_CONCURRENCY } from '@waha/apps/app_sdk/constants';
import { QueueName } from '@waha/apps/chatwoot/consumers/QueueName';
import { EventData } from '@waha/apps/chatwoot/consumers/types';
import {
ChatWootWAHABaseConsumer,
IMessageInfo,
} from '@waha/apps/chatwoot/consumers/waha/base';
import { MessageBaseHandler } from '@waha/apps/chatwoot/consumers/waha/base';
import { TKey } from '@waha/apps/chatwoot/locale';
import { WhatsappToMarkdown } from '@waha/apps/chatwoot/text';
import { SessionManager } from '@waha/core/abc/manager.abc';
import { RMutexService } from '@waha/modules/rmutex/rmutex.service';
import { WAHAEvents } from '@waha/structures/enums.dto';
import {
WAHAWebhookMessageEdited,
WAMessageEditedBody,
} from '@waha/structures/webhooks.dto';
import { Job } from 'bullmq';
import { PinoLogger } from 'nestjs-pino';
import { SendAttachment } from '../../client/types';
import { WAHASessionAPI } from '../../session/WAHASelf';
@Processor(QueueName.WAHA_MESSAGE_EDITED, { concurrency: JOB_CONCURRENCY })
export class WAHAMessageEditedConsumer extends ChatWootWAHABaseConsumer {
constructor(
protected readonly manager: SessionManager,
log: PinoLogger,
rmutex: RMutexService,
) {
super(manager, log, rmutex, 'WAHAMessageEditedConsumer');
}
GetChatId(event: WAHAWebhookMessageEdited): string {
return event.payload.from;
}
async Process(
job: Job<EventData, any, WAHAEvents>,
info: IMessageInfo,
): Promise<any> {
const container = await this.DIContainer(job, job.data.app);
const event: WAHAWebhookMessageEdited = job.data.event as any;
const session = new WAHASessionAPI(event.session, container.WAHASelf());
const handler = new MessageEditedHandler(
container.MessageMappingService(),
container.ContactConversationService(),
container.Logger(),
info,
session,
container.Locale(),
container.WAHASelf(),
);
return await handler.handle(event);
}
}
class MessageEditedHandler extends MessageBaseHandler<WAMessageEditedBody> {
getContent(payload: WAMessageEditedBody): string {
const body = payload.body;
const formatted = WhatsappToMarkdown(body);
return this.l
.key(TKey.MESSAGE_EDITED_IN_WHATSAPP)
.render({ text: formatted });
}
getReplyToWhatsAppID(payload: WAMessageEditedBody): string {
return payload.editedMessageId;
}
async getAttachments(
payload: WAMessageEditedBody,
): Promise<SendAttachment[]> {
return [];
}
}
@@ -0,0 +1,78 @@
import { Processor } from '@nestjs/bullmq';
import { JOB_CONCURRENCY } from '@waha/apps/app_sdk/constants';
import { SendAttachment } from '@waha/apps/chatwoot/client/types';
import { QueueName } from '@waha/apps/chatwoot/consumers/QueueName';
import { EventData } from '@waha/apps/chatwoot/consumers/types';
import {
ChatWootWAHABaseConsumer,
IMessageInfo,
} from '@waha/apps/chatwoot/consumers/waha/base';
import { MessageBaseHandler } from '@waha/apps/chatwoot/consumers/waha/base';
import { TKey } from '@waha/apps/chatwoot/locale';
import { WAHASessionAPI } from '@waha/apps/chatwoot/session/WAHASelf';
import { SessionManager } from '@waha/core/abc/manager.abc';
import { parseMessageIdSerialized } from '@waha/core/utils/ids';
import { RMutexService } from '@waha/modules/rmutex/rmutex.service';
import { WAHAEvents } from '@waha/structures/enums.dto';
import { WAMessageReaction, WAReaction } from '@waha/structures/responses.dto';
import { WAHAWebhookMessageReaction } from '@waha/structures/webhooks.dto';
import { Job } from 'bullmq';
import { PinoLogger } from 'nestjs-pino';
@Processor(QueueName.WAHA_MESSAGE_REACTION, { concurrency: JOB_CONCURRENCY })
export class WAHAMessageReactionConsumer extends ChatWootWAHABaseConsumer {
constructor(
protected readonly manager: SessionManager,
log: PinoLogger,
rmutex: RMutexService,
) {
super(manager, log, rmutex, 'WAHAMessageReactionConsumer');
}
GetChatId(event: WAHAWebhookMessageReaction): string {
return event.payload.from;
}
async Process(
job: Job<EventData, any, WAHAEvents>,
info: IMessageInfo,
): Promise<any> {
const container = await this.DIContainer(job, job.data.app);
const event: WAHAWebhookMessageReaction = job.data.event as any;
const session = new WAHASessionAPI(event.session, container.WAHASelf());
const handler = new MessageReactionHandler(
container.MessageMappingService(),
container.ContactConversationService(),
container.Logger(),
info,
session,
container.Locale(),
container.WAHASelf(),
);
return await handler.handle(event);
}
}
export class MessageReactionHandler extends MessageBaseHandler<WAMessageReaction> {
getContent(payload: WAMessageReaction) {
const reaction = payload.reaction as WAReaction;
const emoji = reaction.text;
if (!emoji) {
return this.l.key(TKey.WHATSAPP_REACTION_REMOVED).render();
}
return this.l.key(TKey.WHATSAPP_REACTION_ADDED).render({
emoji: emoji,
});
}
getReplyToWhatsAppID(payload: WAMessageReaction) {
const reaction = payload.reaction as WAReaction;
const messageId = reaction.messageId;
const key = parseMessageIdSerialized(messageId, false);
return key.id;
}
async getAttachments(payload: WAMessageReaction): Promise<SendAttachment[]> {
return [];
}
}
@@ -0,0 +1,100 @@
import { conversation_message_create } from '@figuro/chatwoot-sdk';
import { Processor } from '@nestjs/bullmq';
import { JOB_CONCURRENCY } from '@waha/apps/app_sdk/constants';
import { ILogger } from '@waha/apps/app_sdk/ILogger';
import { ContactConversationService } from '@waha/apps/chatwoot/client/ContactConversationService';
import { MessageType } from '@waha/apps/chatwoot/client/types';
import { QueueName } from '@waha/apps/chatwoot/consumers/QueueName';
import { EventData } from '@waha/apps/chatwoot/consumers/types';
import {
ChatWootWAHABaseConsumer,
IMessageInfo,
} from '@waha/apps/chatwoot/consumers/waha/base';
import { Locale, TKey } from '@waha/apps/chatwoot/locale';
import {
ChatwootMessage,
MessageMappingService,
} from '@waha/apps/chatwoot/storage';
import { SessionManager } from '@waha/core/abc/manager.abc';
import { toCusFormat } from '@waha/core/engines/noweb/session.noweb.core';
import { RMutexService } from '@waha/modules/rmutex/rmutex.service';
import { WAHAEvents } from '@waha/structures/enums.dto';
import { WAHAWebhookMessageRevoked } from '@waha/structures/webhooks.dto';
import { Job } from 'bullmq';
import { PinoLogger } from 'nestjs-pino';
@Processor(QueueName.WAHA_MESSAGE_REVOKED, { concurrency: JOB_CONCURRENCY })
export class WAHAMessageRevokedConsumer extends ChatWootWAHABaseConsumer {
constructor(
protected readonly manager: SessionManager,
log: PinoLogger,
rmutex: RMutexService,
) {
super(manager, log, rmutex, 'WAHAMessageRevokedConsumer');
}
GetChatId(event: WAHAWebhookMessageRevoked): string {
return event.payload.after.from;
}
async Process(
job: Job<EventData, any, WAHAEvents>,
info: IMessageInfo,
): Promise<any> {
const container = await this.DIContainer(job, job.data.app);
const event: WAHAWebhookMessageRevoked = job.data.event as any;
const handler = new MessageRevokedHandler(
container.MessageMappingService(),
container.ContactConversationService(),
container.Logger(),
container.Locale(),
info,
);
return await handler.handle(event);
}
}
export class MessageRevokedHandler {
constructor(
private mappingService: MessageMappingService,
private repo: ContactConversationService,
private logger: ILogger,
private l: Locale,
private info: IMessageInfo,
) {}
async handle(event: WAHAWebhookMessageRevoked) {
const payload = event.payload;
const messageId = event.payload.revokedMessageId;
const chatId = payload.after.from;
const chatwoot = await this.mappingService.getChatWootMessage({
chat_id: toCusFormat(chatId),
message_id: messageId,
});
if (!chatwoot) {
this.logger.warn('Message not found in mapping service');
return;
}
const conversation = this.repo.ConversationById(chatwoot.conversation_id);
const message = this.buildChatWootMessage(chatwoot, payload.after.fromMe);
this.info.onConversationId(chatwoot.conversation_id);
this.info.onMessageType(message.message_type as MessageType);
await conversation.send(message);
}
private buildChatWootMessage(
chatwoot: ChatwootMessage,
fromMe: boolean,
): conversation_message_create {
const content = this.l.key(TKey.MESSAGE_REMOVED_IN_WHATSAPP).render();
const type = fromMe ? MessageType.OUTGOING : MessageType.INCOMING;
return {
content: content,
message_type: type,
private: true,
content_attributes: {
in_reply_to: chatwoot.message_id,
},
};
}
}
@@ -0,0 +1,106 @@
import type { conversation_message_create } from '@figuro/chatwoot-sdk/dist/models/conversation_message_create';
import { Processor } from '@nestjs/bullmq';
import { JOB_CONCURRENCY } from '@waha/apps/app_sdk/constants';
import { ContactConversationService } from '@waha/apps/chatwoot/client/ContactConversationService';
import { AttachmentFromBuffer } from '@waha/apps/chatwoot/client/messages';
import { MessageType } from '@waha/apps/chatwoot/client/types';
import { QueueName } from '@waha/apps/chatwoot/consumers/QueueName';
import { EventData } from '@waha/apps/chatwoot/consumers/types';
import {
ChatWootWAHABaseConsumer,
IMessageInfo,
} from '@waha/apps/chatwoot/consumers/waha/base';
import { Locale, TKey } from '@waha/apps/chatwoot/locale';
import { WAHASelf } from '@waha/apps/chatwoot/session/WAHASelf';
import { SessionStatusEmoji } from '@waha/apps/chatwoot/SessionStatusEmoji';
import { SessionManager } from '@waha/core/abc/manager.abc';
import { RMutexService } from '@waha/modules/rmutex/rmutex.service';
import { WAHAEvents, WAHASessionStatus } from '@waha/structures/enums.dto';
import { WAHAWebhookSessionStatus } from '@waha/structures/webhooks.dto';
import { Job } from 'bullmq';
import { PinoLogger } from 'nestjs-pino';
@Processor(QueueName.WAHA_SESSION_STATUS, { concurrency: JOB_CONCURRENCY })
export class WAHASessionStatusConsumer extends ChatWootWAHABaseConsumer {
constructor(
protected readonly manager: SessionManager,
log: PinoLogger,
rmutex: RMutexService,
) {
super(manager, log, rmutex, 'WAHASessionStatusConsumer');
}
GetChatId(event: any): string {
return 'session.status';
}
async Process(
job: Job<EventData, any, WAHAEvents>,
info: IMessageInfo,
): Promise<any> {
const container = await this.DIContainer(job, job.data.app);
const handler = new SessionStatusHandler(
container.ContactConversationService(),
container.Locale(),
container.WAHASelf(),
);
return await handler.handle(job.data.event as any);
}
}
export class SessionStatusHandler {
constructor(
private repo: ContactConversationService,
private l: Locale,
private waha: WAHASelf,
) {}
async handle(data: WAHAWebhookSessionStatus) {
const conversation = await this.repo.InboxNotifications();
// Report session status
const emoji = SessionStatusEmoji(data.payload.status);
const activity = this.l.key(TKey.APP_SESSION_STATUS_CHANGE).r({
emoji: emoji,
session: data.session,
status: data.payload.status,
});
await conversation.activity(activity);
const payload = data.payload;
let text = '';
switch (payload.status) {
case WAHASessionStatus.STARTING:
break;
case WAHASessionStatus.WORKING:
text = this.l.key(TKey.APP_SESSION_STATUS_WORKING).r({
name: data.me?.pushName || 'Unknown',
id: data.me?.id || 'No phone number',
});
break;
case WAHASessionStatus.STOPPED:
case WAHASessionStatus.FAILED:
text = this.l.key(TKey.APP_SESSION_STATUS_ERROR).r();
text += '\n\n';
text += this.l.key(TKey.APP_HELP_REMINDER).r();
text += '\n\n';
break;
case WAHASessionStatus.SCAN_QR_CODE:
text = this.l.key(TKey.APP_SESSION_SCAN_QR_CODE).r();
}
if (text) {
const message: conversation_message_create = {
content: text,
message_type: MessageType.INCOMING as any,
};
await conversation.send(message);
}
// Send QR to the inbox
if (payload.status === WAHASessionStatus.SCAN_QR_CODE) {
const buffer = await this.waha.qr(data.session);
const message = AttachmentFromBuffer(buffer, 'qr.jpg');
message.message_type = MessageType.INCOMING;
return await conversation.send(message);
}
}
}
@@ -0,0 +1,34 @@
import { public_contact_create_update_payload } from '@figuro/chatwoot-sdk';
import { ContactInfo } from '@waha/apps/chatwoot/client/ContactConversationService';
import { AttributeKey, INBOX_CONTACT_CHAT_ID } from '@waha/apps/chatwoot/const';
import { Locale, TKey } from '@waha/apps/chatwoot/locale';
/**
* Inbox Notifications Contact
*/
export class InboxContactInfo implements ContactInfo {
constructor(private l: Locale) {}
ChatId(): string {
return INBOX_CONTACT_CHAT_ID;
}
async AvatarUrl() {
return this.l.key(TKey.APP_INBOX_CONTACT_AVATAR_URL).render();
}
async Attributes(): Promise<any> {
return {
[AttributeKey.WA_CHAT_ID]: INBOX_CONTACT_CHAT_ID,
};
}
async PublicContactCreate(): Promise<public_contact_create_update_payload> {
return {
identifier: INBOX_CONTACT_CHAT_ID,
name: this.l.key(TKey.APP_INBOX_CONTACT_NAME).render(),
avatar_url: await this.AvatarUrl(),
custom_attributes: await this.Attributes(),
};
}
}
@@ -0,0 +1,242 @@
import {
isJidGroup,
isJidNewsletter,
isJidStatusBroadcast,
} from '@adiwajshing/baileys';
import { isLidUser } from '@adiwajshing/baileys/lib/WABinary/jid-utils';
import { public_contact_create_update_payload as Contact } from '@figuro/chatwoot-sdk';
import { ContactInfo } from '@waha/apps/chatwoot/client/ContactConversationService';
import { AttributeKey } from '@waha/apps/chatwoot/const';
import { Locale, TKey } from '@waha/apps/chatwoot/locale';
import { WAHASessionAPI } from '@waha/apps/chatwoot/session/WAHASelf';
import { Channel } from '@waha/structures/channels.dto';
import { CacheAsync } from '@waha/utils/Cache';
/**
* Base WhatsApp contact info class
*/
abstract class ChatContactInfo implements ContactInfo {
constructor(
protected session: WAHASessionAPI,
protected chatId: string,
protected locale: Locale,
) {}
ChatId(): string {
return this.chatId;
}
abstract AvatarUrl(): Promise<string | null>;
abstract Attributes(): Promise<any>;
abstract PublicContactCreate(): Promise<Contact>;
}
/**
* Regular JID contact info
*/
class JidContactInfo extends ChatContactInfo {
@CacheAsync()
async AvatarUrl(): Promise<string> {
return await this.session.getChatPicture(this.chatId);
}
@CacheAsync()
async fetchLid() {
return await this.session.findLIDByPN(this.chatId);
}
@CacheAsync()
async Attributes() {
const attributes = {
[AttributeKey.WA_CHAT_ID]: this.chatId,
[AttributeKey.WA_JID]: this.chatId,
};
const lid = await this.fetchLid();
if (lid) {
attributes[AttributeKey.WA_LID] = lid;
}
return attributes;
}
async PublicContactCreate(): Promise<Contact> {
const contact: any = await this.session.getContact(this.chatId);
const name =
contact?.name || contact?.pushName || contact?.pushname || this.chatId;
const phoneNumber = this.chatId.split('@')[0];
const phoneNumberE164 = '+' + phoneNumber;
const result: Contact = {
name: name,
custom_attributes: {
[AttributeKey.WA_CHAT_ID]: this.chatId,
[AttributeKey.WA_JID]: this.chatId,
},
phone_number: phoneNumberE164,
};
result.custom_attributes = await this.Attributes();
return result;
}
}
/**
* LID contact info
*/
class LidContactInfo extends ChatContactInfo {
@CacheAsync()
async jid() {
const pn = await this.session.findPNByLid(this.chatId);
return new JidContactInfo(this.session, pn, this.locale);
}
async AvatarUrl(): Promise<string | null> {
const jid = await this.jid();
if (jid) {
return await jid.AvatarUrl();
}
return null;
}
@CacheAsync()
async Attributes() {
const jid = await this.jid();
let attributes = {};
if (jid) {
attributes = await jid.Attributes();
}
attributes[AttributeKey.WA_CHAT_ID] = this.chatId;
attributes[AttributeKey.WA_LID] = this.chatId;
return attributes;
}
async PublicContactCreate(): Promise<Contact> {
const jid = await this.jid();
let result;
if (jid) {
result = await jid.PublicContactCreate();
} else {
result = {
inbox_id: 0,
identifier: this.chatId,
name: this.chatId,
};
}
result.custom_attributes = await this.Attributes();
return result;
}
}
/**
* Group contact info
*/
class GroupContactInfo extends ChatContactInfo {
@CacheAsync()
async AvatarUrl(): Promise<string> {
return await this.session.getChatPicture(this.chatId);
}
async Attributes() {
return {
[AttributeKey.WA_CHAT_ID]: this.chatId,
};
}
async PublicContactCreate(): Promise<Contact> {
let name = this.chatId;
const group: any = await this.session?.getGroup(this.chatId);
if (group) {
name = group.subject || group.name || group.topic || name;
const suffix = this.locale
.key(TKey.WHATSAPP_CONTACT_GROUP_SUFFIX)
.render();
name = `${name} (${suffix})`;
}
return {
identifier: this.chatId,
name: name,
custom_attributes: await this.Attributes(),
};
}
}
/**
* Channel (newsletter) contact info
*/
class ChannelContactInfo extends ChatContactInfo {
@CacheAsync()
async AvatarUrl(): Promise<string> {
return await this.session.getChatPicture(this.chatId);
}
async Attributes() {
return {
[AttributeKey.WA_CHAT_ID]: this.chatId,
};
}
async PublicContactCreate(): Promise<Contact> {
let name = this.chatId;
const channel: Channel = await this.session.getChannel(this.chatId);
if (channel) {
name = channel.name || name;
const suffix = this.locale
.key(TKey.WHATSAPP_CONTACT_CHANNEL_SUFFIX)
.render();
name = `${name} (${suffix})`;
}
return {
identifier: this.chatId,
name: name,
custom_attributes: await this.Attributes(),
};
}
}
/**
* Status broadcast contact info
*/
class StatusContactInfo extends ChatContactInfo {
@CacheAsync()
async AvatarUrl(): Promise<string | null> {
return null;
}
async Attributes() {
return {
[AttributeKey.WA_CHAT_ID]: this.chatId,
};
}
async PublicContactCreate(): Promise<Contact> {
const name = this.locale.key(TKey.WHATSAPP_CONTACT_STATUS_NAME).render();
return {
identifier: this.chatId,
name: `🟢 ${name}`,
custom_attributes: await this.Attributes(),
};
}
}
/**
* Factory function to get the appropriate ContactInfo implementation
* based on the chat ID type
*/
export function WhatsAppContactInfo(
session: WAHASessionAPI,
chatId: string,
locale: Locale,
): ContactInfo {
if (isJidGroup(chatId)) {
return new GroupContactInfo(session, chatId, locale);
} else if (isJidNewsletter(chatId)) {
return new ChannelContactInfo(session, chatId, locale);
} else if (isJidStatusBroadcast(chatId)) {
return new StatusContactInfo(session, chatId, locale);
} else if (isLidUser(chatId)) {
return new LidContactInfo(session, chatId, locale);
}
return new JidContactInfo(session, chatId, locale);
}
+201
View File
@@ -0,0 +1,201 @@
import ChatwootClient from '@figuro/chatwoot-sdk';
import { AxiosLogging } from '@waha/apps/app_sdk/AxiosLogging';
import { ILogger } from '@waha/apps/app_sdk/ILogger';
import { ContactAPI } from '@waha/apps/chatwoot/client/ContactAPI';
import { ContactConversationService } from '@waha/apps/chatwoot/client/ContactConversationService';
import { ConversationAPI } from '@waha/apps/chatwoot/client/ConversationAPI';
import { CustomAttributesAPI } from '@waha/apps/chatwoot/client/CustomAttributesAPI';
import { ChatWootInboxAPI } from '@waha/apps/chatwoot/client/interfaces';
import { ChatWootAppConfig } from '@waha/apps/chatwoot/dto/config.dto';
import { ChatWootErrorReporter } from '@waha/apps/chatwoot/error/ChatWootErrorReporter';
import { Locale } from '@waha/apps/chatwoot/locale';
import { WAHASelf } from '@waha/apps/chatwoot/session/WAHASelf';
import {
ChatwootMessageRepository,
MessageMappingRepository,
MessageMappingService,
WhatsAppMessageRepository,
} from '@waha/apps/chatwoot/storage';
import { Job } from 'bullmq';
import { Knex } from 'knex';
/**
* Dependency Injection Container for ChatWoot
* Manages the creation and caching of various clients and repositories
*/
export class DIContainer {
private accountAPI: ChatwootClient;
private inboxAPI: ChatWootInboxAPI;
private contactAPI: ContactAPI;
private conversationAPI: ConversationAPI;
private contactConversationService: ContactConversationService;
private locale: Locale;
private messageMappingService: MessageMappingService;
private wahaSelf: WAHASelf;
/**
* Creates a new DIContainer with the given configuration
*/
constructor(
private readonly appPk: number,
private readonly config: ChatWootAppConfig,
private readonly logger: ILogger,
private readonly knex: Knex,
) {}
public Logger(): ILogger {
return this.logger;
}
private AppPk() {
if (!this.appPk) {
throw new Error('AppPk not set or 0');
}
return this.appPk;
}
private Knex() {
if (!this.knex) {
throw new Error('Knex not set');
}
return this.knex;
}
public Locale(): Locale {
if (!this.locale) {
this.locale = new Locale(this.config.locale);
}
return this.locale;
}
/**
* Gets the AccountAPI client
* @returns ChatwootClient instance
*/
public AccountAPI(): ChatwootClient {
if (!this.accountAPI) {
this.accountAPI = new ChatwootClient({
config: {
basePath: this.config.url,
with_credentials: true,
credentials: 'include',
token: this.config.accountToken,
},
});
}
return this.accountAPI;
}
/**
* Gets the InboxAPI client
* @returns ChatWootInboxAPI instance
*/
public InboxAPI(): ChatWootInboxAPI {
if (!this.inboxAPI) {
const chatwootClientAPI = new ChatwootClient({
config: {
basePath: this.config.url,
with_credentials: true,
credentials: 'include',
token: this.config.inboxIdentifier,
},
});
this.inboxAPI = chatwootClientAPI.client as ChatWootInboxAPI;
}
return this.inboxAPI;
}
/**
* Gets the ContactAPI
* @returns ContactAPI instance
*/
private ContactAPI(): ContactAPI {
if (!this.contactAPI) {
this.contactAPI = new ContactAPI(
this.config,
this.AccountAPI(),
this.InboxAPI(),
this.logger,
);
}
return this.contactAPI;
}
/**
* Gets the ConversationAPI
* @returns ConversationAPI instance
*/
private ConversationAPI(): ConversationAPI {
if (!this.conversationAPI) {
this.conversationAPI = new ConversationAPI(
this.config,
this.InboxAPI(),
this.logger,
);
}
return this.conversationAPI;
}
/**
* Gets the ContactConversationService
* @returns ContactConversationService instance
*/
public ContactConversationService(): ContactConversationService {
if (!this.contactConversationService) {
this.contactConversationService = new ContactConversationService(
this.config,
this.ContactAPI(),
this.ConversationAPI(),
this.AccountAPI(),
this.logger,
this.Locale(),
);
}
return this.contactConversationService;
}
private ChatwootMessageRepository(): ChatwootMessageRepository {
return new ChatwootMessageRepository(this.Knex(), this.AppPk());
}
private WhatsAppMessageRepository(): WhatsAppMessageRepository {
return new WhatsAppMessageRepository(this.Knex(), this.AppPk());
}
private MessageMappingRepository(): MessageMappingRepository {
return new MessageMappingRepository(this.Knex(), this.AppPk());
}
public MessageMappingService(): MessageMappingService {
if (!this.messageMappingService) {
this.messageMappingService = new MessageMappingService(
this.Knex(),
this.WhatsAppMessageRepository(),
this.ChatwootMessageRepository(),
this.MessageMappingRepository(),
);
}
return this.messageMappingService;
}
public ChatWootErrorReporter(job: Job): ChatWootErrorReporter {
return new ChatWootErrorReporter(this.Logger(), job, this.Locale());
}
/**
* Gets the WAHASelf instance
* @returns WAHASelf instance
*/
public WAHASelf(): WAHASelf {
if (!this.wahaSelf) {
this.wahaSelf = new WAHASelf();
const logging = new AxiosLogging(this.Logger());
logging.applyTo(this.wahaSelf.client);
}
return this.wahaSelf;
}
public CustomAttributesAPI() {
return new CustomAttributesAPI(this.config, this.AccountAPI());
}
}
Loaded 100 of 311 files, more files were not shown because too many files have changed in this diff. Show more