Compare commits

...
128 Commits
Author SHA1 Message Date
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
91 changed files with 6717 additions and 3856 deletions

No files matched your search

+84 -168
View File
@@ -1,191 +1,107 @@
# 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
# ==================
# ===== COMMON =====
# ==================
# Base URL for the API (used for webhooks, file URLs, etc.)
WAHA_BASE_URL=http://localhost:3000
#WHATSAPP_API_SCHEMA=http
#WHATSAPP_API_PORT=3000
#WHATSAPP_API_HOSTNAME=localhost
# Server configuration (if you need to customize hostname/port)
# WHATSAPP_API_SCHEMA=http
# WHATSAPP_API_PORT=3000
# WHATSAPP_API_HOSTNAME=localhost
#
# 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=admin
# ====================
# ===== 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_USERNAME=admin
#WHATSAPP_SWAGGER_PASSWORD=admin
#
# Dashboard
#
# https://waha.devlike.pro/docs/how-to/waha-dashboard/#configuration
# Enabled by default
#WAHA_DASHBOARD_ENABLED=true
#WAHA_DASHBOARD_USERNAME=admin
#WAHA_DASHBOARD_PASSWORD=admin
#
# 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/
#
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
#
#
# PostgresSQL Configuration
# https://waha.devlike.pro/docs/how-to/storages/#sessions---postgresql
#WHATSAPP_SESSIONS_POSTGRESQL_URL=postgres://postgres:postgres@postgres:5432/postgres?sslmode=disable
#
# Media - PostgresSQL Storage
# https://waha.devlike.pro/docs/how-to/storages/#media---postgresql
#WAHA_MEDIA_STORAGE=POSTGRESQL
#WAHA_MEDIA_POSTGRESQL_URL=postgres://postgres:postgres@postgres:5432/postgres?sslmode=disable
#
# WhatsApp engine (WEBJS is default, GOWS or NOWEB for better performance)
WHATSAPP_DEFAULT_ENGINE=WEBJS
#
# 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
#
# ===================
# ===== 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
# ===========================
# ===== 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
#
# MongoDB Configuration
# https://waha.devlike.pro/docs/how-to/storages/#sessions---mongodb
#WHATSAPP_SESSIONS_MONGO_URL=mongodb://mongouser:mongopassword@mongodb:27017
#
# ==================================
# ===== ADVANCED CONFIGURATION =====
# ==================================
#
# 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
#
# 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
+5 -1
View File
@@ -126,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 -1
View File
@@ -1 +1 @@
v22.8
v22.16
+1
View File
@@ -39,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
+47 -6
View File
@@ -1,8 +1,10 @@
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
@@ -10,6 +12,10 @@ 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
@@ -23,11 +29,14 @@ RUN yarn build && find ./dist -name "*.d.ts" -delete
#
# Dashboard
#
FROM node:${NODE_VERSION} AS dashboard
FROM node:${NODE_IMAGE_TAG} AS dashboard
# 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 \
WAHA_DASHBOARD_GITHUB_REPO=$(jq -r '.waha.dashboard.repo' /tmp/waha.config.json) && \
@@ -42,7 +51,7 @@ RUN \
#
# GOWS
#
FROM golang:1.23-bullseye AS 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/*
@@ -73,7 +82,7 @@ RUN \
#
# 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
@@ -98,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 \
@@ -119,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 \
@@ -131,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="135.0.7049.114-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 \
@@ -140,12 +171,20 @@ RUN if [ "$USE_BROWSER" = "chrome" ]; then \
&& rm -rf /var/lib/apt/lists/*; \
fi
# 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
@@ -170,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"]
+8 -8
View File
@@ -1,20 +1,20 @@
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-plus-gows:
build-gows:
docker build . -t devlikeapro/waha-plus:gows --build-arg USE_BROWSER=none --build-arg WHATSAPP_DEFAULT_ENGINE=GOWS
build-all: build build-chrome build-noweb
build-plus:
docker build . -t devlikeapro/waha-plus
build-ssh:
# check IMAGE provided
@[ "${IMAGE}" ] || ( echo "Add APP: make build-ssh image=devlikeapro/waha"; exit 1 );
@@ -41,7 +41,7 @@ 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
+1 -1
View File
@@ -146,7 +146,7 @@ curl -d "{\"chatId\": \"${PHONE}@c.us\", \"text\": \"Hello from WhatsApp HTTP AP
## Start the project
1. Clone the repository
2. Make sure you're using node>=22
2. Make sure you're using node>=22 (check [.nvmrc](/.nvmrc) to get the version)
3. Run the following commands:
```bash
# Install dependencies
+27 -1
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'
@@ -32,13 +43,18 @@ services:
# POSTGRES_PASSWORD: postgres
# POSTGRES_DB: postgres
# ports:
# - "5432:5432"
# - "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
@@ -53,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
@@ -71,6 +92,11 @@ services:
# volumes:
# - minio_data:/data
# command: server /data --console-address ":9001"
# logging:
# driver: "json-file"
# options:
# max-size: "100m"
# max-file: "10"
volumes:
mongodb_data: {}
+1 -1
View File
@@ -10,7 +10,7 @@ services:
- './.sessions:/app/.sessions'
- './.media:/app/.media'
environment:
- WHATSAPP_API_KEY=321
- WAHA_API_KEY=321
- WAHA_DASHBOARD_USERNAME=admin
- WAHA_DASHBOARD_PASSWORD=admin
- WHATSAPP_DEFAULT_ENGINE=WEBJS
+2 -2
View File
@@ -29,7 +29,7 @@ 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
@@ -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
+57 -2
View File
@@ -1,11 +1,66 @@
#!/bin/sh
#
# Calculate UV_THREADPOOL_SIZE based on number of CPUs
#
cpus=$(node -e "const os = require('os'); console.log(os.cpus().length);")
uv_threadpool_size=$(($cpus * 1))
# 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
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
#
# Start your application using node with exec to ensure proper signal handling
#
if [ "$USE_XVFB" = "true" ]; then
exec xvfb-run --auto-servernum node dist/main
else
exec node dist/main
fi
Binary file not shown.
Binary file not shown.
+22 -20
View File
@@ -31,31 +31,32 @@
"@aws-sdk/client-s3": "^3.633.0",
"@aws-sdk/s3-request-presigner": "^3.633.0",
"@nestjs/axios": "^3.0.2",
"@nestjs/common": "^9.0.9",
"@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.0.0",
"@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/passport": "^1.0.17",
"@types/ws": "^8.5.4",
"adm-zip": "0.5.10",
"agentkeepalive": "^4.5.0",
"async-lock": "^1.4.1",
"audio-decode": "^2.2.2",
"axios": "^1.7.7",
"axios": "^1.9.0",
"axios-retry": "^4.5.0",
"better-sqlite3": "11.3.0",
"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",
@@ -71,23 +72,23 @@
"mongodb": "6.9.0",
"nestjs-pino": "^4.1.0",
"node-cache": "5.1.2",
"passport": "^0.6.0",
"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",
"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"
},
"optionalDependencies": {
@@ -96,16 +97,17 @@
"resolutions": {
"bufferutil": "^4.0.8",
"ws": "^8.18.0",
"puppeteer": "^24.7.2",
"puppeteer": "^24.10.0",
"whatwg-url": "13.0.0",
"libsignal": "github:devlikeapro/libsignal-node#fork-master"
"libsignal": "github:devlikeapro/libsignal-node#fork-master",
"axios": "^1.9.0"
},
"devDependencies": {
"@grpc/grpc-js": "^1.12.4",
"@grpc/grpc-js": "^1.13.4",
"@grpc/proto-loader": "^0.7.13",
"@nestjs/cli": "^9.0.0",
"@nestjs/schematics": "^9.0.1",
"@nestjs/testing": "^9.0.9",
"@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",
@@ -116,7 +118,7 @@
"eslint-config-prettier": "^6.10.0",
"eslint-plugin-import": "^2.20.1",
"eslint-plugin-simple-import-sort": "^10.0.0",
"grpc-tools": "^1.12.4",
"grpc-tools": "^1.13.0",
"jest": "^29.7.0",
"prettier": "^1.19.1",
"protoc-gen-ts": "^0.8.7",
+2 -2
View File
@@ -12,9 +12,9 @@ touch "$OUTPUT_FILE"
# Find all .md files in the source directory and combine them
find "$SOURCE_DIR" -type f -name "*.ts" | while read -r file; do
# Add file name to OUTPUT_FILE
echo -e "\n-------" >> "$OUTPUT_FILE"
echo "\n-------" >> "$OUTPUT_FILE"
echo "File: $file" >> "$OUTPUT_FILE"
echo -e "-------\n" >> "$OUTPUT_FILE"
echo "-------\n" >> "$OUTPUT_FILE"
# Append the content of the file to the output file
cat "$file" >> "$OUTPUT_FILE"
done
+2 -2
View File
@@ -36,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,
+18 -1
View File
@@ -30,6 +30,8 @@ import {
GetChatMessageQuery,
GetChatMessagesFilter,
GetChatMessagesQuery,
OverviewBodyRequest,
OverviewFilter,
OverviewPaginationParams,
PinMessageRequest,
ReadChatMessagesQuery,
@@ -65,8 +67,23 @@ class ChatsController {
getChatsOverview(
@WorkingSessionParam session: WhatsappSession,
@Query() pagination: OverviewPaginationParams,
@Query() filter: OverviewFilter,
): Promise<ChatSummary[]> {
return session.getChatsOverview(pagination);
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')
+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 };
+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}`,
});
}
}
+9
View File
@@ -233,6 +233,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 +258,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 +289,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);
@@ -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,
@@ -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,14 +64,23 @@ 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
@@ -97,26 +119,11 @@ 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);
// Close clients and server
await this.close(this.server);
this.logger.log('Websocket server is down');
}
// Cherry-pick from nestjs new version
// https://github.com/nestjs/nest/pull/13531/files
private async close(server: any) {
const closeEventSignal = new Promise((resolve, reject) =>
server.close((err) => (err ? reject(err) : resolve(undefined))),
);
for (const ws of server.clients) {
ws.terminate();
}
await closeEventSignal;
}
afterInit(server: Server) {
this.logger.debug('Websocket server initialized');
+4 -1
View File
@@ -138,7 +138,10 @@ export class WhatsappConfigService implements OnApplicationBootstrap {
}
getApiKey(): string | undefined {
return this.configService.get('WHATSAPP_API_KEY', '');
return (
this.configService.get('WHATSAPP_API_KEY', '') ||
this.configService.get('WAHA_API_KEY', '')
);
}
getExcludedPaths(): string[] {
+28 -2
View File
@@ -1,6 +1,8 @@
import { INestApplication } from '@nestjs/common';
import { DocumentBuilder, OpenAPIObject, SwaggerModule } from '@nestjs/swagger';
import { DECORATORS } from '@nestjs/swagger/dist/constants';
import { BasicAuthFunction } from '@waha/core/auth/basicAuth';
import { DashboardConfigServiceCore } from '@waha/core/config/DashboardConfigServiceCore';
import { Logger } from 'nestjs-pino';
import { WhatsappConfigService } from '../config.service';
@@ -9,16 +11,22 @@ import { SwaggerConfigServiceCore } from './config/SwaggerConfigServiceCore';
export class SwaggerConfiguratorCore {
protected logger: any;
private config: SwaggerConfigServiceCore;
constructor(protected app: INestApplication) {
this.logger = app.get(Logger);
this.config = app.get(SwaggerConfigServiceCore);
}
get title() {
return 'WAHA - WhatsApp HTTP API';
return this.config.title || 'WAHA - WhatsApp HTTP API';
}
get description() {
if (this.config.description) {
return this.config.description;
}
return (
'<b>WhatsApp HTTP API</b> that you can run in a click!<br/>' +
'<a href="/dashboard"><b>📊 Dashboard</b></a><br/>' +
@@ -43,10 +51,19 @@ export class SwaggerConfiguratorCore {
}
get externalDocUrl() {
return 'https://waha.devlike.pro/';
return this.config.externalDocUrl || 'https://waha.devlike.pro/';
}
configure(webhooks: any[]) {
if (!this.config.enabled) {
return;
}
const credentials = this.config.credentials;
if (credentials) {
this.setUpAuth(credentials);
}
const app = this.app;
const builder = new DocumentBuilder();
@@ -76,6 +93,7 @@ export class SwaggerConfiguratorCore {
'🏷️ Labels',
'Labels - available only for WhatsApp Business accounts',
)
.addTag('🖼️ Media', 'Media methods')
.addTag('🔍 Observability', 'Other methods')
.addTag('🗄️ Storage', 'Storage methods')
.addApiKey({
@@ -162,4 +180,12 @@ export class SwaggerConfiguratorCore {
document.webhooks = webhooks;
return document;
}
setUpAuth(credentials: [string, string]): void {
const [username, password] = credentials;
const dashboardConfig = this.app.get(DashboardConfigServiceCore);
const exclude = ['/api/', dashboardConfig.dashboardUri, '/health', '/ws'];
const authFunction = BasicAuthFunction(username, password, exclude);
this.app.use(authFunction);
}
}
+1
View File
@@ -57,6 +57,7 @@ export abstract class SessionManager
protected gowsConfigService: GowsEngineConfigService,
) {
this.lock = new AsyncLock({
timeout: 5_000,
maxPending: Infinity,
maxExecutionTime: 30_000,
});
+40 -11
View File
@@ -1,4 +1,8 @@
import { isJidBroadcast } from '@adiwajshing/baileys/lib/WABinary/jid-utils';
import {
CoreMediaConverter,
IMediaConverter,
} from '@waha/core/media/IConverter';
import { MessagesForRead } from '@waha/core/utils/convertors';
import { isJidNewsletter } from '@waha/core/utils/jids';
import {
@@ -16,6 +20,7 @@ import {
GetChatMessageQuery,
GetChatMessagesFilter,
GetChatMessagesQuery,
OverviewFilter,
ReadChatMessagesQuery,
ReadChatMessagesResponse,
} from '@waha/structures/chats.dto';
@@ -23,16 +28,16 @@ import { SendButtonsRequest } from '@waha/structures/chatting.buttons.dto';
import { BinaryFile, RemoteFile } from '@waha/structures/files.dto';
import { Label, LabelDTO, LabelID } from '@waha/structures/labels.dto';
import { LidToPhoneNumber } from '@waha/structures/lids.dto';
import {
LimitOffsetParams,
PaginationParams,
} from '@waha/structures/pagination.dto';
import { PaginationParams } from '@waha/structures/pagination.dto';
import { MessageSource, WAMessage } from '@waha/structures/responses.dto';
import { BrowserTraceQuery } from '@waha/structures/server.debug.dto';
import { DefaultMap } from '@waha/utils/DefaultMap';
import { generatePrefixedId } from '@waha/utils/ids';
import { LoggerBuilder } from '@waha/utils/logging';
import { complete } from '@waha/utils/reactive/complete';
import { SwitchObservable } from '@waha/utils/reactive/SwitchObservable';
import axios from 'axios';
import axiosRetry from 'axios-retry';
import * as fs from 'fs';
import * as lodash from 'lodash';
import * as NodeCache from 'node-cache';
@@ -108,9 +113,12 @@ import { NotImplementedByEngineError } from '../exceptions';
import { IMediaManager } from '../media/IMediaManager';
import { QR } from '../QR';
import { DataStore } from './DataStore';
// eslint-disable-next-line @typescript-eslint/no-var-requires
const qrcode = require('qrcode-terminal');
axiosRetry(axios, { retries: 3 });
const CHROME_PATH = '/usr/bin/google-chrome-stable';
const CHROMIUM_PATH = '/usr/bin/chromium';
@@ -167,6 +175,8 @@ export abstract class WhatsappSession {
stdTTL: 10 * 60, // 10 minutes
});
public mediaConverter: IMediaConverter = new CoreMediaConverter();
public constructor({
name,
printQR,
@@ -276,7 +286,6 @@ export abstract class WhatsappSession {
'--aggressive-cache-discard',
'--disable-accelerated-2d-canvas',
'--disable-application-cache',
// '--disable-background-networking', // COMMENTED to test WEBJS stability
// DO NOT disable software rasterizer, it will break the video
// https://github.com/devlikeapro/waha/issues/629
// '--disable-software-rasterizer',
@@ -287,7 +296,7 @@ export abstract class WhatsappSession {
'--disable-dev-shm-usage',
'--disable-extensions',
// '--disable-features=site-per-process', // COMMENTED to test WEBJS stability
//'--disable-gpu', // COMMENTED to test WEBJS stability
'--disable-gpu', // COMMENTED to test WEBJS stability
'--disable-offer-store-unmasked-wallet-cards',
'--disable-offline-load-stale-cache',
'--disable-popup-blocking',
@@ -311,9 +320,8 @@ export abstract class WhatsappSession {
'--no-sandbox',
'--no-zygote',
'--password-store=basic',
// '--renderer-process-limit=2', // COMMENTED to test WEBJS stability
'--renderer-process-limit=1',
'--safebrowsing-disable-auto-update',
// '--single-process',
'--use-mock-keychain',
'--window-size=1280,720',
];
@@ -342,6 +350,10 @@ export abstract class WhatsappSession {
* START - Methods for API
*/
public browserTrace(query: BrowserTraceQuery): Promise<string> {
throw new NotImplementedByEngineError();
}
/**
* Auth methods
*/
@@ -491,6 +503,7 @@ export abstract class WhatsappSession {
public getChatsOverview(
pagination: PaginationParams,
filter?: OverviewFilter,
): Promise<ChatSummary[]> {
throw new NotImplementedByEngineError();
}
@@ -672,10 +685,17 @@ export abstract class WhatsappSession {
}
protected async refreshProfilePicture(id: string) {
let fn: Promise<string>;
if (isJidBroadcast(id)) {
this.logger.debug(`Refreshing profile picture for id "${id}"...`);
// Have no pictures
if (id === '0@c.us' || id === '0@s.whatsapp.net') {
return null;
} else if (isJidNewsletter(id)) {
} else if (isJidBroadcast(id)) {
return null;
}
// Find the right method
let fn: Promise<string>;
if (isJidNewsletter(id)) {
fn = this.channelsGetChannel(id).then(
(channel: Channel) => channel.picture || channel.preview,
);
@@ -991,6 +1011,15 @@ export abstract class WhatsappSession {
const api = this.sentMessageIds.has(id);
return api ? MessageSource.API : MessageSource.APP;
}
/**
* Fetches the content from the specified URL and returns it as a Buffer.
*/
public async fetch(url: string): Promise<Buffer> {
return axios.get(url, { responseType: 'arraybuffer' }).then((res) => {
return Buffer.from(res.data);
});
}
}
export function getGroupInviteLink(code: string) {
+75 -21
View File
@@ -1,4 +1,7 @@
import { INestApplication, Module } from '@nestjs/common';
import * as process from 'node:process';
import { INestApplication, MiddlewareConsumer, Module } from '@nestjs/common';
import { Provider } from '@nestjs/common/interfaces/modules/provider.interface';
import { ConfigModule } from '@nestjs/config';
import { APP_INTERCEPTOR } from '@nestjs/core';
import { PassportModule } from '@nestjs/passport';
@@ -7,24 +10,31 @@ import { TerminusModule } from '@nestjs/terminus';
import { ChannelsController } from '@waha/api/channels.controller';
import { LidsController } from '@waha/api/lids.controller';
import { ProfileController } from '@waha/api/profile.controller';
import {
ServerController,
ServerDebugController,
} from '@waha/api/server.controller';
import { WebsocketGatewayCore } from '@waha/core/api/websocket.gateway.core';
import { ServerController } from '@waha/api/server.controller';
import { ServerDebugController } from '@waha/api/server.debug.controller';
import { WebsocketGatewayCore } from '@waha/api/websocket.gateway.core';
import { ApiKeyStrategy } from '@waha/core/auth/apiKey.strategy';
import { IApiKeyAuth } from '@waha/core/auth/auth';
import { AuthMiddleware } from '@waha/core/auth/auth.middleware';
import { BasicAuthFunction } from '@waha/core/auth/basicAuth';
import { WebSocketAuth } from '@waha/core/auth/WebSocketAuth';
import { GowsEngineConfigService } from '@waha/core/config/GowsEngineConfigService';
import { WebJSEngineConfigService } from '@waha/core/config/WebJSEngineConfigService';
import { MediaLocalStorageModule } from '@waha/core/media/local/media.local.storage.module';
import { MediaLocalStorageConfig } from '@waha/core/media/local/MediaLocalStorageConfig';
import { ChannelsInfoServiceCore } from '@waha/core/services/ChannelsInfoServiceCore';
import { parseBool } from '@waha/helpers';
import { BufferJsonReplacerInterceptor } from '@waha/nestjs/BufferJsonReplacerInterceptor';
import { HttpsExpress } from '@waha/nestjs/HttpsExpress';
import {
getPinoHttpUseLevel,
getPinoLogLevel,
getPinoTransport,
} from '@waha/utils/logging';
import { noSlashAtTheEnd } from '@waha/utils/string';
import * as Joi from 'joi';
import { LoggerModule } from 'nestjs-pino';
import { Logger as NestJSPinoLogger } from 'nestjs-pino';
import { join } from 'path';
import { Logger } from 'pino';
@@ -36,6 +46,7 @@ import { EventsController } from '../api/events.controller';
import { GroupsController } from '../api/groups.controller';
import { HealthController } from '../api/health.controller';
import { LabelsController } from '../api/labels.controller';
import { MediaController } from '../api/media.controller';
import { PingController } from '../api/ping.controller';
import { PresenceController } from '../api/presence.controller';
import { ScreenshotController } from '../api/screenshot.controller';
@@ -45,6 +56,7 @@ import { VersionController } from '../api/version.controller';
import { WhatsappConfigService } from '../config.service';
import { SessionManager } from './abc/manager.abc';
import { WAHAHealthCheckService } from './abc/WAHAHealthCheckService';
import { ApiKeyAuthFactory } from './auth/ApiKeyAuthFactory';
import { DashboardConfigServiceCore } from './config/DashboardConfigServiceCore';
import { EngineConfigService } from './config/EngineConfigService';
import { SwaggerConfigServiceCore } from './config/SwaggerConfigServiceCore';
@@ -62,6 +74,7 @@ export const IMPORTS_CORE = [
autoLogging: {
ignore: (req) => {
return (
req.url.startsWith('/ping') ||
req.url.startsWith('/dashboard/') ||
req.url.startsWith('/api/files/') ||
req.url.startsWith('/api/s3/')
@@ -141,7 +154,30 @@ export const CONTROLLERS = [
ServerController,
ServerDebugController,
VersionController,
MediaController,
];
export const PROVIDERS_BASE: Provider[] = [
{
provide: APP_INTERCEPTOR,
useClass: BufferJsonReplacerInterceptor,
},
DashboardConfigServiceCore,
SwaggerConfigServiceCore,
WebJSEngineConfigService,
GowsEngineConfigService,
WhatsappConfigService,
EngineConfigService,
WebsocketGatewayCore,
MediaLocalStorageConfig,
WebSocketAuth,
ApiKeyStrategy,
{
provide: IApiKeyAuth,
useFactory: ApiKeyAuthFactory,
inject: [WhatsappConfigService, NestJSPinoLogger],
},
];
const PROVIDERS = [
{
provide: SessionManager,
@@ -151,19 +187,8 @@ const PROVIDERS = [
provide: WAHAHealthCheckService,
useClass: WAHAHealthCheckServiceCore,
},
{
provide: APP_INTERCEPTOR,
useClass: BufferJsonReplacerInterceptor,
},
ChannelsInfoServiceCore,
DashboardConfigServiceCore,
SwaggerConfigServiceCore,
WebJSEngineConfigService,
GowsEngineConfigService,
WhatsappConfigService,
EngineConfigService,
WebsocketGatewayCore,
MediaLocalStorageConfig,
...PROVIDERS_BASE,
];
@Module({
@@ -174,15 +199,44 @@ const PROVIDERS = [
export class AppModuleCore {
public startTimestamp: number;
constructor(protected config: WhatsappConfigService) {
constructor(
protected config: WhatsappConfigService,
private dashboardConfig: DashboardConfigServiceCore,
) {
this.startTimestamp = Date.now();
}
static getHttpsOptions(logger: Logger) {
return undefined;
const httpsEnabled = parseBool(process.env.WAHA_HTTPS_ENABLED);
if (!httpsEnabled) {
return undefined;
}
const httpsExpress = new HttpsExpress(logger);
return httpsExpress.readSync();
}
static appReady(app: INestApplication, logger: Logger) {
return;
const httpsEnabled = parseBool(process.env.WAHA_HTTPS_ENABLED);
if (!httpsEnabled) {
return;
}
const httpd = app.getHttpServer();
const httpsExpress = new HttpsExpress(logger);
httpsExpress.watchCertChanges(httpd);
}
configure(consumer: MiddlewareConsumer) {
const exclude = this.config.getExcludedPaths();
consumer
.apply(AuthMiddleware)
.exclude(...exclude)
.forRoutes('api', 'health', 'ws');
const dashboardCredentials = this.dashboardConfig.credentials;
if (dashboardCredentials) {
const username = dashboardCredentials[0];
const password = dashboardCredentials[1];
const route = noSlashAtTheEnd(this.dashboardConfig.dashboardUri);
consumer.apply(BasicAuthFunction(username, password)).forRoutes(route);
}
}
}
+53
View File
@@ -0,0 +1,53 @@
import { LoggerService } from '@nestjs/common';
import { WhatsappConfigService } from '@waha/config.service';
import {
HashAuth,
IApiKeyAuth,
NoAuth,
PlainApiKeyAuth,
} from '@waha/core/auth/auth';
export function ApiKeyAuthFactory(
config: WhatsappConfigService,
logger: LoggerService,
): IApiKeyAuth {
const apiKey = config.getApiKey();
if (!apiKey) {
setTimeout(() => {
logger.warn('🚫🚫🚫🚫🚫🚫🚫🚫🚫🚫🚫🚫🚫🚫🚫🚫🚫🚫🚫🚫🚫🚫🚫🚫🚫');
logger.warn('WARNING: No API key detected. This is a security risk.');
logger.warn(
'Your API is publicly accessible without any authentication.',
);
logger.warn(
'To secure your API, set environment variable: WAHA_API_KEY=your_api_key',
);
logger.warn(
'For better security, use WAHA_API_KEY=sha512:{SHA512_HASH_FOR_YOUR_API_KEY}',
);
logger.warn('🚫🚫🚫🚫🚫🚫🚫🚫🚫🚫🚫🚫🚫🚫🚫🚫🚫🚫🚫🚫🚫🚫🚫🚫🚫');
}, 3000);
return new NoAuth();
}
if (apiKey.startsWith('sha512:')) {
const hash = apiKey.slice(7);
return new HashAuth(hash, 'sha512');
}
// Fallback to plain text
setTimeout(() => {
logger.warn('⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️');
logger.warn(
'WARNING: Plain text API key detected. This is a security risk.',
);
logger.warn(
'Your API key can be exposed in environment variables or process lists.',
);
logger.warn(
'For better security, use WAHA_API_KEY=sha512:{SHA512_HASH_FOR_YOUR_API_KEY}',
);
logger.warn('⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️');
}, 2000);
return new PlainApiKeyAuth(apiKey);
}
+34
View File
@@ -0,0 +1,34 @@
import { Injectable } from '@nestjs/common';
import { IncomingMessage } from 'http';
import * as url from 'url';
import { IApiKeyAuth } from './auth';
@Injectable()
export class WebSocketAuth {
constructor(private auth: IApiKeyAuth) {}
validateRequest(request: IncomingMessage) {
if (this.auth.skipAuth()) {
return true;
}
const provided = this.getKeyFromQueryParams(request);
return this.auth.isValid(provided);
}
private getKeyFromQueryParams(request: IncomingMessage) {
let query = url.parse(request.url, true).query;
// case-insensitive query params
query = Object.keys(query).reduce((acc, key) => {
acc[key.toLowerCase()] = query[key];
return acc;
}, {});
const provided = query['x-api-key'];
// Check if it's array - return first
if (Array.isArray(provided)) {
return provided[0];
}
return provided;
}
}
+19
View File
@@ -0,0 +1,19 @@
import { Injectable } from '@nestjs/common';
import { PassportStrategy } from '@nestjs/passport';
import { IApiKeyAuth } from '@waha/core/auth/auth';
import { HeaderAPIKeyStrategy } from 'passport-headerapikey';
@Injectable()
export class ApiKeyStrategy extends PassportStrategy(HeaderAPIKeyStrategy) {
constructor(private auth: IApiKeyAuth) {
super({ header: 'X-Api-Key', prefix: '' }, true, (apikey, done) => {
const isValid = this.auth.isValid(apikey);
return done(isValid);
});
}
validate(apikey: string, done: (result: boolean) => void): void {
const isValid = this.auth.isValid(apikey);
return done(isValid);
}
}
+28
View File
@@ -0,0 +1,28 @@
import {
Injectable,
NestMiddleware,
UnauthorizedException,
} from '@nestjs/common';
import * as passport from 'passport';
import { IApiKeyAuth } from './auth';
@Injectable()
export class AuthMiddleware implements NestMiddleware {
constructor(private auth: IApiKeyAuth) {}
use(req: any, res: any, next: () => void) {
// Skip authentication if auth says so
if (this.auth.skipAuth()) {
next();
return;
}
passport.authenticate('headerapikey', { session: false }, (value) => {
if (!value) {
throw new UnauthorizedException();
}
next();
})(req, res, next);
}
}
+79
View File
@@ -0,0 +1,79 @@
import * as crypto from 'crypto';
export abstract class IApiKeyAuth {
abstract isValid(plain: string): boolean;
abstract skipAuth(): boolean;
}
export class NoAuth implements IApiKeyAuth {
isValid(plain: string): boolean {
return true;
}
skipAuth(): boolean {
return true;
}
}
export class PlainApiKeyAuth implements IApiKeyAuth {
constructor(private key: string) {}
isValid(plain: string): boolean {
return compare(plain, this.key);
}
skipAuth(): boolean {
return false;
}
}
export class HashAuth implements IApiKeyAuth {
constructor(
private hash: string,
private algorithm: string,
) {
this.algorithm = algorithm;
}
isValid(plain: string): boolean {
if (!plain) {
return false;
}
const hash = crypto.createHash(this.algorithm).update(plain).digest('hex');
return compare(hash, this.hash);
}
skipAuth(): boolean {
return false;
}
}
/**
* Securely compare 2 strings
*/
export function compare(provided: string, stored: string | undefined): boolean {
if (!stored || !provided) {
return false;
}
try {
// Convert strings to buffers for constant-time comparison
const providedBuffer = Buffer.from(provided);
const storedBuffer = Buffer.from(stored);
// If lengths are different, return false but use a dummy comparison to prevent timing attacks
if (providedBuffer.length !== storedBuffer.length) {
// Create a dummy buffer of the same length as the provided key
const dummyBuffer = Buffer.alloc(providedBuffer.length);
// Perform comparison with dummy buffer to maintain constant time
crypto.timingSafeEqual(providedBuffer, dummyBuffer);
return false;
}
// Perform constant-time comparison
return crypto.timingSafeEqual(providedBuffer, storedBuffer);
} catch (error) {
return false;
}
}
+21
View File
@@ -0,0 +1,21 @@
import * as basicAuth from 'express-basic-auth';
export function BasicAuthFunction(username, password, exclude: string[] = []) {
function authFunction(req, res, next) {
const ignore = exclude.filter((url) => req.url.startsWith(url)).length > 0;
if (ignore) {
next();
return;
}
const auth = basicAuth({
challenge: true,
users: {
[username]: password,
},
});
auth(req, res, next);
}
return authFunction;
}
@@ -18,4 +18,20 @@ export class DashboardConfigServiceCore {
const value = this.configService.get('WAHA_DASHBOARD_ENABLED', 'true');
return parseBool(value);
}
get credentials(): [string, string] | null {
const user = this.configService.get('WAHA_DASHBOARD_USERNAME', '');
const password = this.configService.get('WAHA_DASHBOARD_PASSWORD', '');
if (!user && !password) {
return null;
}
if ((user && !password) || (!user && password)) {
this.logger.warn(
'Set up both WAHA_DASHBOARD_USERNAME and WAHA_DASHBOARD_PASSWORD ' +
'to enable dashboard authentication.',
);
return null;
}
return [user, password];
}
}
+6 -1
View File
@@ -1,16 +1,21 @@
import { Injectable } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { WhatsappConfigService } from '@waha/config.service';
import { BootstrapConfig } from '@waha/core/engines/gows/GowsBootstrap';
import { GowsConfig } from '@waha/core/engines/gows/session.gows.core';
@Injectable()
export class GowsEngineConfigService {
constructor(protected configService: ConfigService) {}
constructor(
protected configService: ConfigService,
protected whatsappConfigService: WhatsappConfigService,
) {}
getBootstrapConfig(): BootstrapConfig {
return {
path: this.configService.get('WAHA_GOWS_PATH'),
socket: this.getSocket(),
pprof: this.whatsappConfigService.debugModeEnabled,
};
}
@@ -19,4 +19,43 @@ export class SwaggerConfigServiceCore {
);
return parseBool(value);
}
get enabled(): boolean {
const value = this.configService.get('WHATSAPP_SWAGGER_ENABLED', 'true');
return parseBool(value);
}
get credentials(): [string, string] | undefined {
const user = this.configService.get<string>(
'WHATSAPP_SWAGGER_USERNAME',
undefined,
);
const password = this.configService.get<string>(
'WHATSAPP_SWAGGER_PASSWORD',
undefined,
);
if (!user && !password) {
return null;
}
if ((user && !password) || (!user && password)) {
this.logger.warn(
'Set up both WHATSAPP_SWAGGER_USERNAME and WHATSAPP_SWAGGER_PASSWORD ' +
'to enable swagger authentication.',
);
return null;
}
return [user, password];
}
get title() {
return this.configService.get('WHATSAPP_SWAGGER_TITLE', '');
}
get description() {
return this.configService.get('WHATSAPP_SWAGGER_DESCRIPTION', '');
}
get externalDocUrl() {
return this.configService.get('WHATSAPP_SWAGGER_EXTERNAL_DOC_URL', '');
}
}
+2
View File
@@ -22,6 +22,7 @@ export async function isUnixSocket(socketPath: string): Promise<boolean> {
export interface BootstrapConfig {
path: string;
socket: string;
pprof?: boolean;
}
export class GowsBootstrap implements EngineBootstrap {
@@ -44,6 +45,7 @@ export class GowsBootstrap implements EngineBootstrap {
this.logger,
this.config.path,
this.config.socket,
this.config.pprof,
);
this.gows.start(() => {
this.logger.info(`GOWS stopped, exiting...`);
+10 -1
View File
@@ -14,13 +14,22 @@ export class GowsSubprocess {
private logger: Logger,
readonly path: string,
readonly socket: string,
readonly pprof: boolean = false,
) {}
start(onExit: (code: number) => void) {
this.logger.info('Starting GOWS subprocess...');
this.logger.debug(`GOWS path '${this.path}', socket: '${this.socket}'...`);
this.child = spawn(this.path, ['--socket', this.socket], {
const args = ['--socket', this.socket];
if (this.pprof) {
this.logger.info('Debug mode enabled, adding pprof flags');
args.push('--pprof');
args.push('--pprof-port=6060');
args.push('--pprof-host=0.0.0.0');
}
this.child = spawn(this.path, args, {
detached: true,
});
this.logger.debug(`GOWS started with PID: ${this.child.pid}`);
+278
View File
@@ -3262,6 +3262,7 @@ export namespace messages {
preview?: LinkPreview;
contacts?: vCardContact[];
event?: EventMessage;
poll?: PollMessage;
}) {
super();
pb_1.Message.initialize(this, Array.isArray(data) ? data : [], 0, -1, [11, 13], this.#one_of_decls);
@@ -3308,6 +3309,9 @@ export namespace messages {
if ("event" in data && data.event != undefined) {
this.event = data.event;
}
if ("poll" in data && data.poll != undefined) {
this.poll = data.poll;
}
}
}
get session() {
@@ -3412,6 +3416,15 @@ export namespace messages {
get has_event() {
return pb_1.Message.getField(this, 14) != null;
}
get poll() {
return pb_1.Message.getWrapperField(this, PollMessage, 15) as PollMessage;
}
set poll(value: PollMessage) {
pb_1.Message.setWrapperField(this, 15, value);
}
get has_poll() {
return pb_1.Message.getField(this, 15) != null;
}
static fromObject(data: {
session?: ReturnType<typeof Session.prototype.toObject>;
jid?: string;
@@ -3427,6 +3440,7 @@ export namespace messages {
preview?: ReturnType<typeof LinkPreview.prototype.toObject>;
contacts?: ReturnType<typeof vCardContact.prototype.toObject>[];
event?: ReturnType<typeof EventMessage.prototype.toObject>;
poll?: ReturnType<typeof PollMessage.prototype.toObject>;
}): MessageRequest {
const message = new MessageRequest({});
if (data.session != null) {
@@ -3471,6 +3485,9 @@ export namespace messages {
if (data.event != null) {
message.event = EventMessage.fromObject(data.event);
}
if (data.poll != null) {
message.poll = PollMessage.fromObject(data.poll);
}
return message;
}
toObject() {
@@ -3489,6 +3506,7 @@ export namespace messages {
preview?: ReturnType<typeof LinkPreview.prototype.toObject>;
contacts?: ReturnType<typeof vCardContact.prototype.toObject>[];
event?: ReturnType<typeof EventMessage.prototype.toObject>;
poll?: ReturnType<typeof PollMessage.prototype.toObject>;
} = {};
if (this.session != null) {
data.session = this.session.toObject();
@@ -3532,6 +3550,9 @@ export namespace messages {
if (this.event != null) {
data.event = this.event.toObject();
}
if (this.poll != null) {
data.poll = this.poll.toObject();
}
return data;
}
serialize(): Uint8Array;
@@ -3566,6 +3587,8 @@ export namespace messages {
writer.writeRepeatedMessage(13, this.contacts, (item: vCardContact) => item.serialize(writer));
if (this.has_event)
writer.writeMessage(14, this.event, () => this.event.serialize(writer));
if (this.has_poll)
writer.writeMessage(15, this.poll, () => this.poll.serialize(writer));
if (!w)
return writer.getResultBuffer();
}
@@ -3617,6 +3640,9 @@ export namespace messages {
case 14:
reader.readMessage(message.event, () => message.event = EventMessage.deserialize(reader));
break;
case 15:
reader.readMessage(message.poll, () => message.poll = PollMessage.deserialize(reader));
break;
default: reader.skipField();
}
}
@@ -7005,6 +7031,8 @@ export namespace messages {
constructor(data?: any[] | {
session?: Session;
message?: string;
jid?: string;
messageId?: string;
}) {
super();
pb_1.Message.initialize(this, Array.isArray(data) ? data : [], 0, -1, [], this.#one_of_decls);
@@ -7015,6 +7043,12 @@ export namespace messages {
if ("message" in data && data.message != undefined) {
this.message = data.message;
}
if ("jid" in data && data.jid != undefined) {
this.jid = data.jid;
}
if ("messageId" in data && data.messageId != undefined) {
this.messageId = data.messageId;
}
}
}
get session() {
@@ -7032,9 +7066,23 @@ export namespace messages {
set message(value: string) {
pb_1.Message.setField(this, 2, value);
}
get jid() {
return pb_1.Message.getFieldWithDefault(this, 3, "") as string;
}
set jid(value: string) {
pb_1.Message.setField(this, 3, value);
}
get messageId() {
return pb_1.Message.getFieldWithDefault(this, 4, "") as string;
}
set messageId(value: string) {
pb_1.Message.setField(this, 4, value);
}
static fromObject(data: {
session?: ReturnType<typeof Session.prototype.toObject>;
message?: string;
jid?: string;
messageId?: string;
}): DownloadMediaRequest {
const message = new DownloadMediaRequest({});
if (data.session != null) {
@@ -7043,12 +7091,20 @@ export namespace messages {
if (data.message != null) {
message.message = data.message;
}
if (data.jid != null) {
message.jid = data.jid;
}
if (data.messageId != null) {
message.messageId = data.messageId;
}
return message;
}
toObject() {
const data: {
session?: ReturnType<typeof Session.prototype.toObject>;
message?: string;
jid?: string;
messageId?: string;
} = {};
if (this.session != null) {
data.session = this.session.toObject();
@@ -7056,6 +7112,12 @@ export namespace messages {
if (this.message != null) {
data.message = this.message;
}
if (this.jid != null) {
data.jid = this.jid;
}
if (this.messageId != null) {
data.messageId = this.messageId;
}
return data;
}
serialize(): Uint8Array;
@@ -7066,6 +7128,10 @@ export namespace messages {
writer.writeMessage(1, this.session, () => this.session.serialize(writer));
if (this.message.length)
writer.writeString(2, this.message);
if (this.jid.length)
writer.writeString(3, this.jid);
if (this.messageId.length)
writer.writeString(4, this.messageId);
if (!w)
return writer.getResultBuffer();
}
@@ -7081,6 +7147,12 @@ export namespace messages {
case 2:
message.message = reader.readString();
break;
case 3:
message.jid = reader.readString();
break;
case 4:
message.messageId = reader.readString();
break;
default: reader.skipField();
}
}
@@ -7991,12 +8063,80 @@ export namespace messages {
return GetContactsRequest.deserialize(bytes);
}
}
export class ChatFilter extends pb_1.Message {
#one_of_decls: number[][] = [];
constructor(data?: any[] | {
jids?: string[];
}) {
super();
pb_1.Message.initialize(this, Array.isArray(data) ? data : [], 0, -1, [1], this.#one_of_decls);
if (!Array.isArray(data) && typeof data == "object") {
if ("jids" in data && data.jids != undefined) {
this.jids = data.jids;
}
}
}
get jids() {
return pb_1.Message.getFieldWithDefault(this, 1, []) as string[];
}
set jids(value: string[]) {
pb_1.Message.setField(this, 1, value);
}
static fromObject(data: {
jids?: string[];
}): ChatFilter {
const message = new ChatFilter({});
if (data.jids != null) {
message.jids = data.jids;
}
return message;
}
toObject() {
const data: {
jids?: string[];
} = {};
if (this.jids != null) {
data.jids = this.jids;
}
return data;
}
serialize(): Uint8Array;
serialize(w: pb_1.BinaryWriter): void;
serialize(w?: pb_1.BinaryWriter): Uint8Array | void {
const writer = w || new pb_1.BinaryWriter();
if (this.jids.length)
writer.writeRepeatedString(1, this.jids);
if (!w)
return writer.getResultBuffer();
}
static deserialize(bytes: Uint8Array | pb_1.BinaryReader): ChatFilter {
const reader = bytes instanceof pb_1.BinaryReader ? bytes : new pb_1.BinaryReader(bytes), message = new ChatFilter();
while (reader.nextField()) {
if (reader.isEndGroup())
break;
switch (reader.getFieldNumber()) {
case 1:
pb_1.Message.addToRepeatedField(message, 1, reader.readString());
break;
default: reader.skipField();
}
}
return message;
}
serializeBinary(): Uint8Array {
return this.serialize();
}
static deserializeBinary(bytes: Uint8Array): ChatFilter {
return ChatFilter.deserialize(bytes);
}
}
export class GetChatsRequest extends pb_1.Message {
#one_of_decls: number[][] = [];
constructor(data?: any[] | {
session?: Session;
sortBy?: SortBy;
pagination?: Pagination;
filter?: ChatFilter;
}) {
super();
pb_1.Message.initialize(this, Array.isArray(data) ? data : [], 0, -1, [], this.#one_of_decls);
@@ -8010,6 +8150,9 @@ export namespace messages {
if ("pagination" in data && data.pagination != undefined) {
this.pagination = data.pagination;
}
if ("filter" in data && data.filter != undefined) {
this.filter = data.filter;
}
}
}
get session() {
@@ -8039,10 +8182,20 @@ export namespace messages {
get has_pagination() {
return pb_1.Message.getField(this, 3) != null;
}
get filter() {
return pb_1.Message.getWrapperField(this, ChatFilter, 4) as ChatFilter;
}
set filter(value: ChatFilter) {
pb_1.Message.setWrapperField(this, 4, value);
}
get has_filter() {
return pb_1.Message.getField(this, 4) != null;
}
static fromObject(data: {
session?: ReturnType<typeof Session.prototype.toObject>;
sortBy?: ReturnType<typeof SortBy.prototype.toObject>;
pagination?: ReturnType<typeof Pagination.prototype.toObject>;
filter?: ReturnType<typeof ChatFilter.prototype.toObject>;
}): GetChatsRequest {
const message = new GetChatsRequest({});
if (data.session != null) {
@@ -8054,6 +8207,9 @@ export namespace messages {
if (data.pagination != null) {
message.pagination = Pagination.fromObject(data.pagination);
}
if (data.filter != null) {
message.filter = ChatFilter.fromObject(data.filter);
}
return message;
}
toObject() {
@@ -8061,6 +8217,7 @@ export namespace messages {
session?: ReturnType<typeof Session.prototype.toObject>;
sortBy?: ReturnType<typeof SortBy.prototype.toObject>;
pagination?: ReturnType<typeof Pagination.prototype.toObject>;
filter?: ReturnType<typeof ChatFilter.prototype.toObject>;
} = {};
if (this.session != null) {
data.session = this.session.toObject();
@@ -8071,6 +8228,9 @@ export namespace messages {
if (this.pagination != null) {
data.pagination = this.pagination.toObject();
}
if (this.filter != null) {
data.filter = this.filter.toObject();
}
return data;
}
serialize(): Uint8Array;
@@ -8083,6 +8243,8 @@ export namespace messages {
writer.writeMessage(2, this.sortBy, () => this.sortBy.serialize(writer));
if (this.has_pagination)
writer.writeMessage(3, this.pagination, () => this.pagination.serialize(writer));
if (this.has_filter)
writer.writeMessage(4, this.filter, () => this.filter.serialize(writer));
if (!w)
return writer.getResultBuffer();
}
@@ -8101,6 +8263,9 @@ export namespace messages {
case 3:
reader.readMessage(message.pagination, () => message.pagination = Pagination.deserialize(reader));
break;
case 4:
reader.readMessage(message.filter, () => message.filter = ChatFilter.deserialize(reader));
break;
default: reader.skipField();
}
}
@@ -8790,6 +8955,119 @@ export namespace messages {
return GetLidsRequest.deserialize(bytes);
}
}
export class PollMessage extends pb_1.Message {
#one_of_decls: number[][] = [];
constructor(data?: any[] | {
name?: string;
options?: string[];
multipleAnswers?: boolean;
}) {
super();
pb_1.Message.initialize(this, Array.isArray(data) ? data : [], 0, -1, [2], this.#one_of_decls);
if (!Array.isArray(data) && typeof data == "object") {
if ("name" in data && data.name != undefined) {
this.name = data.name;
}
if ("options" in data && data.options != undefined) {
this.options = data.options;
}
if ("multipleAnswers" in data && data.multipleAnswers != undefined) {
this.multipleAnswers = data.multipleAnswers;
}
}
}
get name() {
return pb_1.Message.getFieldWithDefault(this, 1, "") as string;
}
set name(value: string) {
pb_1.Message.setField(this, 1, value);
}
get options() {
return pb_1.Message.getFieldWithDefault(this, 2, []) as string[];
}
set options(value: string[]) {
pb_1.Message.setField(this, 2, value);
}
get multipleAnswers() {
return pb_1.Message.getFieldWithDefault(this, 3, false) as boolean;
}
set multipleAnswers(value: boolean) {
pb_1.Message.setField(this, 3, value);
}
static fromObject(data: {
name?: string;
options?: string[];
multipleAnswers?: boolean;
}): PollMessage {
const message = new PollMessage({});
if (data.name != null) {
message.name = data.name;
}
if (data.options != null) {
message.options = data.options;
}
if (data.multipleAnswers != null) {
message.multipleAnswers = data.multipleAnswers;
}
return message;
}
toObject() {
const data: {
name?: string;
options?: string[];
multipleAnswers?: boolean;
} = {};
if (this.name != null) {
data.name = this.name;
}
if (this.options != null) {
data.options = this.options;
}
if (this.multipleAnswers != null) {
data.multipleAnswers = this.multipleAnswers;
}
return data;
}
serialize(): Uint8Array;
serialize(w: pb_1.BinaryWriter): void;
serialize(w?: pb_1.BinaryWriter): Uint8Array | void {
const writer = w || new pb_1.BinaryWriter();
if (this.name.length)
writer.writeString(1, this.name);
if (this.options.length)
writer.writeRepeatedString(2, this.options);
if (this.multipleAnswers != false)
writer.writeBool(3, this.multipleAnswers);
if (!w)
return writer.getResultBuffer();
}
static deserialize(bytes: Uint8Array | pb_1.BinaryReader): PollMessage {
const reader = bytes instanceof pb_1.BinaryReader ? bytes : new pb_1.BinaryReader(bytes), message = new PollMessage();
while (reader.nextField()) {
if (reader.isEndGroup())
break;
switch (reader.getFieldNumber()) {
case 1:
message.name = reader.readString();
break;
case 2:
pb_1.Message.addToRepeatedField(message, 2, reader.readString());
break;
case 3:
message.multipleAnswers = reader.readBool();
break;
default: reader.skipField();
}
}
return message;
}
serializeBinary(): Uint8Array {
return this.serialize();
}
static deserializeBinary(bytes: Uint8Array): PollMessage {
return PollMessage.deserialize(bytes);
}
}
interface GrpcUnaryServiceInterface<P, R> {
(message: P, metadata: grpc_1.Metadata, options: grpc_1.CallOptions, callback: grpc_1.requestCallback<R>): grpc_1.ClientUnaryCall;
(message: P, metadata: grpc_1.Metadata, callback: grpc_1.requestCallback<R>): grpc_1.ClientUnaryCall;
+2 -2
View File
@@ -671,7 +671,7 @@ var EventStreamService = exports.EventStreamService = {
},
};
exports.EventStreamClient = grpc.makeGenericClientConstructor(EventStreamService);
exports.EventStreamClient = grpc.makeGenericClientConstructor(EventStreamService, 'EventStream');
var MessageServiceService = exports.MessageServiceService = {
//
// Session management
@@ -1381,4 +1381,4 @@ getMessageById: {
},
};
exports.MessageServiceClient = grpc.makeGenericClientConstructor(MessageServiceService);
exports.MessageServiceClient = grpc.makeGenericClientConstructor(MessageServiceService, 'MessageService');
+581 -3
View File
@@ -24,6 +24,7 @@ var global = (function() {
goog.exportSymbol('proto.messages.AudioInfo', null, global);
goog.exportSymbol('proto.messages.ButtonReplyRequest', null, global);
goog.exportSymbol('proto.messages.CancelEventMessageRequest', null, global);
goog.exportSymbol('proto.messages.ChatFilter', null, global);
goog.exportSymbol('proto.messages.ChatLabelRequest', null, global);
goog.exportSymbol('proto.messages.ChatPresence', null, global);
goog.exportSymbol('proto.messages.ChatPresenceRequest', null, global);
@@ -81,6 +82,7 @@ goog.exportSymbol('proto.messages.PairCodeRequest', null, global);
goog.exportSymbol('proto.messages.PairCodeResponse', null, global);
goog.exportSymbol('proto.messages.ParticipantAction', null, global);
goog.exportSymbol('proto.messages.PhoneInfo', null, global);
goog.exportSymbol('proto.messages.PollMessage', null, global);
goog.exportSymbol('proto.messages.Presence', null, global);
goog.exportSymbol('proto.messages.PresenceRequest', null, global);
goog.exportSymbol('proto.messages.ProfileNameRequest', null, global);
@@ -1620,6 +1622,27 @@ if (goog.DEBUG && !COMPILED) {
*/
proto.messages.GetContactsRequest.displayName = 'proto.messages.GetContactsRequest';
}
/**
* Generated by JsPbCodeGenerator.
* @param {Array=} opt_data Optional initial data array, typically from a
* server response, or constructed directly in Javascript. The array is used
* in place and becomes part of the constructed object. It is not cloned.
* If no data is provided, the constructed object will be empty, but still
* valid.
* @extends {jspb.Message}
* @constructor
*/
proto.messages.ChatFilter = function(opt_data) {
jspb.Message.initialize(this, opt_data, 0, -1, proto.messages.ChatFilter.repeatedFields_, null);
};
goog.inherits(proto.messages.ChatFilter, jspb.Message);
if (goog.DEBUG && !COMPILED) {
/**
* @public
* @override
*/
proto.messages.ChatFilter.displayName = 'proto.messages.ChatFilter';
}
/**
* Generated by JsPbCodeGenerator.
* @param {Array=} opt_data Optional initial data array, typically from a
@@ -1788,6 +1811,27 @@ if (goog.DEBUG && !COMPILED) {
*/
proto.messages.GetLidsRequest.displayName = 'proto.messages.GetLidsRequest';
}
/**
* Generated by JsPbCodeGenerator.
* @param {Array=} opt_data Optional initial data array, typically from a
* server response, or constructed directly in Javascript. The array is used
* in place and becomes part of the constructed object. It is not cloned.
* If no data is provided, the constructed object will be empty, but still
* valid.
* @extends {jspb.Message}
* @constructor
*/
proto.messages.PollMessage = function(opt_data) {
jspb.Message.initialize(this, opt_data, 0, -1, proto.messages.PollMessage.repeatedFields_, null);
};
goog.inherits(proto.messages.PollMessage, jspb.Message);
if (goog.DEBUG && !COMPILED) {
/**
* @public
* @override
*/
proto.messages.PollMessage.displayName = 'proto.messages.PollMessage';
}
@@ -7903,7 +7947,8 @@ proto.messages.MessageRequest.toObject = function(includeInstance, msg) {
preview: (f = msg.getPreview()) && proto.messages.LinkPreview.toObject(includeInstance, f),
contactsList: jspb.Message.toObjectList(msg.getContactsList(),
proto.messages.vCardContact.toObject, includeInstance),
event: (f = msg.getEvent()) && proto.messages.EventMessage.toObject(includeInstance, f)
event: (f = msg.getEvent()) && proto.messages.EventMessage.toObject(includeInstance, f),
poll: (f = msg.getPoll()) && proto.messages.PollMessage.toObject(includeInstance, f)
};
if (includeInstance) {
@@ -8003,6 +8048,11 @@ proto.messages.MessageRequest.deserializeBinaryFromReader = function(msg, reader
reader.readMessage(value,proto.messages.EventMessage.deserializeBinaryFromReader);
msg.setEvent(value);
break;
case 15:
var value = new proto.messages.PollMessage;
reader.readMessage(value,proto.messages.PollMessage.deserializeBinaryFromReader);
msg.setPoll(value);
break;
default:
reader.skipField();
break;
@@ -8137,6 +8187,14 @@ proto.messages.MessageRequest.serializeBinaryToWriter = function(message, writer
proto.messages.EventMessage.serializeBinaryToWriter
);
}
f = message.getPoll();
if (f != null) {
writer.writeMessage(
15,
f,
proto.messages.PollMessage.serializeBinaryToWriter
);
}
};
@@ -8545,6 +8603,43 @@ proto.messages.MessageRequest.prototype.hasEvent = function() {
};
/**
* optional PollMessage poll = 15;
* @return {?proto.messages.PollMessage}
*/
proto.messages.MessageRequest.prototype.getPoll = function() {
return /** @type{?proto.messages.PollMessage} */ (
jspb.Message.getWrapperField(this, proto.messages.PollMessage, 15));
};
/**
* @param {?proto.messages.PollMessage|undefined} value
* @return {!proto.messages.MessageRequest} returns this
*/
proto.messages.MessageRequest.prototype.setPoll = function(value) {
return jspb.Message.setWrapperField(this, 15, value);
};
/**
* Clears the message field making it undefined.
* @return {!proto.messages.MessageRequest} returns this
*/
proto.messages.MessageRequest.prototype.clearPoll = function() {
return this.setPoll(undefined);
};
/**
* Returns whether this field is set.
* @return {boolean}
*/
proto.messages.MessageRequest.prototype.hasPoll = function() {
return jspb.Message.getField(this, 15) != null;
};
@@ -14794,7 +14889,9 @@ proto.messages.DownloadMediaRequest.prototype.toObject = function(opt_includeIns
proto.messages.DownloadMediaRequest.toObject = function(includeInstance, msg) {
var f, obj = {
session: (f = msg.getSession()) && proto.messages.Session.toObject(includeInstance, f),
message: jspb.Message.getFieldWithDefault(msg, 2, "")
message: jspb.Message.getFieldWithDefault(msg, 2, ""),
jid: jspb.Message.getFieldWithDefault(msg, 3, ""),
messageid: jspb.Message.getFieldWithDefault(msg, 4, "")
};
if (includeInstance) {
@@ -14840,6 +14937,14 @@ proto.messages.DownloadMediaRequest.deserializeBinaryFromReader = function(msg,
var value = /** @type {string} */ (reader.readString());
msg.setMessage(value);
break;
case 3:
var value = /** @type {string} */ (reader.readString());
msg.setJid(value);
break;
case 4:
var value = /** @type {string} */ (reader.readString());
msg.setMessageid(value);
break;
default:
reader.skipField();
break;
@@ -14884,6 +14989,20 @@ proto.messages.DownloadMediaRequest.serializeBinaryToWriter = function(message,
f
);
}
f = message.getJid();
if (f.length > 0) {
writer.writeString(
3,
f
);
}
f = message.getMessageid();
if (f.length > 0) {
writer.writeString(
4,
f
);
}
};
@@ -14942,6 +15061,42 @@ proto.messages.DownloadMediaRequest.prototype.setMessage = function(value) {
};
/**
* optional string jid = 3;
* @return {string}
*/
proto.messages.DownloadMediaRequest.prototype.getJid = function() {
return /** @type {string} */ (jspb.Message.getFieldWithDefault(this, 3, ""));
};
/**
* @param {string} value
* @return {!proto.messages.DownloadMediaRequest} returns this
*/
proto.messages.DownloadMediaRequest.prototype.setJid = function(value) {
return jspb.Message.setProto3StringField(this, 3, value);
};
/**
* optional string messageId = 4;
* @return {string}
*/
proto.messages.DownloadMediaRequest.prototype.getMessageid = function() {
return /** @type {string} */ (jspb.Message.getFieldWithDefault(this, 4, ""));
};
/**
* @param {string} value
* @return {!proto.messages.DownloadMediaRequest} returns this
*/
proto.messages.DownloadMediaRequest.prototype.setMessageid = function(value) {
return jspb.Message.setProto3StringField(this, 4, value);
};
@@ -16757,6 +16912,162 @@ proto.messages.GetContactsRequest.prototype.hasPagination = function() {
/**
* List of repeated fields within this message type.
* @private {!Array<number>}
* @const
*/
proto.messages.ChatFilter.repeatedFields_ = [1];
if (jspb.Message.GENERATE_TO_OBJECT) {
/**
* Creates an object representation of this proto.
* Field names that are reserved in JavaScript and will be renamed to pb_name.
* Optional fields that are not set will be set to undefined.
* To access a reserved field use, foo.pb_<name>, eg, foo.pb_default.
* For the list of reserved names please see:
* net/proto2/compiler/js/internal/generator.cc#kKeyword.
* @param {boolean=} opt_includeInstance Deprecated. whether to include the
* JSPB instance for transitional soy proto support:
* http://goto/soy-param-migration
* @return {!Object}
*/
proto.messages.ChatFilter.prototype.toObject = function(opt_includeInstance) {
return proto.messages.ChatFilter.toObject(opt_includeInstance, this);
};
/**
* Static version of the {@see toObject} method.
* @param {boolean|undefined} includeInstance Deprecated. Whether to include
* the JSPB instance for transitional soy proto support:
* http://goto/soy-param-migration
* @param {!proto.messages.ChatFilter} msg The msg instance to transform.
* @return {!Object}
* @suppress {unusedLocalVariables} f is only used for nested messages
*/
proto.messages.ChatFilter.toObject = function(includeInstance, msg) {
var f, obj = {
jidsList: (f = jspb.Message.getRepeatedField(msg, 1)) == null ? undefined : f
};
if (includeInstance) {
obj.$jspbMessageInstance = msg;
}
return obj;
};
}
/**
* Deserializes binary data (in protobuf wire format).
* @param {jspb.ByteSource} bytes The bytes to deserialize.
* @return {!proto.messages.ChatFilter}
*/
proto.messages.ChatFilter.deserializeBinary = function(bytes) {
var reader = new jspb.BinaryReader(bytes);
var msg = new proto.messages.ChatFilter;
return proto.messages.ChatFilter.deserializeBinaryFromReader(msg, reader);
};
/**
* Deserializes binary data (in protobuf wire format) from the
* given reader into the given message object.
* @param {!proto.messages.ChatFilter} msg The message object to deserialize into.
* @param {!jspb.BinaryReader} reader The BinaryReader to use.
* @return {!proto.messages.ChatFilter}
*/
proto.messages.ChatFilter.deserializeBinaryFromReader = function(msg, reader) {
while (reader.nextField()) {
if (reader.isEndGroup()) {
break;
}
var field = reader.getFieldNumber();
switch (field) {
case 1:
var value = /** @type {string} */ (reader.readString());
msg.addJids(value);
break;
default:
reader.skipField();
break;
}
}
return msg;
};
/**
* Serializes the message to binary data (in protobuf wire format).
* @return {!Uint8Array}
*/
proto.messages.ChatFilter.prototype.serializeBinary = function() {
var writer = new jspb.BinaryWriter();
proto.messages.ChatFilter.serializeBinaryToWriter(this, writer);
return writer.getResultBuffer();
};
/**
* Serializes the given message to binary data (in protobuf wire
* format), writing to the given BinaryWriter.
* @param {!proto.messages.ChatFilter} message
* @param {!jspb.BinaryWriter} writer
* @suppress {unusedLocalVariables} f is only used for nested messages
*/
proto.messages.ChatFilter.serializeBinaryToWriter = function(message, writer) {
var f = undefined;
f = message.getJidsList();
if (f.length > 0) {
writer.writeRepeatedString(
1,
f
);
}
};
/**
* repeated string jids = 1;
* @return {!Array<string>}
*/
proto.messages.ChatFilter.prototype.getJidsList = function() {
return /** @type {!Array<string>} */ (jspb.Message.getRepeatedField(this, 1));
};
/**
* @param {!Array<string>} value
* @return {!proto.messages.ChatFilter} returns this
*/
proto.messages.ChatFilter.prototype.setJidsList = function(value) {
return jspb.Message.setField(this, 1, value || []);
};
/**
* @param {string} value
* @param {number=} opt_index
* @return {!proto.messages.ChatFilter} returns this
*/
proto.messages.ChatFilter.prototype.addJids = function(value, opt_index) {
return jspb.Message.addToRepeatedField(this, 1, value, opt_index);
};
/**
* Clears the list making it empty but non-null.
* @return {!proto.messages.ChatFilter} returns this
*/
proto.messages.ChatFilter.prototype.clearJidsList = function() {
return this.setJidsList([]);
};
if (jspb.Message.GENERATE_TO_OBJECT) {
@@ -16790,7 +17101,8 @@ proto.messages.GetChatsRequest.toObject = function(includeInstance, msg) {
var f, obj = {
session: (f = msg.getSession()) && proto.messages.Session.toObject(includeInstance, f),
sortby: (f = msg.getSortby()) && proto.messages.SortBy.toObject(includeInstance, f),
pagination: (f = msg.getPagination()) && proto.messages.Pagination.toObject(includeInstance, f)
pagination: (f = msg.getPagination()) && proto.messages.Pagination.toObject(includeInstance, f),
filter: (f = msg.getFilter()) && proto.messages.ChatFilter.toObject(includeInstance, f)
};
if (includeInstance) {
@@ -16842,6 +17154,11 @@ proto.messages.GetChatsRequest.deserializeBinaryFromReader = function(msg, reade
reader.readMessage(value,proto.messages.Pagination.deserializeBinaryFromReader);
msg.setPagination(value);
break;
case 4:
var value = new proto.messages.ChatFilter;
reader.readMessage(value,proto.messages.ChatFilter.deserializeBinaryFromReader);
msg.setFilter(value);
break;
default:
reader.skipField();
break;
@@ -16895,6 +17212,14 @@ proto.messages.GetChatsRequest.serializeBinaryToWriter = function(message, write
proto.messages.Pagination.serializeBinaryToWriter
);
}
f = message.getFilter();
if (f != null) {
writer.writeMessage(
4,
f,
proto.messages.ChatFilter.serializeBinaryToWriter
);
}
};
@@ -17009,6 +17334,43 @@ proto.messages.GetChatsRequest.prototype.hasPagination = function() {
};
/**
* optional ChatFilter filter = 4;
* @return {?proto.messages.ChatFilter}
*/
proto.messages.GetChatsRequest.prototype.getFilter = function() {
return /** @type{?proto.messages.ChatFilter} */ (
jspb.Message.getWrapperField(this, proto.messages.ChatFilter, 4));
};
/**
* @param {?proto.messages.ChatFilter|undefined} value
* @return {!proto.messages.GetChatsRequest} returns this
*/
proto.messages.GetChatsRequest.prototype.setFilter = function(value) {
return jspb.Message.setWrapperField(this, 4, value);
};
/**
* Clears the message field making it undefined.
* @return {!proto.messages.GetChatsRequest} returns this
*/
proto.messages.GetChatsRequest.prototype.clearFilter = function() {
return this.setFilter(undefined);
};
/**
* Returns whether this field is set.
* @return {boolean}
*/
proto.messages.GetChatsRequest.prototype.hasFilter = function() {
return jspb.Message.getField(this, 4) != null;
};
@@ -18327,6 +18689,222 @@ proto.messages.GetLidsRequest.prototype.hasSession = function() {
};
/**
* List of repeated fields within this message type.
* @private {!Array<number>}
* @const
*/
proto.messages.PollMessage.repeatedFields_ = [2];
if (jspb.Message.GENERATE_TO_OBJECT) {
/**
* Creates an object representation of this proto.
* Field names that are reserved in JavaScript and will be renamed to pb_name.
* Optional fields that are not set will be set to undefined.
* To access a reserved field use, foo.pb_<name>, eg, foo.pb_default.
* For the list of reserved names please see:
* net/proto2/compiler/js/internal/generator.cc#kKeyword.
* @param {boolean=} opt_includeInstance Deprecated. whether to include the
* JSPB instance for transitional soy proto support:
* http://goto/soy-param-migration
* @return {!Object}
*/
proto.messages.PollMessage.prototype.toObject = function(opt_includeInstance) {
return proto.messages.PollMessage.toObject(opt_includeInstance, this);
};
/**
* Static version of the {@see toObject} method.
* @param {boolean|undefined} includeInstance Deprecated. Whether to include
* the JSPB instance for transitional soy proto support:
* http://goto/soy-param-migration
* @param {!proto.messages.PollMessage} msg The msg instance to transform.
* @return {!Object}
* @suppress {unusedLocalVariables} f is only used for nested messages
*/
proto.messages.PollMessage.toObject = function(includeInstance, msg) {
var f, obj = {
name: jspb.Message.getFieldWithDefault(msg, 1, ""),
optionsList: (f = jspb.Message.getRepeatedField(msg, 2)) == null ? undefined : f,
multipleanswers: jspb.Message.getBooleanFieldWithDefault(msg, 3, false)
};
if (includeInstance) {
obj.$jspbMessageInstance = msg;
}
return obj;
};
}
/**
* Deserializes binary data (in protobuf wire format).
* @param {jspb.ByteSource} bytes The bytes to deserialize.
* @return {!proto.messages.PollMessage}
*/
proto.messages.PollMessage.deserializeBinary = function(bytes) {
var reader = new jspb.BinaryReader(bytes);
var msg = new proto.messages.PollMessage;
return proto.messages.PollMessage.deserializeBinaryFromReader(msg, reader);
};
/**
* Deserializes binary data (in protobuf wire format) from the
* given reader into the given message object.
* @param {!proto.messages.PollMessage} msg The message object to deserialize into.
* @param {!jspb.BinaryReader} reader The BinaryReader to use.
* @return {!proto.messages.PollMessage}
*/
proto.messages.PollMessage.deserializeBinaryFromReader = function(msg, reader) {
while (reader.nextField()) {
if (reader.isEndGroup()) {
break;
}
var field = reader.getFieldNumber();
switch (field) {
case 1:
var value = /** @type {string} */ (reader.readString());
msg.setName(value);
break;
case 2:
var value = /** @type {string} */ (reader.readString());
msg.addOptions(value);
break;
case 3:
var value = /** @type {boolean} */ (reader.readBool());
msg.setMultipleanswers(value);
break;
default:
reader.skipField();
break;
}
}
return msg;
};
/**
* Serializes the message to binary data (in protobuf wire format).
* @return {!Uint8Array}
*/
proto.messages.PollMessage.prototype.serializeBinary = function() {
var writer = new jspb.BinaryWriter();
proto.messages.PollMessage.serializeBinaryToWriter(this, writer);
return writer.getResultBuffer();
};
/**
* Serializes the given message to binary data (in protobuf wire
* format), writing to the given BinaryWriter.
* @param {!proto.messages.PollMessage} message
* @param {!jspb.BinaryWriter} writer
* @suppress {unusedLocalVariables} f is only used for nested messages
*/
proto.messages.PollMessage.serializeBinaryToWriter = function(message, writer) {
var f = undefined;
f = message.getName();
if (f.length > 0) {
writer.writeString(
1,
f
);
}
f = message.getOptionsList();
if (f.length > 0) {
writer.writeRepeatedString(
2,
f
);
}
f = message.getMultipleanswers();
if (f) {
writer.writeBool(
3,
f
);
}
};
/**
* optional string name = 1;
* @return {string}
*/
proto.messages.PollMessage.prototype.getName = function() {
return /** @type {string} */ (jspb.Message.getFieldWithDefault(this, 1, ""));
};
/**
* @param {string} value
* @return {!proto.messages.PollMessage} returns this
*/
proto.messages.PollMessage.prototype.setName = function(value) {
return jspb.Message.setProto3StringField(this, 1, value);
};
/**
* repeated string options = 2;
* @return {!Array<string>}
*/
proto.messages.PollMessage.prototype.getOptionsList = function() {
return /** @type {!Array<string>} */ (jspb.Message.getRepeatedField(this, 2));
};
/**
* @param {!Array<string>} value
* @return {!proto.messages.PollMessage} returns this
*/
proto.messages.PollMessage.prototype.setOptionsList = function(value) {
return jspb.Message.setField(this, 2, value || []);
};
/**
* @param {string} value
* @param {number=} opt_index
* @return {!proto.messages.PollMessage} returns this
*/
proto.messages.PollMessage.prototype.addOptions = function(value, opt_index) {
return jspb.Message.addToRepeatedField(this, 2, value, opt_index);
};
/**
* Clears the list making it empty but non-null.
* @return {!proto.messages.PollMessage} returns this
*/
proto.messages.PollMessage.prototype.clearOptionsList = function() {
return this.setOptionsList([]);
};
/**
* optional bool multipleAnswers = 3;
* @return {boolean}
*/
proto.messages.PollMessage.prototype.getMultipleanswers = function() {
return /** @type {boolean} */ (jspb.Message.getBooleanFieldWithDefault(this, 3, false));
};
/**
* @param {boolean} value
* @return {!proto.messages.PollMessage} returns this
*/
proto.messages.PollMessage.prototype.setMultipleanswers = function(value) {
return jspb.Message.setProto3BooleanField(this, 3, value);
};
/**
* @enum {number}
*/
+145 -10
View File
@@ -64,6 +64,7 @@ import {
GetChatMessageQuery,
GetChatMessagesFilter,
GetChatMessagesQuery,
OverviewFilter,
ReadChatMessagesQuery,
ReadChatMessagesResponse,
} from '@waha/structures/chats.dto';
@@ -72,12 +73,12 @@ import {
CheckNumberStatusQuery,
EditMessageRequest,
MessageContactVcardRequest,
MessageDestination,
MessageFileRequest,
MessageForwardRequest,
MessageImageRequest,
MessageLinkCustomPreviewRequest,
MessageLocationRequest,
MessagePollRequest,
MessageReactionRequest,
MessageReplyRequest,
MessageTextRequest,
@@ -124,7 +125,13 @@ import {
DeleteStatusRequest,
TextStatus,
} from '@waha/structures/status.dto';
import { EnginePayload, WAMessageAckBody } from '@waha/structures/webhooks.dto';
import {
EnginePayload,
PollVotePayload,
WAMessageAckBody,
WAMessageEditedBody,
WAMessageRevokedBody,
} from '@waha/structures/webhooks.dto';
import { PaginatorInMemory } from '@waha/utils/Paginator';
import { sleep, waitUntil } from '@waha/utils/promiseTimeout';
import { onlyEvent } from '@waha/utils/reactive/ops/onlyEvent';
@@ -181,6 +188,8 @@ enum WhatsMeowEvent {
LABEL_ASSOCIATION_CHAT = 'events.LabelAssociationChat',
// Events
EVENT_MESSAGE_RESPONSE = 'gows.EventMessageResponse',
// Polls
POLL_VOTE_EVENT = 'gows.PollVoteEvent',
}
const gRPCClientConfig = {
@@ -400,6 +409,53 @@ export class WhatsappSessionGoWSCore extends WhatsappSession {
this.events2.get(WAHAEvents.MESSAGE).switch(messagesFromOthers$);
this.events2.get(WAHAEvents.MESSAGE_ANY).switch(messagesFromAll$);
// Handle revoked messages
const messagesRevoked$ = messages$.pipe(
filter((msg) => {
return (
msg?.Message?.protocolMessage?.type === 0 &&
msg?.Message?.protocolMessage?.key !== undefined
);
}),
mergeMap(async (message): Promise<WAMessageRevokedBody> => {
const afterMessage = await this.toWAMessage(message);
// Extract the revoked message ID from protocolMessage.key
const revokedMessageId = message.Message.protocolMessage.key?.ID;
return {
after: afterMessage,
before: null,
revokedMessageId: revokedMessageId,
_data: message,
};
}),
);
this.events2.get(WAHAEvents.MESSAGE_REVOKED).switch(messagesRevoked$);
// Handle edited messages
const messagesEdited$ = messages$.pipe(
filter((msg) => {
return (
msg?.Message?.protocolMessage?.type === 14 &&
msg?.Message?.protocolMessage?.editedMessage !== undefined
);
}),
mergeMap(async (message): Promise<WAMessageEditedBody> => {
const waMessage = await this.toWAMessage(message);
// Extract the body from editedMessage using extractBody function
const body =
this.extractBody(message.Message.protocolMessage.editedMessage) || '';
// Extract the original message ID from protocolMessage.key
const editedMessageId = message.Message.protocolMessage.key?.ID;
return {
...waMessage,
body: body,
editedMessageId: editedMessageId,
_data: message,
};
}),
);
this.events2.get(WAHAEvents.MESSAGE_EDITED).switch(messagesEdited$);
const receipt$ = all$.pipe(onlyEvent(WhatsMeowEvent.RECEIPT));
const messageAck$ = receipt$.pipe(
mergeMap(this.receiptToMessageAck.bind(this)),
@@ -472,7 +528,27 @@ export class WhatsappSessionGoWSCore extends WhatsappSession {
this.events2.get(WAHAEvents.LABEL_UPSERT).switch(labelUpsert$);
this.events2.get(WAHAEvents.LABEL_DELETED).switch(labelDeleted$);
// Handle event message response
//
// Polls
//
const pollVoteEvent$ = all$.pipe(
onlyEvent(WhatsMeowEvent.POLL_VOTE_EVENT),
map(this.toPollVotePayload.bind(this)),
filter(Boolean),
);
// Split into successful and failed responses
const [pollVoteSuccess$, pollVoteFailed$] = partition(
pollVoteEvent$,
(payload: PollVotePayload) => !!payload.vote.selectedOptions,
);
this.events2.get(WAHAEvents.POLL_VOTE).switch(pollVoteSuccess$);
this.events2.get(WAHAEvents.POLL_VOTE_FAILED).switch(pollVoteFailed$);
//
// Event Message
//
const eventMessageResponse$ = all$.pipe(
onlyEvent(WhatsMeowEvent.EVENT_MESSAGE_RESPONSE),
map(this.toEventResponsePayload.bind(this)),
@@ -490,6 +566,9 @@ export class WhatsappSessionGoWSCore extends WhatsappSession {
.get(WAHAEvents.EVENT_RESPONSE_FAILED)
.switch(eventResponseFailed$);
//
// Labels
//
// Handle label association events
const labelAssociationEvents$ = all$.pipe(
onlyEvent(WhatsMeowEvent.LABEL_ASSOCIATION_CHAT),
@@ -695,6 +774,23 @@ export class WhatsappSessionGoWSCore extends WhatsappSession {
return this.messageResponse(jid, data);
}
async sendPoll(request: MessagePollRequest) {
const jid = toJID(request.chatId);
const message = new messages.MessageRequest({
jid: jid,
session: this.session,
replyTo: getMessageIdFromSerialized(request.reply_to),
poll: new messages.PollMessage({
name: request.poll.name,
options: request.poll.options,
multipleAnswers: request.poll.multipleAnswers,
}),
});
const response = await promisify(this.client.SendMessage)(message);
const data = response.toObject();
return this.messageResponse(jid, data);
}
public async deleteMessage(chatId: string, messageId: string) {
const jid = toJID(this.ensureSuffix(chatId));
const key = parseMessageIdSerialized(messageId);
@@ -1167,13 +1263,13 @@ export class WhatsappSessionGoWSCore extends WhatsappSession {
}
public async getPresence(chatId: string): Promise<WAHAChatPresences> {
const remoteJid = toJID(chatId);
if (!(remoteJid in this.presences.keys())) {
await this.subscribePresence(remoteJid);
const jid = toJID(chatId);
await this.subscribePresence(jid);
if (!(jid in this.presences.keys())) {
await sleep(1000);
}
const result = this.presences.get(remoteJid) || [];
return this.toWahaPresences(remoteJid, result);
const result = this.presences.get(jid) || [];
return this.toWahaPresences(jid, result);
}
async subscribePresence(chatId: string) {
@@ -1468,6 +1564,7 @@ export class WhatsappSessionGoWSCore extends WhatsappSession {
*/
public async getChatsOverview(
pagination: PaginationParams,
filter?: OverviewFilter,
): Promise<ChatSummary[]> {
if (!pagination.sortBy) {
pagination.sortBy = 'timestamp';
@@ -1475,7 +1572,7 @@ export class WhatsappSessionGoWSCore extends WhatsappSession {
if (!pagination.sortOrder) {
pagination.sortOrder = SortOrder.DESC;
}
const chats = await this.getChats(pagination);
const chats = await this.getChats(pagination, filter);
const promises = [];
for (const chat of chats) {
@@ -1514,10 +1611,17 @@ export class WhatsappSessionGoWSCore extends WhatsappSession {
};
}
public async getChats(pagination: PaginationParams) {
public async getChats(
pagination: PaginationParams,
filter: OverviewFilter | null = null,
) {
if (pagination.sortBy === ChatSortField.CONVERSATION_TIMESTAMP) {
pagination.sortBy = 'timestamp';
}
let jids = [];
if (filter?.ids && filter.ids.length > 0) {
jids = filter.ids.map((id) => toJID(id));
}
const request = new messages.GetChatsRequest({
session: this.session,
pagination: new messages.Pagination({
@@ -1531,6 +1635,9 @@ export class WhatsappSessionGoWSCore extends WhatsappSession {
? messages.SortBy.Order.DESC
: messages.SortBy.Order.ASC,
}),
filter: new messages.ChatFilter({
jids: jids,
}),
});
const response = await promisify(this.client.GetChats)(request);
const data = parseJsonList(response);
@@ -1796,6 +1903,32 @@ export class WhatsappSessionGoWSCore extends WhatsappSession {
};
}
private toPollVotePayload(event: any): PollVotePayload {
// Extract event creation message key from the message
const creationKey = event.Message?.pollUpdateMessage.pollCreationMessageKey;
const key: WAMessageKey = {
remoteJid: creationKey.remoteJID,
fromMe: creationKey.fromMe,
id: creationKey.ID,
participant: creationKey.participant,
};
const fromToParticipant = getFromToParticipant(event);
const pollCreationKey = getDestination(key);
return {
poll: pollCreationKey,
vote: {
id: buildMessageId(event),
from: toCusFormat(fromToParticipant.from),
fromMe: event.Info.IsFromMe,
to: toCusFormat(fromToParticipant.to),
participant: toCusFormat(fromToParticipant.participant),
selectedOptions: event.Votes,
timestamp: event.Message.pollUpdateMessage.senderTimestampMS,
},
_data: event,
};
}
private toEventResponsePayload(event: any): EventResponsePayload {
const msg = this.toWAMessage(event);
let response: EventResponse | null = null;
@@ -2019,6 +2152,8 @@ export class GOWSEngineMediaProcessor implements IMediaEngineProcessor<any> {
// double "session" it's not a mistake here
session: this.session.session,
message: data,
jid: message.Info.Chat,
messageId: message.Info.ID,
});
const response = await promisify(this.session.client.DownloadMedia)(
request,
+82 -24
View File
@@ -90,6 +90,7 @@ import {
GetChatMessageQuery,
GetChatMessagesFilter,
GetChatMessagesQuery,
OverviewFilter,
PinDuration,
ReadChatMessagesQuery,
ReadChatMessagesResponse,
@@ -158,6 +159,7 @@ import {
PollVote,
PollVotePayload,
WAMessageAckBody,
WAMessageEditedBody,
WAMessageRevokedBody,
} from '@waha/structures/webhooks.dto';
import { LoggerBuilder } from '@waha/utils/logging';
@@ -386,7 +388,7 @@ export class WhatsappSessionNoWebCore extends WhatsappSession {
resubscribeToKnownPresences() {
for (const jid in this.store.presences) {
this.sock.presenceSubscribe(jid);
this.subscribePresence(jid);
}
}
@@ -468,7 +470,10 @@ export class WhatsappSessionNoWebCore extends WhatsappSession {
} else if (connection === 'open') {
this.qr.save('');
this.status = WAHASessionStatus.WORKING;
this.resubscribeToKnownPresences();
// Do we need to resubscribe?
// Ideally not, we need to explicitly call interesting
// jids every 1 minute
// this.resubscribeToKnownPresences();
return;
} else if (connection === 'close') {
this.qr.save('');
@@ -591,7 +596,8 @@ export class WhatsappSessionNoWebCore extends WhatsappSession {
if (
protocolMsg !== null &&
protocolMsg !== undefined &&
protocolMsg.editedMessage
protocolMsg.editedMessage &&
protocolMsg.key
) {
this.sock?.ev.emit('messages.update', [
{
@@ -616,7 +622,7 @@ export class WhatsappSessionNoWebCore extends WhatsappSession {
}
for (const message of messages) {
const content = normalizeMessageContent(message.message);
if (!content.pollUpdateMessage) {
if (!content?.pollUpdateMessage) {
continue;
}
const creationMsgKey = content.pollUpdateMessage.pollCreationMessageKey;
@@ -1127,8 +1133,17 @@ export class WhatsappSessionNoWebCore extends WhatsappSession {
public async getChatsOverview(
pagination: PaginationParams,
filter?: OverviewFilter,
): Promise<ChatSummary[]> {
const chats = await this.store.getChats(pagination, false);
// Convert customer format IDs to JID format if filter is provided
let jidFilter;
if (filter?.ids && filter.ids.length > 0) {
jidFilter = {
ids: filter.ids.map((id) => toJID(id)),
};
}
const chats = await this.store.getChats(pagination, false, jidFilter);
// Remove unreadCount, it's not ready yet
chats.forEach((chat) => delete chat.unreadCount);
@@ -1512,14 +1527,14 @@ export class WhatsappSessionNoWebCore extends WhatsappSession {
}
public async getPresence(chatId: string): Promise<WAHAChatPresences> {
const remoteJid = toJID(chatId);
if (!(remoteJid in this.store.presences)) {
this.store.presences[remoteJid] = {};
await this.sock.presenceSubscribe(remoteJid);
const jid = toJID(chatId);
await this.subscribePresence(jid);
if (!(jid in this.store.presences)) {
this.store.presences[jid] = {};
await sleep(1000);
}
const result = this.store.presences[remoteJid];
return this.toWahaPresences(remoteJid, result);
const result = this.store.presences[jid];
return this.toWahaPresences(jid, result);
}
public subscribePresence(id: string): Promise<void> {
@@ -1790,6 +1805,7 @@ export class WhatsappSessionNoWebCore extends WhatsappSession {
const messagesUpsert$ = fromEvent(this.sock.ev, 'messages.upsert').pipe(
map((event: BaileysEventMap['messages.upsert']) => event.messages),
mergeAll(),
share(),
);
let [messagesFromMe$, messagesFromOthers$] = partition(
messagesUpsert$,
@@ -1816,15 +1832,45 @@ export class WhatsappSessionNoWebCore extends WhatsappSession {
),
mergeMap(async (message): Promise<WAMessageRevokedBody> => {
const afterMessage = await this.toWAMessage(message);
// Extract the revoked message ID from protocolMessage.key
const revokedMessageId = message.message.protocolMessage.key?.id;
return {
after: afterMessage,
before: null,
revokedMessageId: revokedMessageId,
_data: message,
};
}),
);
this.events2.get(WAHAEvents.MESSAGE_REVOKED).switch(messagesRevoked$);
// Handle edited messages
// @ts-ignore
const messagesEdited$ = messagesUpsert$.pipe(
filter(
(message) =>
// @ts-ignore
message.message?.protocolMessage?.type ===
proto.Message.ProtocolMessage.Type.MESSAGE_EDIT &&
message.message?.protocolMessage?.editedMessage !== undefined,
),
mergeMap(async (message): Promise<WAMessageEditedBody> => {
const waMessage = await this.toWAMessage(message);
// Extract the body from editedMessage using extractBody function
const body =
this.extractBody(message.message.protocolMessage.editedMessage) || '';
// Extract the original message ID from protocolMessage.key
const editedMessageId = message.message.protocolMessage.key?.id;
return {
...waMessage,
body: body,
editedMessageId: editedMessageId,
_data: message,
};
}),
);
this.events2.get(WAHAEvents.MESSAGE_EDITED).switch(messagesEdited$);
//
// Message Reactions
//
@@ -1843,6 +1889,7 @@ export class WhatsappSessionNoWebCore extends WhatsappSession {
).pipe(
// @ts-ignore
mergeAll(),
share(),
);
const messageAckDirect$ = messageUpdates$.pipe(
filter(isMine), // ack comes only for MY messages
@@ -1853,6 +1900,7 @@ export class WhatsappSessionNoWebCore extends WhatsappSession {
fromEvent(this.sock.ev, 'message-receipt.update').pipe(
// @ts-ignore
mergeAll(),
share(),
);
const messageAckGroups$ = messageReceiptUpdate$.pipe(
@@ -1869,7 +1917,7 @@ export class WhatsappSessionNoWebCore extends WhatsappSession {
//
this.events2
.get(WAHAEvents.STATE_CHANGE)
.switch(fromEvent(this.sock.ev, 'connection.update'));
.switch(fromEvent(this.sock.ev, 'connection.update').pipe(share()));
const groupsUpsert$: Observable<GroupMetadata> = fromEvent(
this.sock.ev,
@@ -1877,6 +1925,7 @@ export class WhatsappSessionNoWebCore extends WhatsappSession {
).pipe(
// @ts-ignore
mergeAll(),
share(),
);
const groupsUpdate$: Observable<Partial<GroupMetadata>> = fromEvent(
this.sock.ev,
@@ -1884,11 +1933,12 @@ export class WhatsappSessionNoWebCore extends WhatsappSession {
).pipe(
// @ts-ignore
mergeAll(),
share(),
);
const groupsParticipantsUpdate$: Observable<any> = fromEvent(
this.sock.ev,
'group-participants.update',
);
).pipe(share());
this.events2.get(WAHAEvents.GROUP_JOIN).switch(groupsUpsert$);
@@ -1915,13 +1965,12 @@ export class WhatsappSessionNoWebCore extends WhatsappSession {
);
this.events2.get(WAHAEvents.GROUP_V2_LEAVE).switch(groupV2Leave$);
this.events2
.get(WAHAEvents.PRESENCE_UPDATE)
.switch(
fromEvent(this.sock.ev, 'presence.update').pipe(
map((data: any) => this.toWahaPresences(data.id, data.presences)),
),
);
this.events2.get(WAHAEvents.PRESENCE_UPDATE).switch(
fromEvent(this.sock.ev, 'presence.update').pipe(
map((data: any) => this.toWahaPresences(data.id, data.presences)),
share(),
),
);
//
// Poll votes
@@ -1948,7 +1997,7 @@ export class WhatsappSessionNoWebCore extends WhatsappSession {
//
// @ts-ignore
const calls$: Observable<WACallEvent[]> = fromEvent(this.sock.ev, 'call');
const call$ = calls$.pipe(mergeMap(identity));
const call$ = calls$.pipe(mergeMap(identity), share());
this.events2.get(WAHAEvents.CALL_RECEIVED).switch(
call$.pipe(
filter((call: WACallEvent) => call.status === 'offer'),
@@ -1981,7 +2030,7 @@ export class WhatsappSessionNoWebCore extends WhatsappSession {
const labelsEdit$: Observable<NOWEBLabel> = fromEvent(
this.sock.ev,
'labels.edit',
);
).pipe(share());
this.events2.get(WAHAEvents.LABEL_UPSERT).switch(
labelsEdit$.pipe(
exclude((data: NOWEBLabel) => data.deleted),
@@ -1994,7 +2043,10 @@ export class WhatsappSessionNoWebCore extends WhatsappSession {
map(this.toLabel.bind(this)),
),
);
const labelsAssociation$ = fromEvent(this.sock.ev, 'labels.association');
const labelsAssociation$ = fromEvent(
this.sock.ev,
'labels.association',
).pipe(share());
const labelsAssociationAdd$: Observable<ChatLabelAssociation> =
labelsAssociation$.pipe(
filter(({ type }: any) => type === 'add'),
@@ -2088,12 +2140,18 @@ export class WhatsappSessionNoWebCore extends WhatsappSession {
if (message.message.pollUpdateMessage) return;
// Ignore calls, we have dedicated handler for that
if (message.message.call?.callKey) return;
// Ignore revoke, we have a dedicated handler for that
// Ignore revoke, we have a dedicated event for that
if (
message.message?.protocolMessage?.type ===
proto.Message.ProtocolMessage.Type.REVOKE
)
return;
// Ignore edit, we have a dedicated event for that
if (
message.message?.protocolMessage?.type ===
proto.Message.ProtocolMessage.Type.MESSAGE_EDIT
)
return;
if (
message.message?.protocolMessage?.type ===
proto.Message.ProtocolMessage.Type.EPHEMERAL_SYNC_RESPONSE
@@ -1,4 +1,5 @@
import { Chat } from '@adiwajshing/baileys';
import { OverviewFilter } from '@waha/structures/chats.dto';
import { PaginationParams } from '@waha/structures/pagination.dto';
export interface IChatRepository {
@@ -9,6 +10,7 @@ export interface IChatRepository {
getAllWithMessages(
pagination: PaginationParams,
broadcast: boolean,
filter?: OverviewFilter,
): Promise<Chat[]>;
getById(id: string): Promise<Chat | null>;
+9 -2
View File
@@ -6,7 +6,10 @@ import {
} from '@adiwajshing/baileys';
import { GroupMetadata } from '@adiwajshing/baileys/lib/Types/GroupMetadata';
import { Label } from '@adiwajshing/baileys/lib/Types/Label';
import { GetChatMessagesFilter } from '@waha/structures/chats.dto';
import {
GetChatMessagesFilter,
OverviewFilter,
} from '@waha/structures/chats.dto';
import { LidToPhoneNumber } from '@waha/structures/lids.dto';
import {
LimitOffsetParams,
@@ -32,7 +35,11 @@ export interface INowebStore {
getMessageById(chatId: string, messageId: string): Promise<any>;
getChats(pagination: PaginationParams, broadcast: boolean): Promise<Chat[]>;
getChats(
pagination: PaginationParams,
broadcast: boolean,
filter?: OverviewFilter,
): Promise<Chat[]>;
getChat(jid: string): Promise<Chat | null>;
@@ -23,7 +23,10 @@ import { isLidUser } from '@adiwajshing/baileys/lib/WABinary/jid-utils';
import { IGroupRepository } from '@waha/core/engines/noweb/store/IGroupRepository';
import { ILabelAssociationRepository } from '@waha/core/engines/noweb/store/ILabelAssociationsRepository';
import { ILabelsRepository } from '@waha/core/engines/noweb/store/ILabelsRepository';
import { GetChatMessagesFilter } from '@waha/structures/chats.dto';
import {
GetChatMessagesFilter,
OverviewFilter,
} from '@waha/structures/chats.dto';
import { LidToPhoneNumber } from '@waha/structures/lids.dto';
import {
LimitOffsetParams,
@@ -61,11 +64,13 @@ export class NowebPersistentStore implements INowebStore {
public presences: any;
private lock: any = new AsyncLock({
timeout: 5_000,
maxPending: Infinity,
maxExecutionTime: 60_000,
});
private groupsFetchLock: any = new AsyncLock({
timeout: 5_000,
maxPending: Infinity,
maxExecutionTime: 60_000,
});
@@ -231,6 +236,9 @@ export class NowebPersistentStore implements INowebStore {
for (const update of updates) {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const jid = jidNormalizedUser(update.key.remoteJid!);
if (!update.key.id) {
continue;
}
const message = await this.messagesRepo.getByJidById(jid, update.key.id);
if (!message) {
this.logger.warn(
@@ -521,10 +529,14 @@ export class NowebPersistentStore implements INowebStore {
return this.messagesRepo.getByJidById(chatId, messageId);
}
getChats(pagination: PaginationParams, broadcast: boolean): Promise<Chat[]> {
getChats(
pagination: PaginationParams,
broadcast: boolean,
filter?: OverviewFilter,
): Promise<Chat[]> {
pagination.sortBy ||= 'conversationTimestamp';
pagination.sortOrder ||= SortOrder.DESC;
return this.chatRepo.getAllWithMessages(pagination, broadcast);
return this.chatRepo.getAllWithMessages(pagination, broadcast, filter);
}
async getChat(jid: string): Promise<Chat | null> {
@@ -1,5 +1,6 @@
import { Chat } from '@adiwajshing/baileys';
import { SqlKVRepository } from '@waha/core/storage/sql/SqlKVRepository';
import { OverviewFilter } from '@waha/structures/chats.dto';
import { PaginationParams } from '@waha/structures/pagination.dto';
export class SqlChatMethods {
@@ -8,15 +9,23 @@ export class SqlChatMethods {
async getAllWithMessages(
pagination: PaginationParams,
broadcast: boolean,
filter?: OverviewFilter,
): Promise<Chat[]> {
// Get chats with conversationTimestamp is not Null
let query = this.repository.select().whereNotNull('conversationTimestamp');
if (!broadcast) {
// filter out chat by id if it ends at @newsletter or @broadcast
query = query
.andWhereNot('id', 'like', '%@broadcast')
.andWhereNot('id', 'like', '%@newsletter');
}
// Filter by IDs if provided
if (filter?.ids && filter.ids.length > 0) {
query = query.whereIn('id', filter.ids);
}
query = this.repository.pagination(query, pagination);
return await this.repository.all(query);
}
@@ -1,6 +1,7 @@
import { Chat } from '@adiwajshing/baileys';
import { NowebChatSchema } from '@waha/core/engines/noweb/store/schemas';
import { SqlChatMethods } from '@waha/core/engines/noweb/store/sql/SqlChatMethods';
import { OverviewFilter } from '@waha/structures/chats.dto';
import { PaginationParams } from '@waha/structures/pagination.dto';
import { KnexPaginator } from '@waha/utils/Paginator';
@@ -28,7 +29,8 @@ export class Sqlite3ChatRepository
getAllWithMessages(
pagination: PaginationParams,
broadcast: boolean,
filter?: OverviewFilter,
): Promise<Chat[]> {
return this.methods.getAllWithMessages(pagination, broadcast);
return this.methods.getAllWithMessages(pagination, broadcast, filter);
}
}
@@ -1,20 +1,18 @@
import { Schema } from '@waha/core/storage/Schema';
// eslint-disable-next-line @typescript-eslint/no-var-requires
const Database = require('better-sqlite3');
import Knex from 'knex';
export class Sqlite3SchemaValidation {
constructor(
private table: Schema,
private db,
private knex: Knex.Knex,
) {}
validate() {
async validate() {
const table = this.table;
// Check table has the columns
const columns = this.db.prepare(`PRAGMA table_info(${table.name})`).all();
// Check exact number of columns
const columns = await this.knex.raw(`PRAGMA table_info(${table.name})`);
// Check the exact number of columns
if (columns.length !== table.columns.length) {
throw new Error(
`Table '${table.name}' does not have expected number of columns. Expected ${table.columns.length}, got ${columns.length}`,
@@ -37,7 +35,7 @@ export class Sqlite3SchemaValidation {
}
// Check table has expected indexes
const indexes = this.db.prepare(`PRAGMA index_list(${table.name})`).all();
const indexes = await this.knex.raw(`PRAGMA index_list(${table.name})`);
const indexNames = indexes.map((index) => index.name);
for (const index of table.indexes) {
if (!indexNames.includes(index.name)) {
@@ -6,6 +6,7 @@ import { Sqlite3LabelAssociationsRepository } from '@waha/core/engines/noweb/sto
import { Sqlite3LabelsRepository } from '@waha/core/engines/noweb/store/sqlite3/Sqlite3LabelsRepository';
import { Sqlite3LidPNRepository } from '@waha/core/engines/noweb/store/sqlite3/Sqlite3LidPNRepository';
import { Schema } from '@waha/core/storage/Schema';
import Knex from 'knex';
import { INowebStorage } from '../INowebStorage';
import { Migrations, NOWEB_STORE_SCHEMA } from '../schemas';
@@ -14,70 +15,71 @@ import { Sqlite3ContactRepository } from './Sqlite3ContactRepository';
import { Sqlite3MessagesRepository } from './Sqlite3MessagesRepository';
import { Sqlite3SchemaValidation } from './Sqlite3SchemaValidation';
// eslint-disable-next-line @typescript-eslint/no-var-requires
const Database = require('better-sqlite3');
export class Sqlite3Storage extends INowebStorage {
private readonly db: any;
private readonly tables: Schema[];
private readonly knex: Knex.Knex;
constructor(filePath: string) {
super();
this.db = new Database(filePath);
this.knex = Knex({
client: 'better-sqlite3',
connection: { filename: filePath },
useNullAsDefault: true,
});
this.tables = NOWEB_STORE_SCHEMA;
}
async init() {
this.db.pragma('journal_mode = WAL;');
this.migrate();
this.validateSchema();
await this.knex.raw('PRAGMA journal_mode = WAL;');
await this.migrate();
await this.validateSchema();
}
private migrate() {
this.migration0001init();
private async migrate() {
await this.migration0001init();
}
private validateSchema() {
private async validateSchema() {
for (const table of this.tables) {
new Sqlite3SchemaValidation(table, this.db).validate();
await new Sqlite3SchemaValidation(table, this.knex).validate();
}
}
private migration0001init() {
private async migration0001init() {
for (const migration of Migrations) {
this.db.exec(migration);
await this.knex.raw(migration);
}
}
async close() {
return this.db.close();
return this.knex.destroy();
}
getContactsRepository() {
return new Sqlite3ContactRepository(this.db);
return new Sqlite3ContactRepository(this.knex);
}
getChatRepository() {
return new Sqlite3ChatRepository(this.db);
return new Sqlite3ChatRepository(this.knex);
}
getGroupRepository() {
return new Sqlite3GroupRepository(this.db);
return new Sqlite3GroupRepository(this.knex);
}
getLabelsRepository(): ILabelsRepository {
return new Sqlite3LabelsRepository(this.db);
return new Sqlite3LabelsRepository(this.knex);
}
getLabelAssociationRepository(): ILabelAssociationRepository {
return new Sqlite3LabelAssociationsRepository(this.db);
return new Sqlite3LabelAssociationsRepository(this.knex);
}
getMessagesRepository() {
return new Sqlite3MessagesRepository(this.db);
return new Sqlite3MessagesRepository(this.knex);
}
getLidPNRepository(): INowebLidPNRepository {
return new Sqlite3LidPNRepository(this.db);
return new Sqlite3LidPNRepository(this.knex);
}
}
@@ -19,6 +19,7 @@ const AsyncLock = require('async-lock');
// Default pending is 1000, set it to infinity
// https://github.com/rogierschouten/async-lock/issues/63
const fileLock = new AsyncLock({
timeout: 5_000,
maxPending: Infinity,
maxExecutionTime: 30_000,
});
+21
View File
@@ -0,0 +1,21 @@
/**
* Expose a function to the page if it does not exist
*
* NOTE:
* Rewrite it to 'upsertFunction' after updating Puppeteer to 20.6 or higher
* using page.removeExposedFunction
* https://pptr.dev/api/puppeteer.page.removeExposedFunction
*
* @param {import(puppeteer).Page} page
* @param {string} name
* @param {Function} fn
*/
export async function exposeFunctionIfAbsent(page, name, fn) {
const exist = await page.evaluate((name) => {
return !!window[name];
}, name);
if (exist) {
return;
}
await page.exposeFunction(name, fn);
}
+16
View File
@@ -99,6 +99,18 @@ export class WPage extends EventEmitter {
}
}
async reload(options?: any): Promise<any> {
try {
return await this.page.reload(options);
} catch (err) {
this.emit(PAGE_CALL_ERROR_EVENT, {
method: 'reload',
error: err,
});
throw err;
}
}
on(event: string | symbol, listener: (...args: any[]) => void): this {
if (event === PAGE_CALL_ERROR_EVENT) {
super.on(event, listener);
@@ -107,4 +119,8 @@ export class WPage extends EventEmitter {
this.page.on(event, listener);
return this.page as any;
}
get tracing() {
return this.page.tracing;
}
}
+83 -6
View File
@@ -1,14 +1,16 @@
import { exposeFunctionIfAbsent } from '@waha/core/engines/webjs/Puppeteer';
import { WebJSPresence } from '@waha/core/engines/webjs/types';
import { GetChatMessagesFilter } from '@waha/structures/chats.dto';
import { Label } from '@waha/structures/labels.dto';
import { LidToPhoneNumber } from '@waha/structures/lids.dto';
import { PaginationParams } from '@waha/structures/pagination.dto';
import { TextStatus } from '@waha/structures/status.dto';
import { sleep } from '@waha/utils/promiseTimeout';
import { EventEmitter } from 'events';
import * as lodash from 'lodash';
import { Page } from 'puppeteer';
import { Client, Events } from 'whatsapp-web.js';
import { Message } from 'whatsapp-web.js/src/structures';
import { exposeFunctionIfAbsent } from 'whatsapp-web.js/src/util/Puppeter';
import { CallErrorEvent, PAGE_CALL_ERROR_EVENT, WPage } from './WPage';
@@ -55,6 +57,26 @@ export class WebjsClientCore extends Client {
await this.pupPage.evaluate(LoadWAHA);
}
/**
* @result indicating whether the UX fresh look was successfully hidden.
*/
hideUXFreshLook(): Promise<boolean> {
return this.pupPage.evaluate(() => {
const WAWebUserPrefsUiRefresh = window.require('WAWebUserPrefsUiRefresh');
if (!WAWebUserPrefsUiRefresh) {
return false;
}
if (WAWebUserPrefsUiRefresh.getUiRefreshNuxAcked()) {
return false;
}
WAWebUserPrefsUiRefresh.incrementNuxViewCount();
WAWebUserPrefsUiRefresh.setUiRefreshNuxAcked(true);
const WAWebModalManager = window.require('WAWebModalManager');
WAWebModalManager.ModalManager.close();
return true;
});
}
async attachCustomEventListeners() {
await exposeFunctionIfAbsent(
this.pupPage,
@@ -137,7 +159,7 @@ export class WebjsClientCore extends Client {
}, label);
}
async getChats(pagination?: PaginationParams) {
async getChats(pagination?: PaginationParams, filter?: { ids?: string[] }) {
if (lodash.isEmpty(pagination)) {
return await super.getChats();
}
@@ -146,10 +168,14 @@ export class WebjsClientCore extends Client {
pagination.limit ||= Infinity;
pagination.offset ||= 0;
const chats = await this.pupPage.evaluate(async (pagination) => {
// @ts-ignore
return await window.WAHA.getChats(pagination);
}, pagination);
const chats = await this.pupPage.evaluate(
async (pagination, filter) => {
// @ts-ignore
return await window.WAHA.getChats(pagination, filter);
},
pagination,
filter,
);
return chats.map((chat) => ChatFactory.create(this, chat));
}
@@ -336,4 +362,55 @@ export class WebjsClientCore extends Client {
}, phoneNumber)) as any;
return lid;
}
/**
* Presences methods
*/
public async subscribePresence(chatId: string): Promise<void> {
await this.pupPage.evaluate(async (chatId) => {
const d = require;
const WidFactory = d('WAWebWidFactory');
const wid = WidFactory.createWidFromWidLike(chatId);
const chat = d('WAWebChatCollection').ChatCollection.get(wid);
const tc = chat == null ? void 0 : chat.getTcToken();
await d('WAWebContactPresenceBridge').subscribePresence(wid, tc);
}, chatId);
}
private async getCurrentPresence(chatId: string): Promise<WebJSPresence[]> {
const result = await this.pupPage.evaluate(async (chatId) => {
const d = require;
const WidFactory = d('WAWebWidFactory');
const PresenceCollection = d(
'WAWebPresenceCollection',
).PresenceCollection;
const wid = WidFactory.createWidFromWidLike(chatId);
const presence = PresenceCollection.get(wid);
if (!presence) {
return [];
}
let chatstates = [];
if (chatId.endsWith('@c.us')) {
chatstates = [presence.chatstate];
} else {
chatstates = presence.chatstates.getModelsArray();
}
return chatstates.map((chatstate) => {
return {
participant: chatstate.id._serialized,
lastSeen: chatstate.t,
state: chatstate.type,
};
});
}, chatId);
return result;
}
public async getPresence(chatId: string): Promise<WebJSPresence[]> {
await this.sendPresenceAvailable();
await this.subscribePresence(chatId);
await sleep(3_000);
return await this.getCurrentPresence(chatId);
}
}
+7 -1
View File
@@ -4,8 +4,14 @@ exports.LoadWAHA = () => {
'WAWebBizLabelEditingAction',
);
window.WAHA.getChats = async (pagination) => {
window.WAHA.getChats = async (pagination, filter) => {
let chats = window.Store.Chat.getModelsArray().slice();
// Filter chats by IDs if filter is provided
if (filter && filter.ids && filter.ids.length > 0) {
chats = chats.filter((chat) => filter.ids.includes(chat.id._serialized));
}
const paginator = new window.Paginator(pagination);
chats = paginator.apply(chats);
const chatPromises = chats.map((chat) => window.WWebJS.getChatModel(chat));
+1 -1
View File
@@ -23,7 +23,7 @@ interface Me {
lid?: string;
}
function jid(field: any) {
export function jid(field: any) {
if (!field) {
return field;
}
+67
View File
@@ -0,0 +1,67 @@
import { BinaryNode, WAPresence } from '@adiwajshing/baileys';
import { toCusFormat } from '@waha/core/engines/noweb/session.noweb.core';
import { WAHAPresenceStatus } from '@waha/structures/enums.dto';
import { WAHAChatPresences } from '@waha/structures/presence.dto';
import { jid } from './ack.webjs';
export function TagPresenceToPresence(node: BinaryNode): WAHAChatPresences {
const { attrs } = node;
const id = jid(attrs.from);
const state =
attrs.type === 'unavailable'
? WAHAPresenceStatus.OFFLINE
: WAHAPresenceStatus.ONLINE;
const lastSeen = attrs.last && attrs.last !== 'deny' ? +attrs.last : null;
return {
id: toCusFormat(id),
presences: [
{
participant: toCusFormat(id),
lastKnownPresence: state,
lastSeen: lastSeen,
},
],
};
}
export function TagChatstateToPresence(node: BinaryNode): WAHAChatPresences {
const { attrs, content } = node;
const id = jid(attrs.from);
const participant = jid(attrs.participant) || jid(attrs.from);
const firstChild = content[0] as BinaryNode;
const type = firstChild.tag as WAPresence;
let status = WAHAPresenceStatus.OFFLINE;
switch (type) {
case 'unavailable':
status = WAHAPresenceStatus.OFFLINE;
break;
case 'available':
status = WAHAPresenceStatus.ONLINE;
break;
case 'paused':
status = WAHAPresenceStatus.PAUSED;
break;
case 'composing':
status = WAHAPresenceStatus.TYPING;
break;
}
if (firstChild.attrs?.media === 'audio') {
status = WAHAPresenceStatus.RECORDING;
}
return {
id: toCusFormat(id),
presences: [
{
participant: toCusFormat(participant),
lastKnownPresence: status,
lastSeen: null,
},
],
};
}
+155 -14
View File
@@ -19,6 +19,10 @@ import {
ToGroupV2UpdateEvent,
} from '@waha/core/engines/webjs/groups.webjs';
import { LocalAuth } from '@waha/core/engines/webjs/LocalAuth';
import {
TagChatstateToPresence,
TagPresenceToPresence,
} from '@waha/core/engines/webjs/presence';
import { WebjsClientCore } from '@waha/core/engines/webjs/WebjsClientCore';
import {
CallErrorEvent,
@@ -55,6 +59,7 @@ import {
GetChatMessageQuery,
GetChatMessagesFilter,
GetChatMessagesQuery,
OverviewFilter,
ReadChatMessagesQuery,
ReadChatMessagesResponse,
} from '@waha/structures/chats.dto';
@@ -97,23 +102,27 @@ import { LidToPhoneNumber } from '@waha/structures/lids.dto';
import { ReplyToMessage } from '@waha/structures/message.dto';
import { PaginationParams, SortOrder } from '@waha/structures/pagination.dto';
import {
MessageSource,
WAMessage,
WAMessageReaction,
} from '@waha/structures/responses.dto';
WAHAChatPresences,
WAHAPresenceData,
} from '@waha/structures/presence.dto';
import { WAMessage, WAMessageReaction } from '@waha/structures/responses.dto';
import { BrowserTraceQuery } from '@waha/structures/server.debug.dto';
import { MeInfo } from '@waha/structures/sessions.dto';
import { StatusRequest, TextStatus } from '@waha/structures/status.dto';
import {
EnginePayload,
WAMessageAckBody,
WAMessageEditedBody,
WAMessageRevokedBody,
} from '@waha/structures/webhooks.dto';
import { PaginatorInMemory } from '@waha/utils/Paginator';
import { sleep, waitUntil } from '@waha/utils/promiseTimeout';
import { SingleDelayedJobRunner } from '@waha/utils/SingleDelayedJobRunner';
import { TmpDir } from '@waha/utils/tmpdir';
import * as lodash from 'lodash';
import * as path from 'path';
import { ProtocolError } from 'puppeteer';
import { filter, fromEvent, merge, mergeMap, Observable } from 'rxjs';
import { filter, fromEvent, merge, mergeMap, Observable, share } from 'rxjs';
import { map } from 'rxjs/operators';
import {
AuthStrategy,
@@ -128,13 +137,14 @@ import {
Label as WEBJSLabel,
Location,
Message,
MessageAck,
MessageMedia,
Reaction,
WAState,
} from 'whatsapp-web.js';
import { Message as MessageInstance } from 'whatsapp-web.js/src/structures';
import { WAJSPresenceChatStateType, WebJSPresence } from './types';
export interface WebJSConfig {
webVersion?: string;
cacheType: 'local' | 'none';
@@ -187,13 +197,19 @@ export class WhatsappSessionWebJSCore extends WhatsappSession {
if (cacheType === 'local') {
this.logger.info(`Using web version: '${webVersion}'`);
}
const args = this.getBrowserArgsForPuppeteer();
// add at the start
args.unshift(`--a-waha-timestamp=${new Date()}`);
args.unshift(`--a-waha-session=${this.name}`);
return {
puppeteer: {
headless: true,
executablePath: this.getBrowserExecutablePath(),
args: this.getBrowserArgsForPuppeteer(),
args: args,
dumpio: this.isDebugEnabled(),
},
userAgent:
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36',
webVersion: webVersion,
webVersionCache: {
type: cacheType,
@@ -261,7 +277,11 @@ export class WhatsappSessionWebJSCore extends WhatsappSession {
.then(() => {
// Listen for browser disconnected event
this.whatsapp.pupBrowser.on('disconnected', () => {
this.logger.error('The browser has been disconnected');
if (this.shouldRestart) {
this.logger.error('The browser has been disconnected');
} else {
this.logger.info('The browser has been disconnected');
}
this.failed();
});
@@ -426,6 +446,22 @@ export class WhatsappSessionWebJSCore extends WhatsappSession {
this.logger.info(`Session '${this.name}' is ready!`);
});
//
// Temp fix for hiding "Fresh look" modal
// https://github.com/devlikeapro/waha/issues/987
//
this.whatsapp.on(Events.READY, async () => {
try {
const hidden = await this.whatsapp.hideUXFreshLook();
if (hidden) {
this.logger.info('"Fresh look" modal has been hidden');
}
} catch (err) {
this.logger.warn('Failed to hide "Fresh look" modal');
this.logger.warn(err, err.stack);
}
});
this.whatsapp.on(Events.AUTHENTICATED, (args) => {
this.qr.save('');
this.logger.info({ args: args }, `Session has been authenticated!`);
@@ -486,6 +522,23 @@ export class WhatsappSessionWebJSCore extends WhatsappSession {
/**
* START - Methods for API
*/
public async browserTrace(query: BrowserTraceQuery): Promise<string> {
const tmpdir = new TmpDir(
this.logger,
`waha-browser-trace-${this.name}-`,
(10 * query.seconds + 120) * 1000,
);
const page = this.whatsapp.pupPage;
return await tmpdir.use(async (dir) => {
this.logger.info({ query }, `Starting browser tracing...`);
const filepath = path.join(dir, 'trace.json');
await page.tracing.start({ path: filepath });
await sleep(query.seconds * 1000);
await page.tracing.stop();
this.logger.info(`Browser tracing finished, saved to ${filepath}`);
return filepath;
});
}
/**
* Auth methods
@@ -678,7 +731,7 @@ export class WhatsappSessionWebJSCore extends WhatsappSession {
/**
* Chats methods
*/
getChats(pagination: PaginationParams) {
getChats(pagination: PaginationParams, filter: OverviewFilter | null = null) {
switch (pagination.sortBy) {
case ChatSortField.ID:
pagination.sortBy = 'id._serialized';
@@ -687,18 +740,19 @@ export class WhatsappSessionWebJSCore extends WhatsappSession {
pagination.sortBy = 't';
break;
}
return this.whatsapp.getChats(pagination);
return this.whatsapp.getChats(pagination, filter);
}
public async getChatsOverview(
pagination: PaginationParams,
filter?: OverviewFilter,
): Promise<ChatSummary[]> {
pagination = {
...pagination,
sortBy: ChatSortField.CONVERSATION_TIMESTAMP,
sortOrder: SortOrder.DESC,
};
const chats = await this.getChats(pagination);
const chats = await this.getChats(pagination, filter);
const promises = [];
for (const chat of chats) {
@@ -1262,6 +1316,52 @@ export class WhatsappSessionWebJSCore extends WhatsappSession {
}
}
public getPresences(): Promise<WAHAChatPresences[]> {
throw new NotImplementedByEngineError();
}
public async getPresence(id: string): Promise<WAHAChatPresences> {
const chatId = toCusFormat(id);
const presences = await this.whatsapp.getPresence(chatId);
return this.toWahaPresences(chatId, presences);
}
public async subscribePresence(id: string): Promise<any> {
const chatId = toCusFormat(id);
await this.whatsapp.subscribePresence(chatId);
}
private toWahaPresences(
chatId: string,
data: WebJSPresence[],
): WAHAChatPresences {
const presences: WAHAPresenceData[] = data.map((presence) => {
let status: WAHAPresenceStatus = WAHAPresenceStatus.OFFLINE;
switch (presence.state) {
case WAJSPresenceChatStateType.AVAILABLE:
status = WAHAPresenceStatus.ONLINE;
break;
case WAJSPresenceChatStateType.UNAVAILABLE:
status = WAHAPresenceStatus.OFFLINE;
break;
case WAJSPresenceChatStateType.TYPING:
status = WAHAPresenceStatus.TYPING;
break;
case WAJSPresenceChatStateType.RECORDING_AUDIO:
status = WAHAPresenceStatus.RECORDING;
}
return {
participant: presence.participant,
lastSeen: presence.lastSeen || null,
lastKnownPresence: status,
};
});
return {
id: toCusFormat(chatId),
presences: presences,
};
}
/**
* Status methods
*/
@@ -1345,9 +1445,12 @@ export class WhatsappSessionWebJSCore extends WhatsappSession {
const beforeMessage = event.before
? this.toWAMessage(event.before)
: null;
// Extract the revoked message ID from the protocolMessageKey.id field
const revokedMessageId = afterMessage?._data?.protocolMessageKey?.id;
return {
after: afterMessage,
before: beforeMessage,
revokedMessageId: revokedMessageId,
};
}),
);
@@ -1359,6 +1462,26 @@ export class WhatsappSessionWebJSCore extends WhatsappSession {
);
this.events2.get(WAHAEvents.MESSAGE_REACTION).switch(messagesReaction$);
const messageEdit$ = fromEvent(
this.whatsapp,
Events.MESSAGE_EDIT,
(message, newBody, prevBody) => {
return { message, newBody, prevBody };
},
);
const messagesEdit$ = messageEdit$.pipe(
map((event): WAMessageEditedBody => {
const message = this.toWAMessage(event.message);
return {
...message,
body: event.newBody,
editedMessageId: message._data?.id?.id,
_data: event,
};
}),
);
this.events2.get(WAHAEvents.MESSAGE_EDITED).switch(messagesEdit$);
const messageAckWEBJS$ = fromEvent(
this.whatsapp,
Events.MESSAGE_ACK,
@@ -1391,6 +1514,22 @@ export class WhatsappSessionWebJSCore extends WhatsappSession {
const stateChanged$ = fromEvent(this.whatsapp, Events.STATE_CHANGED);
this.events2.get(WAHAEvents.STATE_CHANGE).switch(stateChanged$);
//
// Presence
//
const tagPresenceNode$ = fromEvent(this.whatsapp, Events.TAG_PRESENCE);
const presences$ = tagPresenceNode$.pipe(
map(TagPresenceToPresence),
filter(Boolean),
);
const tagChatstateNode$ = fromEvent(this.whatsapp, 'tag:chatstate');
const chatstatePresences$ = tagChatstateNode$.pipe(
map(TagChatstateToPresence),
filter(Boolean),
);
const presenceUpdate$ = merge(presences$, chatstatePresences$);
this.events2.get(WAHAEvents.PRESENCE_UPDATE).switch(presenceUpdate$);
//
// Groups
//
@@ -1588,10 +1727,12 @@ export class WhatsappSessionWebJSCore extends WhatsappSession {
}
public async getEngineInfo() {
// Add 1 seconds timeout
if (!this.whatsapp || !this.whatsapp.pupPage) {
return null;
}
return {
WWebVersion: await this.whatsapp?.getWWebVersion(),
state: await this.whatsapp?.getState(),
WWebVersion: await this.whatsapp.getWWebVersion(),
state: await this.whatsapp.getState(),
};
}
+12
View File
@@ -0,0 +1,12 @@
export enum WAJSPresenceChatStateType {
AVAILABLE = 'available', // User is online
UNAVAILABLE = 'unavailable', // User went offline (may include `t`)
TYPING = 'typing', // User is typing text
RECORDING_AUDIO = 'recording_audio', // User is recording a voice message
}
export interface WebJSPresence {
participant: string;
lastSeen?: number;
state: WAJSPresenceChatStateType;
}
+5 -2
View File
@@ -1,8 +1,11 @@
import { UnprocessableEntityException } from '@nestjs/common';
import {
NotImplementedException,
UnprocessableEntityException,
} from '@nestjs/common';
export const DOCS_URL = 'https://waha.devlike.pro/';
export class NotImplementedByEngineError extends UnprocessableEntityException {
export class NotImplementedByEngineError extends NotImplementedException {
constructor(msg = '') {
let error = 'The method is not implemented by the engine.';
if (msg) {
+16
View File
@@ -0,0 +1,16 @@
import { AvailableInPlusVersion } from '../exceptions';
export interface IMediaConverter {
voice(content: Buffer): Promise<Buffer>;
video(content: Buffer): Promise<Buffer>;
}
export class CoreMediaConverter implements IMediaConverter {
video(content: Buffer): Promise<Buffer> {
throw new AvailableInPlusVersion();
}
voice(content: Buffer): Promise<Buffer> {
throw new AvailableInPlusVersion();
}
}
+4
View File
@@ -0,0 +1,4 @@
export enum WAMimeType {
VOICE = 'audio/ogg; codecs=opus',
VIDEO = 'video/mp4',
}
+2 -2
View File
@@ -4,8 +4,8 @@ import * as fsp from 'fs/promises';
import * as path from 'path';
import { Logger } from 'pino';
import fs = require('fs');
import del = require('del');
import { fileExists } from '@waha/utils/files';
import { deleteAsync } from 'del';
// eslint-disable-next-line @typescript-eslint/no-var-requires
const writeFileAtomic = require('write-file-atomic');
@@ -59,7 +59,7 @@ export class MediaLocalStorage implements IMediaStorage {
}
if (fs.existsSync(this.filesFolder)) {
del([`${this.filesFolder}/*`], { force: true }).then((paths) => {
deleteAsync([`${this.filesFolder}/*`], { force: true }).then((paths) => {
if (paths.length === 0) {
return;
}
+3 -1
View File
@@ -1,3 +1,5 @@
import Knex from 'knex';
import { DataStore } from '../abc/DataStore';
export abstract class LocalStore extends DataStore {
@@ -23,5 +25,5 @@ export abstract class LocalStore extends DataStore {
*/
abstract getFilePath(session: string, file: string): string;
abstract getWAHADatabase(): any;
abstract getWAHADatabase(): Knex.Knex;
}
+27 -18
View File
@@ -1,19 +1,16 @@
import * as crypto from 'crypto';
import { safeJoin } from '@waha/utils/files';
import * as fs from 'fs/promises';
import * as os from 'os';
import Knex from 'knex';
import * as path from 'path';
import { LocalStore } from './LocalStore';
// eslint-disable-next-line @typescript-eslint/no-var-requires
const Database = require('better-sqlite3');
export class LocalStoreCore extends LocalStore {
protected readonly baseDirectory: string =
process.env.WAHA_LOCAL_STORE_BASE_DIR || './.sessions';
private readonly engine: string;
private db: any;
private knex: Knex.Knex;
constructor(engine: string) {
super();
@@ -22,6 +19,11 @@ export class LocalStoreCore extends LocalStore {
async init(sessionName?: string) {
await fs.mkdir(this.getEngineDirectory(), { recursive: true });
if (!this.knex) {
this.knex = this.buildKnex();
await this.knex.raw('PRAGMA journal_mode = WAL;');
await this.knex.raw('PRAGMA foreign_keys = ON;');
}
if (sessionName) {
await fs.mkdir(this.getSessionDirectory(sessionName), {
recursive: true,
@@ -33,14 +35,14 @@ export class LocalStoreCore extends LocalStore {
* Get the directory where all the engines and sessions are stored
*/
getBaseDirectory() {
return path.join(this.baseDirectory);
return path.resolve(this.baseDirectory);
}
/**
* Get the directory where the engine sessions are stored
*/
getEngineDirectory() {
return path.join(this.baseDirectory, this.engine);
return safeJoin(this.baseDirectory, this.engine);
}
getSessionDirectory(name: string): string {
@@ -48,24 +50,31 @@ export class LocalStoreCore extends LocalStore {
}
getFilePath(session: string, file: string): string {
return path.join(this.getSessionDirectory(session), file);
return safeJoin(this.getSessionDirectory(session), file);
}
protected getDirectoryPath(name: string): string {
return path.join(this.getEngineDirectory(), name);
return safeJoin(this.getEngineDirectory(), name);
}
getWAHADatabase(): any {
if (!this.db) {
const engineDir = this.getEngineDirectory();
const database = path.join(engineDir, 'waha.sqlite3');
this.db = new Database(database);
this.db.pragma('journal_mode = WAL;');
getWAHADatabase(): Knex.Knex {
if (!this.knex) {
throw new Error('Knex is not initialized, call LocalStore.init() first');
}
return this.db;
return this.knex;
}
buildKnex(): Knex.Knex {
const engineDir = this.getEngineDirectory();
const database = path.join(engineDir, 'waha.sqlite3');
return Knex({
client: 'better-sqlite3',
connection: { filename: database },
useNullAsDefault: true,
});
}
async close() {
this.db?.close();
await this.knex.destroy();
}
}
-28
View File
@@ -1,28 +0,0 @@
import Knex from 'knex';
export interface ISQLEngine {
/**
* Run a query that does not return any data
*/
run(query: Knex.QueryBuilder): Promise<void>;
/**
* Get a single row from the database
*/
get(query: Knex.QueryBuilder): Promise<any | null>;
/**
* Get all rows from the database
*/
all(query: Knex.QueryBuilder): Promise<any[]>;
/**
* Run a raw SQL query with bindings
*/
raw(sql: string, bindings: any[]): Promise<void>;
/**
* Execute a raw SQL query
*/
exec(sql: string): Promise<void>;
}
+11 -17
View File
@@ -1,9 +1,8 @@
import { Field, Schema } from '@waha/core/storage/Schema';
import { IJsonQuery } from '@waha/core/storage/sql/IJsonQuery';
import { ISQLEngine } from '@waha/core/storage/sql/ISQLEngine';
import { PaginationParams } from '@waha/structures/pagination.dto';
import { KnexPaginator } from '@waha/utils/Paginator';
import Knex from 'knex';
import { Knex } from 'knex';
import * as lodash from 'lodash';
export type Migration = string;
@@ -26,10 +25,7 @@ export class SqlKVRepository<Entity> {
return [];
}
constructor(
private engine: ISQLEngine,
protected knex: Knex.Knex,
) {}
constructor(protected knex: Knex) {}
get columns(): Field[] {
return this.schema.columns;
@@ -49,7 +45,7 @@ export class SqlKVRepository<Entity> {
protected async applyMigrations() {
for (const migration of this.migrations) {
await this.engine.exec(migration);
await this.knex.raw(migration);
}
}
@@ -79,15 +75,13 @@ export class SqlKVRepository<Entity> {
}
}
private async upsertBatch(entities: Entity[]): Promise<void> {
protected async upsertBatch(entities: Entity[]): Promise<void> {
const all = entities.map((entity) => this.dump(entity));
// make it unique by .id
const data = lodash.uniqBy(all, (d: any) => d.id);
if (data.length != all.length) {
console.warn(
`WARNING - Duplicated entities for upsert batch: ${JSON.stringify(
entities,
)}`,
`WARNING - Duplicated entities for upsert batch: all=${all.length}, data=${data.length}`,
);
}
const columns = this.columns.map((c) => `"${c.fieldName}"`);
@@ -121,7 +115,7 @@ export class SqlKVRepository<Entity> {
async getCount(): Promise<number> {
const query = this.select().count({ count: 'id' });
const row = await this.engine.get(query);
const row = await query.first();
if (!row) {
return 0;
}
@@ -140,7 +134,7 @@ export class SqlKVRepository<Entity> {
return new Map();
}
const rows = await this.engine.all(this.select().whereIn('id', ids));
const rows = await this.select().whereIn('id', ids);
const entitiesMap = new Map<string, Entity | null>();
// Initialize a map with null values for all requested IDs
@@ -193,15 +187,15 @@ export class SqlKVRepository<Entity> {
* SQL Implementation details
*/
public async raw(sql: string, bindings: any[]): Promise<void> {
await this.engine.raw(sql, bindings);
await this.knex.raw(sql, bindings);
}
protected async run(query: Knex.QueryBuilder): Promise<void> {
await this.engine.run(query);
await query;
}
protected async get(query: Knex.QueryBuilder): Promise<Entity | null> {
const row = await this.engine.get(query);
const row = await query.first();
if (!row) {
return null;
}
@@ -209,7 +203,7 @@ export class SqlKVRepository<Entity> {
}
public async all(query: Knex.QueryBuilder): Promise<Entity[]> {
const rows = await this.engine.all(query);
const rows = await query;
return rows.map((row) => this.parse(row));
}
-38
View File
@@ -1,38 +0,0 @@
import { ISQLEngine } from '@waha/core/storage/sql/ISQLEngine';
import { Database } from 'better-sqlite3';
import { Knex } from 'knex';
export class Sqlite3Engine implements ISQLEngine {
constructor(protected db: Database) {}
async all(query: Knex.QueryBuilder): Promise<any[]> {
const sql = query.toSQL().sql;
const bind = query.toSQL().bindings;
return this.db.prepare(sql).all(bind);
}
async get(query: Knex.QueryBuilder): Promise<any | null> {
const sql = query.toSQL().sql;
const bind = query.toSQL().bindings;
const row: any = this.db.prepare(sql).get(bind);
if (!row) {
return null;
}
return row;
}
async run(query: Knex.QueryBuilder): Promise<void> {
const sql = query.toSQL().sql;
const bind = query.toSQL().bindings;
this.db.prepare(sql).run(bind);
return;
}
async raw(sql: string, bindings: any[]) {
this.db.prepare(sql).run(bindings);
}
async exec(sql): Promise<void> {
this.db.exec(sql);
}
}
@@ -1,21 +1,24 @@
import { SqlKVRepository } from '@waha/core/storage/sql/SqlKVRepository';
import { Sqlite3Engine } from '@waha/core/storage/sqlite3/Sqlite3Engine';
import { Sqlite3JsonQuery } from '@waha/core/storage/sqlite3/Sqlite3JsonQuery';
import { Database } from 'better-sqlite3';
import { sleep } from '@waha/utils/promiseTimeout';
import Knex from 'knex';
/**
* Key value repository with extra metadata
*/
export class Sqlite3KVRepository<Entity> extends SqlKVRepository<Entity> {
protected db: Database;
protected knex: Knex.Knex;
protected jsonQuery = new Sqlite3JsonQuery();
constructor(db: Database) {
// sqlite does not support inserting default values. Set the `useNullAsDefault` flag to hide this warning. (see docs https://knexjs.org/guide/query-builder.html#insert).
const knex = Knex({ client: 'better-sqlite3', useNullAsDefault: true });
const engine = new Sqlite3Engine(db);
super(engine, knex);
this.db = db;
constructor(knex: Knex.Knex) {
super(knex);
}
protected async upsertBatch(entities: Entity[]): Promise<void> {
await super.upsertBatch(entities);
// Give some time to the Node.js loop because we're using sync better-sqlite
if (entities.length >= this.UPSERT_BATCH_SIZE) {
await sleep(1);
}
}
}
@@ -23,8 +23,8 @@ export class Sqlite3SessionMeRepository
}
constructor(store: LocalStore) {
const db = store.getWAHADatabase();
super(db);
const knex = store.getWAHADatabase();
super(knex);
}
upsertMe(sessionName: string, me: MeInfo): Promise<void> {
@@ -41,7 +41,7 @@ export class Sqlite3SessionMeRepository
}
protected async validateSchema() {
const validation = new Sqlite3SchemaValidation(this.schema, this.db);
validation.validate();
const validation = new Sqlite3SchemaValidation(this.schema, this.knex);
await validation.validate();
}
}
@@ -23,8 +23,8 @@ export class Sqlite3SessionWorkerRepository
}
constructor(store: LocalStore) {
const db = store.getWAHADatabase();
super(db);
const knex = store.getWAHADatabase();
super(knex);
}
assign(session: string, worker: string): Promise<void> {
@@ -45,7 +45,7 @@ export class Sqlite3SessionWorkerRepository
}
protected async validateSchema() {
const validation = new Sqlite3SchemaValidation(this.schema, this.db);
validation.validate();
const validation = new Sqlite3SchemaValidation(this.schema, this.knex);
await validation.validate();
}
}
+8 -15
View File
@@ -47,31 +47,24 @@ process.on('SIGTERM', () => {
logger.info('SIGTERM received');
});
async function loadModules(): Promise<
[typeof AppModuleCore, typeof SwaggerConfiguratorCore]
> {
async function loadModules(): Promise<typeof AppModuleCore> {
const version = getWAHAVersion();
if (version === WAHAVersion.CORE) {
const { AppModuleCore } = await import('./core/app.module.core');
const { SwaggerConfiguratorCore } = await import(
'./core/SwaggerConfiguratorCore'
);
return [AppModuleCore, SwaggerConfiguratorCore];
return AppModuleCore;
}
// Ignore if it's core version - there's no plus module
// Ignore if it's a core version - there's no plus module
// @ts-ignore
const { AppModulePlus } = await import('./plus/app.module.plus');
// @ts-ignore
const { SwaggerConfiguratorPlus } = await import('./plus/SwaggerConfiguratorPlus'); // prettier-ignore
// @ts-ignore
return [AppModulePlus, SwaggerConfiguratorPlus];
return AppModulePlus;
}
async function bootstrap() {
const version = getWAHAVersion();
logger.info(`WAHA (WhatsApp HTTP API) - Running ${version} version...`);
const [AppModule, SwaggerModule] = await loadModules();
const AppModule = await loadModules();
const httpsOptions = AppModule.getHttpsOptions(logger);
const app = await NestFactory.create(AppModule, {
logger: getNestJSLogLevels(),
@@ -81,7 +74,7 @@ async function bootstrap() {
});
app.useLogger(app.get(NestJSPinoLogger));
// Print original stack, not pino one
// Print the original stack, not pino one
// https://github.com/iamolegga/nestjs-pino?tab=readme-ov-file#expose-stack-trace-and-error-class-in-err-property
app.useGlobalInterceptors(new LoggerErrorInterceptor());
@@ -91,13 +84,13 @@ async function bootstrap() {
// but for now we added it ValidationPipe on Controller or endpoint level
// app.useGlobalPipes(new ValidationPipe({ transform: true }));
// Allow to send big body - for images and attachments
// Allow sending big body - for images and attachments
app.use(json({ limit: '50mb' }));
app.use(urlencoded({ limit: '50mb', extended: false }));
app.useWebSocketAdapter(new WsAdapter(app));
// Configure swagger
const swaggerConfigurator = new SwaggerModule(app);
const swaggerConfigurator = new SwaggerConfiguratorCore(app);
swaggerConfigurator.configure(WAHA_WEBHOOKS);
AppModule.appReady(app, logger);
+2 -2
View File
@@ -11,7 +11,7 @@ function getRefSchemaPaths(models) {
/**
* Decorator to add a file accept header to the swagger documentation
*/
export function ApiFileAcceptHeader(...models) {
export function ApiFileAcceptHeader(mimetype: string, ...models) {
models = models.length ? models : [Base64File];
return applyDecorators(
// Add extra models, otherwise it'll give a error
@@ -20,7 +20,7 @@ export function ApiFileAcceptHeader(...models) {
ApiResponse({
status: 200,
content: {
'image/png': {
[mimetype]: {
schema: {
type: 'string',
format: 'binary',
+32 -19
View File
@@ -21,6 +21,11 @@ import { Base64File } from '../structures/files.dto';
export class BufferResponseInterceptor<T>
implements NestInterceptor<Buffer, Base64File | StreamableFile>
{
constructor(
private mimetype: string,
private filename: string = null,
) {}
intercept(
context: ExecutionContext,
next: CallHandler,
@@ -28,27 +33,35 @@ export class BufferResponseInterceptor<T>
const ctx = context.switchToHttp();
const request = ctx.getRequest<Request>();
const response = ctx.getResponse<Response>();
return next.handle().pipe(map(processBuffer));
function processBuffer(buffer: Buffer) {
// Check buffer is Buffer
if (!Buffer.isBuffer(buffer)) {
return buffer;
}
return next
.handle()
.pipe(map((buffer) => this.processBuffer(buffer, request, response)));
}
const accept = request.headers['accept'];
if (accept == 'application/json') {
return {
mimetype: 'image/png',
data: buffer.toString('base64'),
};
}
const file = new StreamableFile(buffer);
response.set({
'Content-Type': 'image/png',
'Content-Length': buffer.length,
});
return file;
processBuffer(buffer: Buffer, request: Request, response: Response) {
// Check buffer is Buffer
if (!Buffer.isBuffer(buffer)) {
return buffer;
}
const accept = request.headers['accept'];
if (accept == 'application/json') {
return {
mimetype: this.mimetype,
data: buffer.toString('base64'),
};
}
const file = new StreamableFile(buffer);
response.set({
'Content-Type': this.mimetype,
'Content-Length': buffer.length,
});
if (this.filename) {
response.set({
'Content-Disposition': `attachment; filename="${this.filename}"`,
});
}
return file;
}
}
+73
View File
@@ -0,0 +1,73 @@
import * as fs from 'node:fs';
import { LoggerBuilder } from '@waha/utils/logging';
import { Logger } from 'pino';
// eslint-disable-next-line @typescript-eslint/no-var-requires
const chokidar = require('chokidar');
export class HttpsExpress {
private readonly keyPath: string;
private readonly certPath: string;
private readonly caPath: string;
constructor(private logger: Logger) {
//
// Let's encrypt certificates default paths
// cert.pem chain.pem fullchain.pem privkey.pem
//
this.keyPath = process.env.WAHA_HTTPS_PATH_KEY || './.secrets/privkey.pem';
this.certPath = process.env.WAHA_HTTPS_PATH_CERT || './.secrets/cert.pem';
this.caPath = process.env.WAHA_HTTPS_PATH_CA;
if (this.caPath == null) {
this.caPath = './.secrets/chain.pem';
}
}
readSync() {
this.logger.info('Reading HTTPS certificates...');
this.logger.info('HTTPS Key Path:', this.keyPath);
const key = fs.readFileSync(this.keyPath);
this.logger.info('HTTPS Cert Path:', this.certPath);
const cert = fs.readFileSync(this.certPath);
this.logger.info('HTTPS CA Path:', this.caPath);
const ca = this.caPath ? fs.readFileSync(this.caPath) : undefined;
this.logger.info('HTTPS certificates read successfully');
return { key: key, cert: cert, ca: ca };
}
/**
* https://stackoverflow.com/a/74076392
*/
watchCertChanges(httpd) {
let waitForCertAndFullChainToGetUpdatedTooTimeout: any;
const paths = [this.keyPath, this.certPath, this.caPath].filter(
(path) => !!path,
);
const watcher = chokidar.watch(paths, {
followSymlinks: false,
persistent: true,
ignoreInitial: true,
disableGlobbing: true,
});
// IDK why, but it has few bugs:
// 1. It issues 'add' event at the start, even tho ignoreInitial is set to true
// 2. It issues additional 'add' for the same file, but without full path
watcher.on('all', (eventName, path, stats) => {
this.logger.info(`HTTPS file '${path}' has been '${eventName}'...`);
clearTimeout(waitForCertAndFullChainToGetUpdatedTooTimeout);
waitForCertAndFullChainToGetUpdatedTooTimeout = setTimeout(() => {
this.logger.info('Updating HTTPS configuration...');
httpd.setSecureContext(this.readSync());
}, 1000);
});
process.on('SIGTERM', () => {
this.logger.info('SIGTERM received, closing HTTP file watchers');
clearTimeout(waitForCertAndFullChainToGetUpdatedTooTimeout);
watcher.close();
});
}
}
+33 -1
View File
@@ -7,8 +7,17 @@ import {
PaginationParams,
} from '@waha/structures/pagination.dto';
import { ChatIdProperty } from '@waha/structures/properties.dto';
import { SessionConfig } from '@waha/structures/sessions.dto';
import { Transform, Type } from 'class-transformer';
import { IsBoolean, IsEnum, IsNumber, IsOptional } from 'class-validator';
import {
IsArray,
IsBoolean,
IsEnum,
IsNumber,
IsOptional,
IsString,
ValidateNested,
} from 'class-validator';
/**
* Queries
@@ -187,6 +196,29 @@ export class OverviewPaginationParams extends LimitOffsetParams {
limit?: number = 20;
}
export class OverviewFilter {
@IsOptional()
@IsArray()
@IsString({ each: true })
@Transform(({ value }) => (Array.isArray(value) ? value : [value]))
@ApiProperty({
description: 'Filter by chat ids',
required: false,
example: ['111111111@c.us'],
})
ids?: string[];
}
export class OverviewBodyRequest {
@ValidateNested()
@Type(() => OverviewPaginationParams)
pagination: OverviewPaginationParams;
@ValidateNested()
@Type(() => OverviewFilter)
filter: OverviewFilter;
}
export class ChatSummary {
id: string;
name: string | null;
+12 -1
View File
@@ -32,7 +32,11 @@ import {
VoiceBinaryFile,
VoiceRemoteFile,
} from './files.dto';
import { ChatIdProperty, ReplyToProperty } from './properties.dto';
import {
ChatIdProperty,
ConvertApiProperty,
ReplyToProperty,
} from './properties.dto';
/**
* Queries
@@ -320,6 +324,9 @@ export class MessageVoiceRequest extends ChatRequest {
@ReplyToProperty()
reply_to?: string;
@ConvertApiProperty()
convert: boolean;
}
@ApiExtraModels(VideoRemoteFile, VideoBinaryFile)
@@ -346,6 +353,9 @@ export class MessageVideoRequest extends ChatRequest {
example: false,
})
asNote?: boolean;
@ConvertApiProperty()
convert: boolean;
}
export class MessageLinkPreviewRequest extends ChatRequest {
@@ -420,6 +430,7 @@ export class MessageDestination {
to: string;
from: string;
fromMe: boolean;
participant?: string;
}
export class MessageButtonReply extends ChatRequest {
+1
View File
@@ -8,6 +8,7 @@ export enum WAHAEvents {
MESSAGE_ACK = 'message.ack',
MESSAGE_WAITING = 'message.waiting',
MESSAGE_REVOKED = 'message.revoked',
MESSAGE_EDITED = 'message.edited',
STATE_CHANGE = 'state.change',
GROUP_JOIN = 'group.join',
GROUP_LEAVE = 'group.leave',
+31
View File
@@ -35,3 +35,34 @@ export class WAMedia {
// eslint-disable-next-line @typescript-eslint/ban-types
error?: object;
}
export class FileDTO {
@ApiProperty({
description: 'The URL for the file',
})
url?: string;
@ApiProperty({
description: 'Base64 content of the file',
example: null,
})
data?: string;
}
export class VoiceFileDTO extends FileDTO {
@ApiProperty({
description: 'The URL for the voice file',
example:
process.env.WHATSAPP_SWAGGER_VIDEO_EXAMPLE_URL ||
'https://github.com/devlikeapro/waha/raw/core/examples/voice.mp3',
})
url?: string;
}
export class VideoFileDTO extends FileDTO {
@ApiProperty({
description: 'The URL for the video file',
example: 'https://github.com/devlikeapro/waha/raw/core/examples/video.mp4',
})
url?: string;
}
+8
View File
@@ -30,6 +30,14 @@ function ReplyToProperty() {
});
}
export function ConvertApiProperty() {
return ApiProperty({
description:
'Convert the input file to the required format using ffmpeg before sending',
example: true,
});
}
export {
ChatIdProperty,
MessageIdOnlyProperty,
+25
View File
@@ -0,0 +1,25 @@
import { ApiProperty } from '@nestjs/swagger';
import { Transform, Type } from 'class-transformer';
import { IsArray, IsNumber, IsOptional, IsString } from 'class-validator';
export class BrowserTraceQuery {
@Type(() => Number)
@IsNumber()
@ApiProperty({
description: 'How many seconds to trace',
example: 30,
required: true,
})
seconds: number = 30;
@IsArray()
@IsString({ each: true })
@Transform(({ value }) => (Array.isArray(value) ? value : [value]))
@IsOptional()
@ApiProperty({
description: 'Categories to trace (all by default)',
example: ['*'],
required: true,
})
categories: string[] = ['*'];
}
+7
View File
@@ -1,4 +1,5 @@
import { ApiExtraModels, ApiProperty, getSchemaPath } from '@nestjs/swagger';
import { ConvertApiProperty } from '@waha/structures/properties.dto';
import {
BinaryFile,
@@ -63,6 +64,9 @@ export class VoiceStatus extends StatusRequest {
file: VoiceRemoteFile | VoiceBinaryFile;
backgroundColor: string = '#38b42f';
@ConvertApiProperty()
convert: boolean;
}
@ApiExtraModels(VideoRemoteFile, VideoBinaryFile)
@@ -76,6 +80,9 @@ export class VideoStatus extends StatusRequest {
file: VideoRemoteFile | VideoBinaryFile;
caption?: string;
@ConvertApiProperty()
convert: boolean;
}
export class DeleteStatusRequest extends StatusRequest {
+26
View File
@@ -87,14 +87,30 @@ export class PollVote extends MessageDestination {
export class PollVotePayload {
vote: PollVote;
poll: MessageDestination;
_data?: any;
}
export class WAMessageRevokedBody {
after?: WAMessage;
before?: WAMessage;
@ApiProperty({
description: 'ID of the message that was revoked',
example: 'A06CA7BB5DD8C8F705628CDB7E3A33C9',
})
revokedMessageId?: string;
_data?: any;
}
export class WAMessageEditedBody extends WAMessage {
@ApiProperty({
description: 'ID of the original message that was edited',
example: 'A06CA7BB5DD8C8F705628CDB7E3A33C9',
})
editedMessageId?: string;
}
export class WASessionStatusBody {
@ApiProperty({
example: 'default',
@@ -205,6 +221,16 @@ export class WAHAWebhookMessageRevoked extends WAHAWebhook {
payload: WAMessageRevokedBody;
}
export class WAHAWebhookMessageEdited extends WAHAWebhook {
@ApiProperty({
description:
'The event is triggered when a user edits a previously sent message.',
})
event = WAHAEvents.MESSAGE_EDITED;
payload: WAMessageEditedBody;
}
export class WAHAWebhookStateChange extends WAHAWebhook {
@ApiProperty({
description: 'It’s an internal engine’s state, not session status.',
+2
View File
@@ -21,6 +21,7 @@ import {
WAHAWebhookMessage,
WAHAWebhookMessageAck,
WAHAWebhookMessageAny,
WAHAWebhookMessageEdited,
WAHAWebhookMessageReaction,
WAHAWebhookMessageRevoked,
WAHAWebhookPollVote,
@@ -37,6 +38,7 @@ const WAHA_WEBHOOKS = [
WAHAWebhookMessageAny,
WAHAWebhookMessageAck,
WAHAWebhookMessageRevoked,
WAHAWebhookMessageEdited,
WebhookGroupV2Join,
WebhookGroupV2Leave,
WebhookGroupV2Update,
+26
View File
@@ -1,3 +1,5 @@
import * as path from 'path';
// eslint-disable-next-line @typescript-eslint/no-var-requires
const fs = require('fs-extra');
@@ -9,3 +11,27 @@ export async function fileExists(filepath: string) {
}
return true;
}
export function safeJoin(base: string, input: string): string {
base = path.resolve(base);
if (!input || typeof input !== 'string') {
throw new Error('Invalid path');
}
if (input.startsWith('~') || path.isAbsolute(input)) {
throw new Error('Home or absolute paths not allowed');
}
// handles slashes safely
const joined = path.join(base, input);
// normalize to an absolute path
const resolved = path.resolve(joined);
// Prevent escape outside base dir
if (!resolved.startsWith(base + path.sep)) {
throw new Error('Access outside base dir not allowed');
}
return resolved;
}
+47
View File
@@ -0,0 +1,47 @@
import * as fs from 'fs/promises';
import * as os from 'os';
import * as path from 'path';
import { Logger } from 'pino';
function sanitize(input: string) {
return input.replace(/[^a-zA-Z0-9-_]/g, '_');
}
export class TmpDir {
private readonly prefix: string;
constructor(
private logger: Logger,
prefix: string,
private cleanupTimeout = 10_000,
) {
this.prefix = sanitize(prefix);
if (this.prefix.length > 249) {
this.prefix = this.prefix.slice(0, 249);
}
}
/**
* Creates a temporary directory and passes it to the callback.
* The directory is automatically cleaned up after the callback returns.
* @param callback Function that receives the temporary directory path
* @returns The result of the callback
*/
public async use<T>(callback: (dir: string) => Promise<T>): Promise<T> {
const dir = await fs.mkdtemp(path.join(os.tmpdir(), this.prefix));
try {
return await callback(dir);
} finally {
setTimeout(async () => {
fs.rm(dir, { recursive: true, force: true })
.then(() => {
this.logger.trace(`Cleaned up temporary directory '${dir}'`);
})
.catch((err) => {
this.logger.warn(
`Failed to clean up temporary directory '${dir}': ${err.message}`,
);
});
}, this.cleanupTimeout);
}
}
}
+3 -1
View File
@@ -33,9 +33,11 @@ export function getEngineName(): string {
}
export const VERSION: WAHAEnvironment = {
version: '2025.5.4',
version: '2025.7.3',
engine: getEngineName(),
tier: getWAHAVersion(),
browser:
getEngineName() === WAHAEngine.WEBJS ? getBrowserExecutablePath() : null,
};
export const IsChrome = VERSION.browser?.includes('chrome');
+1 -1
View File
@@ -4,4 +4,4 @@ http:
allow-insecure: true
no-follow-redirects: true
timeout: 5000
body: ['version']
body: ['version']
+2 -2
View File
@@ -2,11 +2,11 @@
"waha": {
"gows": {
"repo": "devlikeapro/gows",
"ref": "v1.0.6"
"ref": "v1.0.9"
},
"dashboard": {
"repo": "devlikeapro/dashboard",
"ref": "3fb6694d849ca0b567679dd59311a50b9d727f2f"
"ref": "fd8594d89ef369ed669c8b8b529ab29058c0d13c"
}
}
}
+3838 -3285
View File
File diff suppressed because it is too large. Load diff