Compare commits

..
258 Commits
Author SHA1 Message Date
devlikepro aa364b57e8 [core] up NOWEB
Release / amd64 - chrome - chrome (push) Waiting to run
Release / amd64 - chromium - latest (push) Waiting to run
Release / linux/arm64 - chromium - arm (push) Waiting to run
Release / amd64 - none - noweb (push) Waiting to run
Release / linux/arm64 - none - noweb-arm (push) Waiting to run
2024-09-04 10:53:01 +07:00
devlikepro ea973d0de4 [core] up NOWEB 2024-09-04 10:36:14 +07:00
devlikepro 1a1b64cbb2 [core] 2024.9.3 2024-09-04 10:14:24 +07:00
devlikepro 09c7e11dbc [core] up NOWEB - end() issues 2024-09-04 10:14:12 +07:00
devlikepro 3450a56756 [core] swagger participants 2024-09-03 19:30:01 +07:00
devlikepro f37f4466ae [core] swagger noweb store 2024-09-03 19:23:37 +07:00
devlikepro 451834b219 [core] swagger caption not required 2024-09-03 19:13:27 +07:00
devlikepro 91924353e2 [core] swagger caption not required 2024-09-03 19:13:21 +07:00
devlikepro 67cf94fc95 [core] swagger version 2024-09-03 18:44:38 +07:00
devlikepro 36a343c09f [core] swagger status 2024-09-03 18:43:52 +07:00
devlikepro 4a37aebbf1 [core] swagger server 2024-09-03 18:41:25 +07:00
devlikepro d5fa9f8d72 [core] swagger presence 2024-09-03 18:40:48 +07:00
devlikepro 08c244334b [core] swagger ping 2024-09-03 18:38:17 +07:00
devlikepro 23dd1c2726 [core] swagger labels 2024-09-03 18:37:59 +07:00
devlikepro 23a309d394 [core] swagger health 2024-09-03 18:37:05 +07:00
devlikepro 3c1bb4e145 [core] swagger groups 2024-09-03 18:36:47 +07:00
devlikepro 87b320a3e3 [core] swagger contacts 2024-09-03 18:29:03 +07:00
devlikepro e4fb9615c7 [core] swagger chatting 2024-09-03 18:26:50 +07:00
devlikepro efad4de0af [core] swagger chats 2024-09-03 18:20:33 +07:00
devlikepro 04f62700a2 [core] swagger auth channels fixes 2024-09-03 18:16:52 +07:00
devlikepro 08415f7931 [core] no code 2024-09-03 18:12:42 +07:00
devlikepro 8bc86af3c8 [core] fix swagger 2024-09-01 19:46:18 +07:00
devlikepro fa2c5e42dc [core] fix swagger 2024-09-01 19:43:23 +07:00
devlikepro e3d3952b81 [core] fix participant cus in reply 2024-08-30 21:32:27 +07:00
devlikepro a9171addde [core] 2024.9.2
Release / amd64 - chrome - chrome (push) Waiting to run
Release / amd64 - chromium - latest (push) Waiting to run
Release / linux/arm64 - chromium - arm (push) Waiting to run
Release / amd64 - none - noweb (push) Waiting to run
Release / linux/arm64 - none - noweb-arm (push) Waiting to run
2024-08-29 09:14:16 +07:00
devlikepro 440ac087ad [core] Cannot read properties of null (reading 'conversation')
fix #521
2024-08-29 09:14:16 +07:00
devlikepro 9dd5eb4eeb [core] dashboard
Release / amd64 - chrome - chrome (push) Waiting to run
Release / amd64 - chromium - latest (push) Waiting to run
Release / linux/arm64 - chromium - arm (push) Waiting to run
Release / amd64 - none - noweb (push) Waiting to run
Release / linux/arm64 - none - noweb-arm (push) Waiting to run
2024-08-27 17:20:57 +07:00
devlikepro 26c2c9abc9 [core] Fix default session 2024-08-27 16:54:25 +07:00
devlikepro 663b07c07e [core] 2024.9.1 2024-08-27 16:44:36 +07:00
devlikepro 5add6a0e73 [core] up dashboard 2024-08-27 16:44:36 +07:00
devlikepro 5c4912feb4 [core] accept localhost url 2024-08-27 16:44:25 +07:00
devlikepro 75e115f390 [core] NOWEB - Handle both format for reply_to - full and only message id 2024-08-27 16:44:24 +07:00
devlikepro 8e443d23f4 [core] Add reply_to for incoming messages
Fixes #506
2024-08-27 16:44:24 +07:00
devlikepro c43c9c319b [core] Comment this 2024-08-27 16:44:24 +07:00
devlikepro e32b128b0f [core] Add reply to for all messages
fixes #503
2024-08-27 16:44:23 +07:00
devlikepro 5287421cea [core] Add location example 2024-08-27 16:43:39 +07:00
devlikepro 9bc95ef350 [core] fix delete message 2024-08-27 16:43:38 +07:00
devlikepro 028de6addb [core] cached eslint 2024-08-27 16:43:36 +07:00
devlikepro a107605937 [core] media error 2024-08-27 16:43:36 +07:00
devlikepro cec5d0147b [core] Move media dto 2024-08-27 16:43:35 +07:00
devlikepro e46b879fc4 [core] Add WHATSAPP_API_SCHEMA 2024-08-27 16:43:34 +07:00
devlikepro b6764fbb29 [core] Add WHATSAPP_API_SCHEMA 2024-08-27 16:43:33 +07:00
devlikepro e8899cc314 [core] Do not validate body on deprecated endpoints 2024-08-27 16:43:32 +07:00
devlikepro 2b2054dd6b [core] WEJBS add more optimization 2024-08-27 16:43:32 +07:00
devlikepro 92705b0ae2 [core] sort args 2024-08-27 16:43:32 +07:00
devlikepro 2ae8a36b7f [core] Up NOWEB 2024-08-27 16:43:32 +07:00
devlikepro 899139dda1 [core] ignore all files requests in http access logs 2024-08-27 16:43:32 +07:00
devlikepro b6b571e84b [core] proxy s3 files 2024-08-27 16:43:29 +07:00
devlikepro 9e098fa997 [core] move "schema" to main config.service.ts 2024-08-27 16:43:28 +07:00
devlikepro 13d00f954f [core] add Storage tag 2024-08-27 16:43:27 +07:00
devlikepro fee75d0c8a [core] WEBJS - optimize media 2024-08-27 16:43:26 +07:00
devlikepro abae3f14cd [core] NOWEB - optimize media 2024-08-27 16:43:25 +07:00
devlikepro b8a98e2c6e [core] WAHA_S3_FORCE_PATH_STYLE to docker-compose.yaml 2024-08-27 16:43:25 +07:00
devlikepro d990572b5b [core] Move files config 2024-08-27 16:43:24 +07:00
devlikepro 2d37d392bc [core] add pre-commit 2024-08-27 16:43:22 +07:00
devlikepro 59015fc3bb [core] Remove ConsoleLogger 2024-08-27 16:43:21 +07:00
devlikepro c3be68f988 [core] init for MediaStorage 2024-08-27 16:43:21 +07:00
devlikepro 8610784958 [core] add minio to docker-compose.yaml 2024-08-27 16:43:19 +07:00
devlikepro f01d71c34a [core] fix mongo service 2024-08-27 16:43:18 +07:00
devlikepro a06501e4b1 [core] Add filename 2024-08-27 16:43:16 +07:00
devlikepro 7c2c30fa1b [core] Add session to media 2024-08-27 16:43:16 +07:00
devlikepro 903dc714f2 [core] add s3 data to media 2024-08-27 16:43:16 +07:00
devlikepro b1bd2c8805 [core] Move GET /sendText down 2024-08-27 16:43:16 +07:00
devlikepro 0e4b4f6d8c [core] Stop process if bootstrap failed 2024-08-27 16:43:13 +07:00
devlikepro d8d0eaf247 [core] use modules for media storages 2024-08-27 16:43:13 +07:00
devlikepro 90096faef3 [core] Refactoring media manager 2024-08-27 16:43:13 +07:00
devlikepro f794f75394 [core] Move media files, rename classes 2024-08-27 16:43:09 +07:00
devlikepro 8de63e486c [core] install s3 2024-08-27 16:43:09 +07:00
devlikepro 4308efca47 [core] ignore .media 2024-08-27 16:43:08 +07:00
devlikepro 877fa292d6 [core] ignore .minio 2024-08-27 16:43:07 +07:00
devlikepro df6ea6f497 [core] add types for withLock 2024-08-27 16:43:06 +07:00
devlikepro 5e9be391e1 [core] Add UV_THREADPOOL_SIZE in docker 2024-08-27 16:43:04 +07:00
devlikepro 49960b8234 [core] update NOWEB 2024-08-27 16:43:03 +07:00
devlikepro d669dbe54b [core] Use new API to create a session 2024-08-27 16:43:03 +07:00
devlikepro 2d1859ac0c [core] Check session "name" length 2024-08-27 16:43:01 +07:00
devlikepro dc864055a3 [core] NOWEB - new newsletter actions 2024-08-27 16:43:00 +07:00
devlikepro bad9537aba [core] NOWEB - use fork-master-channels-2 branch 2024-08-27 16:43:00 +07:00
devlikepro 303b1adc0a [core] add emojies to tags 2024-08-27 16:42:58 +07:00
devlikepro c582904d41 [core] Add doc for /api/server/stop 2024-08-27 16:42:58 +07:00
devlikepro 65eabf6153 [core] Simplify /api/server/status 2024-08-27 16:42:58 +07:00
devlikepro cc9c9e2253 [core] /api/server swagger fix 2024-08-27 16:42:57 +07:00
devlikepro 8747ae9564 [core] logging for websocket 2024-08-27 16:42:56 +07:00
devlikepro 9253e54958 [core] API to restart server (actually it's /stop, but docker will handle restart) 2024-08-27 16:42:55 +07:00
devlikepro fb774f2caa [core] Adjust swagger for sendSeen
fixed #485
2024-08-27 16:42:55 +07:00
devlikepro ba120024a6 [core] Add metadata 2024-08-27 16:42:54 +07:00
devlikepro 1fe0e69914 [core] Make /start idempotent 2024-08-27 16:42:54 +07:00
devlikepro 0fe04e411c [core] 3s 2024-08-27 16:42:54 +07:00
devlikepro 35074f959f [core] check directory exists 2024-08-27 16:42:53 +07:00
devlikepro 0d698fbaf1 [core] fix log for unhandled rejection 2024-08-27 16:42:52 +07:00
devlikepro c9e2a52327 [core] check is running 2024-08-27 16:42:51 +07:00
devlikepro a58e06109b [core] PUT /api/sessions/{session} 2024-08-27 16:42:50 +07:00
devlikepro 65375e4836 [core] wait until, so we don't close a websocket on connecting 2024-08-27 16:42:49 +07:00
devlikepro edc517d33f [core] Add deprecated API to lock as well 2024-08-27 16:42:49 +07:00
devlikepro 354c82dc34 [core] timeout after stop a session 2024-08-27 16:42:48 +07:00
devlikepro fe2acd0665 [core] wait for a browser 2024-08-27 16:42:48 +07:00
devlikepro 19b5b1a90c [core] await for a client 2024-08-27 16:42:47 +07:00
devlikepro e7281e2118 [core] Add /api/sessions/{session}/restart 2024-08-27 16:42:45 +07:00
devlikepro 10f334cf15 [core] Log when session started 2024-08-27 16:42:45 +07:00
devlikepro a057fd6458 [core] Add timestamp run 2024-08-27 16:42:43 +07:00
devlikepro 3bafabeb0a [core] Add withLock for dangerous session operations 2024-08-27 16:42:43 +07:00
devlikepro edefbe17da [core] webjs log error 2024-08-27 16:42:42 +07:00
devlikepro a65041ffcd [core] DELETE /api/sessions/{session}/ 2024-08-27 16:42:42 +07:00
devlikepro 0962c60844 [core] DELETE /api/sessions/{session}/ 2024-08-27 16:42:42 +07:00
devlikepro 3d72eceac0 [core] Add /api/sessions/{session}/stop 2024-08-27 16:42:41 +07:00
devlikepro 57b5deff5b [core] Add /api/sessions/{session}/start 2024-08-27 16:42:40 +07:00
devlikepro 1163dee352 [core] uuid4 2024-08-27 16:42:39 +07:00
devlikepro f7fe562bba [core] Add POST /api/sessions
fixes #435
2024-08-27 16:42:39 +07:00
devlikepro 3394f98a8a [core] rm aka 2024-08-27 16:42:38 +07:00
devlikepro a575b35286 [core] Adjust session param 2024-08-27 16:42:36 +07:00
devlikepro 0f41e79e98 [core] Add sessions.deprecated.dto 2024-08-27 16:42:36 +07:00
devlikepro 24bf22a31f [core] Adjust sessions Swagger 2024-08-27 16:42:35 +07:00
devlikepro 05a1b8802e [core] Add swagger api key (forgot about it) 2024-08-27 16:42:34 +07:00
devlikepro 9c409d9507 [core] Simplify actions for manager 2024-08-27 16:42:34 +07:00
devlikepro 4ee7161c36 [core] Warning if client is not destroyed 2024-08-27 16:42:34 +07:00
devlikepro 4cd9ce0ff7 [core] Wait till the store is closed 2024-08-27 16:42:30 +07:00
devlikepro a2d0dfd8b2 [core] Add Old prefix for old methods 2024-08-27 16:42:30 +07:00
devlikepro d4164fe0ba [core] Add fs-extra 2024-08-27 16:42:29 +07:00
devlikepro 6bbecdd7c0 [core] Keep .waha.session. files in directory and remove it in LocalSessionConfigRepository.ts 2024-08-27 16:42:26 +07:00
devlikepro 0784c3ae05 [core] move getAll sessions to session config repo 2024-08-27 16:42:26 +07:00
devlikepro 679757ccfa [core] check is array object
fixes #470
2024-08-27 16:42:25 +07:00
devlikepro 63a4e693e3 [core] Move helpers to nestjs 2024-08-27 16:42:24 +07:00
devlikepro b21041d9e0 [core] Validate session config
fix #470
2024-08-27 16:42:24 +07:00
devlikepro f014bb4587 [core] Add /api/server/environment
fixes #319
2024-08-27 16:42:24 +07:00
devlikepro 9030f7c4ce [core] 2024.8.5
Release / amd64 - chrome - chrome (push) Waiting to run
Release / amd64 - chromium - latest (push) Waiting to run
Release / linux/arm64 - chromium - arm (push) Waiting to run
Release / amd64 - none - noweb (push) Waiting to run
Release / linux/arm64 - none - noweb-arm (push) Waiting to run
2024-08-23 08:20:27 +07:00
devlikepro bde6c38bd4 [core] up NOWEB 2024-08-23 08:20:03 +07:00
devlikepro 0aefb6c6ec [core] 2024.8.4
Release / amd64 - chrome - chrome (push) Waiting to run
Release / amd64 - chromium - latest (push) Waiting to run
Release / linux/arm64 - chromium - arm (push) Waiting to run
Release / amd64 - none - noweb (push) Waiting to run
Release / linux/arm64 - none - noweb-arm (push) Waiting to run
2024-08-18 16:45:05 +07:00
devlikepro da650ccdef [core] Fix get chats with limit offset properties
fixes #486
2024-08-18 14:21:33 +07:00
devlikepro 92d3e70c1f [core] 2024.8.3
Release / amd64 - chrome - chrome (push) Waiting to run
Release / amd64 - chromium - latest (push) Waiting to run
Release / linux/arm64 - chromium - arm (push) Waiting to run
Release / amd64 - none - noweb (push) Waiting to run
Release / linux/arm64 - none - noweb-arm (push) Waiting to run
2024-08-14 15:49:36 +07:00
devlikepro cb5dfaf066 [core] Remove NEW from dashboard 2024-08-14 15:49:36 +07:00
devlikepro cd75e5e4cc [core] auto reconnection fix - reduce to 28 minutes and check if websocket is not connecting
fixes #483
2024-08-14 15:49:36 +07:00
devlikepro 7bd6ab5ec0 [core] 2024.8.2
Release / amd64 - chrome - chrome (push) Waiting to run
Release / amd64 - chromium - latest (push) Waiting to run
Release / linux/arm64 - chromium - arm (push) Waiting to run
Release / amd64 - none - noweb (push) Waiting to run
Release / linux/arm64 - none - noweb-arm (push) Waiting to run
2024-08-07 17:29:11 +07:00
devlikepro 92f9c89cf1 [core] add WAHA_HTTP_LOG_LEVEL
fixed #466
2024-08-07 17:29:11 +07:00
devlikepro eb2632b23a [core] Fix noweb autorestart timing 2024-08-07 17:29:10 +07:00
devlikepro 8c90bc6808 [core] logger warning 2024-08-07 17:29:10 +07:00
devlikepro 66146532e1 [core] fix WEBJS constant restart 2024-08-07 17:29:10 +07:00
devlikepro dbf3a41c1d [core] Bump WEBJS 2024-08-07 17:29:10 +07:00
devlikepro 942bbf9c6c [core] Add sleep 2024-08-07 17:29:09 +07:00
devlikepro a9d7cc527c [core] Use SingleDelayedJobRunner in WEBJS 2024-08-07 17:29:09 +07:00
devlikepro 2d362722ed [core] Use SingleDelayedJobRunner 2024-08-07 17:29:09 +07:00
devlikepro f81638e625 [core] Use SinglePeriodicJobRunner to rerun session 2024-08-07 17:29:08 +07:00
devlikepro 8ae77df0ff [core] Add SingleDelayedJobRunner 2024-08-07 17:29:08 +07:00
devlikepro 110bd0e5f7 [core] Add SinglePeriodicJobRunner 2024-08-07 17:29:08 +07:00
devlikepro 1b3a366cb7 [core] Add test.* to ignore 2024-08-07 17:29:07 +07:00
devlikepro 3b5d9a145f [core] Rename WebsocketHeartbeatJob.ts 2024-08-07 17:29:07 +07:00
devlikepro fc37fc067a [core] Install zip and unzip in docker image 2024-08-07 17:29:07 +07:00
devlikepro bb18a28484 [core] Add pretty-bytes 2024-08-07 17:29:06 +07:00
devlikepro 86ca08b9aa [core] 2024.8.1
Release / amd64 - chrome - chrome (push) Waiting to run
Release / amd64 - chromium - latest (push) Waiting to run
Release / linux/arm64 - chromium - arm (push) Waiting to run
Release / amd64 - none - noweb (push) Waiting to run
Release / linux/arm64 - none - noweb-arm (push) Waiting to run
2024-08-04 10:22:16 +07:00
devlikepro 9522df7835 [core] Bump dashboard 2024-08-04 10:22:16 +07:00
devlikepro 55e0fd0226 [core] rename .removed to .deleted 2024-08-04 10:22:16 +07:00
devlikepro ff9a986a2e [core] Fix for chat id in labels events 2024-08-04 10:09:44 +07:00
devlikepro df31fe2a8b [core] NOWEB labels 2024-08-04 10:09:32 +07:00
devlikepro 1aeaf996bc [core] make up-engine commands 2024-08-04 10:09:32 +07:00
devlikepro cb0e59e91a [core] Print original error, not pino one 2024-08-04 10:09:32 +07:00
devlikepro 5f02f49e9d [core] Add other /labels endpoints 2024-08-04 10:09:32 +07:00
devlikepro ce9f91fe2c [core] Use /api/{session}/labels 2024-08-04 10:09:32 +07:00
devlikepro ac37745721 [core] Add type 2024-08-04 10:09:31 +07:00
aziz e44aeefda8 [core] chore: Add LabelsController to handle label-related API requests 2024-08-04 10:08:58 +07:00
devlikepro dbff2b659a [core] Fix swagger docs 2024-08-04 10:07:09 +07:00
devlikepro 52ef8e8b76 [core] Add /ping 2024-08-04 10:07:08 +07:00
devlikepro b71567c754 [core] exclude path from api-key
fixes #451
2024-08-04 10:07:08 +07:00
devlikepro d1670c743f [core] Add "sessions.name" index for MongoDB
closes #447
2024-08-04 10:06:43 +07:00
devlikepro 009b38f471 [core] Add call.* events
closes #307
2024-08-04 10:06:43 +07:00
devlikepro 0cf5f30c0d [core] Add timestamp to chat.archive 2024-08-04 10:06:42 +07:00
devlikepro cd4e1f179d [core] Add chat.archive event
closes #434
2024-08-04 10:06:42 +07:00
devlikepro b89f9d6b35 [core] Add chats /archive and /unarchive
fixes #434
2024-08-04 10:06:42 +07:00
devlikepro c3ece96390 [core] Remove html 2024-08-02 09:05:31 +07:00
devlikepro 770739b44e [core] Add pdf example 2024-08-01 13:44:37 +07:00
devlikepro 84984c2cde [core] Up chrome version 2024-07-31 17:10:19 +07:00
devlikepro 86393c8674 [core] 2024.7.7
Release / amd64 - chrome - chrome (push) Waiting to run
Release / amd64 - chromium - latest (push) Waiting to run
Release / linux/arm64 - chromium - arm (push) Waiting to run
Release / amd64 - none - noweb (push) Waiting to run
Release / linux/arm64 - none - noweb-arm (push) Waiting to run
2024-07-24 19:02:03 +03:00
devlikepro d4aed3bc45 [core] pin adm-zip 0.5.10
trying to work on #457
2024-07-24 19:01:44 +03:00
devlikepro 818cd77450 [core] key 2024-07-23 10:37:37 +03:00
devlikepro dc26c70400 [core] waha-run 2024-07-23 10:28:52 +03:00
devlikepro 6547d713ea [core] remove version from docker 2024-07-23 10:03:41 +03:00
devlikepro e8dcb0c242 [core] Add full docker-compose.yaml 2024-07-23 09:46:47 +03:00
devlikepro 100d0be513 [core] 2024.7.6
Release / amd64 - chrome - chrome (push) Waiting to run
Release / amd64 - chromium - latest (push) Waiting to run
Release / linux/arm64 - chromium - arm (push) Waiting to run
Release / amd64 - none - noweb (push) Waiting to run
Release / linux/arm64 - none - noweb-arm (push) Waiting to run
2024-07-18 17:41:38 +03:00
devlikepro fc8db2d977 [core] WEBJS update 2024-07-18 17:41:31 +03:00
devlikepro e5d98d7889 [core] ignore /dashboard logs 2024-07-18 17:40:47 +03:00
devlikepro c7b1a05ed3 [core] Download dashboard 2024-07-15 18:46:43 +03:00
devlikepro 649a90bd5e [core] Keep dashboard 2024-07-15 17:56:04 +03:00
devlikepro 0fc880cf21 [core] Keep dashboard 2024-07-15 17:55:54 +03:00
devlikepro 0d82a4c2c6 [core] Remove dashboard 2024-07-15 17:54:38 +03:00
devlikepro fb3aa59683 [core] 2024.7.5
Release / amd64 - chrome - chrome (push) Waiting to run
Release / amd64 - chromium - latest (push) Waiting to run
Release / linux/arm64 - chromium - arm (push) Waiting to run
Release / amd64 - none - noweb (push) Waiting to run
Release / linux/arm64 - none - noweb-arm (push) Waiting to run
2024-07-15 16:03:08 +03:00
devlikepro b938792ca8 [core] channels 2024-07-15 16:02:51 +03:00
devlikepro bb74946d54 [core] bump 2024.7.4
Release / amd64 - chrome - chrome (push) Waiting to run
Release / amd64 - chromium - latest (push) Waiting to run
Release / linux/arm64 - chromium - arm (push) Waiting to run
Release / amd64 - none - noweb (push) Waiting to run
Release / linux/arm64 - none - noweb-arm (push) Waiting to run
2024-07-08 15:02:36 +03:00
devlikepro 3e78105509 [core] fix profile picture
closes #422
2024-07-08 15:02:36 +03:00
devlikepro c6adf0ac89 [core] console.log for webjs 2024-07-05 14:04:03 +03:00
devlikepro 219b4dca94 [core] pretty logs by default 2024-07-05 13:40:13 +03:00
devlikepro acec292b0f [core] 2024.7.3 2024-07-05 13:30:47 +03:00
devlikepro 7ba86cbd23 [core] logging 2024-07-05 13:30:08 +03:00
devlikepro 3002eaf479 [core] Init Logger in manager explicitly 2024-07-05 13:30:05 +03:00
devlikepro 15422ca504 [core] 2024.7.2
Release / amd64 - chrome - chrome (push) Waiting to run
Release / amd64 - chromium - latest (push) Waiting to run
Release / linux/arm64 - chromium - arm (push) Waiting to run
Release / amd64 - none - noweb (push) Waiting to run
Release / linux/arm64 - none - noweb-arm (push) Waiting to run
2024-07-04 12:11:43 +03:00
devlikepro 7067f00bee [core] Up WEBJS engine 2024-07-04 12:11:43 +03:00
devlikepro a0616eb251 [core] 2024.7.1
Release / amd64 - chrome - chrome (push) Waiting to run
Release / amd64 - chromium - latest (push) Waiting to run
Release / linux/arm64 - chromium - arm (push) Waiting to run
Release / amd64 - none - noweb (push) Waiting to run
Release / linux/arm64 - none - noweb-arm (push) Waiting to run
2024-07-03 10:58:33 +03:00
devlikepro 5d4da0e4c7 [core] No cache while waiting 2.3xx release 2024-07-03 10:56:33 +03:00
devlikepro 546e3475fb [core] mark GET sendText as deprecated
closes #406
2024-07-03 09:15:38 +03:00
devlikepro 56c44ebb58 [core] 2.3000
Release / amd64 - chrome - chrome (push) Waiting to run
Release / amd64 - chromium - latest (push) Waiting to run
Release / linux/arm64 - chromium - arm (push) Waiting to run
Release / amd64 - none - noweb (push) Waiting to run
Release / linux/arm64 - none - noweb-arm (push) Waiting to run
2024-06-27 15:25:33 +07:00
devlikepro bd03bc730c [core] Remove 2.3000 2024-06-27 15:25:00 +07:00
devlikepro e0e1e58104 [core] 2024.6.8 2024-06-27 15:19:41 +07:00
devlikepro 940de662b6 [core] WEBJS 2.3000 2024-06-27 15:19:41 +07:00
devlikepro d151c1192b [core] update webjs 2024-06-27 15:19:41 +07:00
devlikepro 15ae10579c [core] use fork 2024-06-27 15:19:41 +07:00
devlikepro 359696408d [core] 2024.6.7
Release / amd64 - chrome - chrome (push) Waiting to run
Release / amd64 - chromium - latest (push) Waiting to run
Release / linux/arm64 - chromium - arm (push) Waiting to run
Release / amd64 - none - noweb (push) Waiting to run
Release / linux/arm64 - none - noweb-arm (push) Waiting to run
2024-06-25 13:36:40 +07:00
devlikepro d3766368f8 [core] delete status endpoint 2024-06-25 13:36:40 +07:00
devlikepro 73af7451fc [core] Use NOWEB fork with fixes
fixes #390
2024-06-25 11:58:24 +07:00
devlikepro e27fcefe45 [core] fix #373 2024-06-24 19:50:22 +07:00
devlikepro aa5ade5517 [core] Remove link to project
Fix #373
2024-06-24 19:43:20 +07:00
devlikepro 94b6a66ce4 [core] dashboard 2024-06-24 19:16:28 +07:00
devlikepro a51e486fa6 [core] timeout 10ms 2024-06-24 19:10:07 +07:00
devlikepro 2b608daea4 update dashboard 2024-06-24 18:50:38 +07:00
devlikepro aa60c2afd1 [core] warning 2024-06-24 18:49:26 +07:00
devlikepro cff22c74a9 [core] Fix sock auth 2024-06-24 18:43:23 +07:00
devlikepro b77b8ee0da [core] timeout for getEngineInfo, logs 2024-06-24 18:41:42 +07:00
devlikepro 610aef2f14 [core] Make getSessionMeInfo sync 2024-06-24 18:41:42 +07:00
devlikepro e15ca41d6e [core] websockets 2024-06-24 17:38:44 +07:00
devlikepro 60e9e53af4 [core] issue session.status, not session.status.changed 2024-06-24 17:36:20 +07:00
devlikepro 9812000724 [core] debug by default in NOWEB 2024-06-21 12:17:12 +07:00
devlikepro d72401fc39 [core] Add msgRetryCounterCache 2024-06-20 14:21:00 +07:00
devlikepro 822eba7982 [core] keepAliveIntervalMs back to 30s 2024-06-19 16:20:01 +07:00
devlikepro 9327564e6e [core] set defaultQueryTimeoutMs to 120s 2024-06-19 16:20:01 +07:00
devlikepro 6e9fe42a83 [core] bump version 2024-06-19 12:55:14 +07:00
devlikepro a32cf28ccb [core] fix forever "typing" status
fix #379
2024-06-19 12:54:43 +07:00
devlikepro 55b101802a [core] no more ssh 2024-06-19 12:04:11 +07:00
devlikepro ecda8b4394 [core] set "trace" by default for noweb 2024-06-19 12:04:09 +07:00
devlikepro 6d4f8d4c26 [core] Add logger to NOWEB engine 2024-06-19 11:30:41 +07:00
devlikepro 26aa53f3fa Use webhook.site 2024-06-18 11:18:39 +07:00
devlikepro 2f7a94ea3f [core] dashboard update
Release / amd64 - chrome - chrome (push) Waiting to run
Release / amd64 - chromium - latest (push) Waiting to run
Release / linux/arm64 - chromium - arm (push) Waiting to run
Release / amd64 - none - noweb (push) Waiting to run
Release / linux/arm64 - none - noweb-arm (push) Waiting to run
2024-06-18 11:02:16 +07:00
devlikepro 2b622b4ee9 [core] dashboard 2024-06-18 10:55:41 +07:00
devlikepro 3e280f1e08 [core] Bump version to 2024.6.5 2024-06-18 10:51:02 +07:00
devlikepro c54d52ad45 [core] built in ssl 2024-06-18 10:50:44 +07:00
devlikepro e54d3ea96b [core] fix for-swagger 2024-06-17 10:56:04 +07:00
devlikepro e07cb8257d [core] Bump to 2024.6.4
Release / amd64 - chrome - chrome (push) Waiting to run
Release / amd64 - chromium - latest (push) Waiting to run
Release / linux/arm64 - chromium - arm (push) Waiting to run
Release / amd64 - none - noweb (push) Waiting to run
Release / linux/arm64 - none - noweb-arm (push) Waiting to run
2024-06-17 10:37:11 +07:00
devlikepro 74b6a8fdba [core] add chats limit and offset 2024-06-17 10:36:51 +07:00
devlikepro e48c76397c [core] downgrade better-sqlite3
Release / amd64 - chrome - chrome (push) Waiting to run
Release / amd64 - chromium - latest (push) Waiting to run
Release / linux/arm64 - chromium - arm (push) Waiting to run
Release / amd64 - none - noweb (push) Waiting to run
Release / linux/arm64 - none - noweb-arm (push) Waiting to run
2024-06-14 13:52:39 +07:00
devlikepro 64aff5d041 [core] fix Dockerfile 2024-06-13 16:06:00 +07:00
devlikepro 87b24e35d0 [core] update dashboard 2024-06-13 15:58:37 +07:00
devlikepro fb48c81207 [core] rm dashboard 2024-06-13 15:57:36 +07:00
devlikepro cd9f302f8b [core] 2024.6.3 2024-06-13 15:46:43 +07:00
devlikepro df3bc97988 [core] noweb store 2024-06-13 15:46:43 +07:00
devlikepro 94191a2902 [core] noweb preview video 2024-06-13 15:46:43 +07:00
devlikepro a0dd540075 [core] webjs fix download media 2024-06-13 15:46:43 +07:00
devlikepro 5684665123 [core] Add getFilePath to LocalStore 2024-06-13 15:46:43 +07:00
devlikepro 5db407d750 [core] Fix docker pulls number 2024-06-08 17:02:30 +07:00
devlikepro 27a86f0dc1 [core] waha
Release / amd64 - chrome - chrome (push) Waiting to run
Release / amd64 - chromium - latest (push) Waiting to run
Release / linux/arm64 - chromium - arm (push) Waiting to run
Release / amd64 - none - noweb (push) Waiting to run
Release / linux/arm64 - none - noweb-arm (push) Waiting to run
2024-06-04 12:55:58 +07:00
devlikepro 767ea23173 [core] bump 2024.6.2 2024-06-04 12:54:24 +07:00
devlikepro 85707508ed [core] waha 2024-06-04 12:54:03 +07:00
devlikepro e8bc59ff9e [core] Bump to 2024.6.1 2024-06-02 10:25:03 +07:00
devlikepro 7f9e622a64 [core] push version tags 2024-06-02 10:06:49 +07:00
devlikepro b22e37596b Revert "[core] Don't push each version"
This reverts commit 0e295bebde.
2024-06-02 10:04:12 +07:00
devlikepro 642e90b1c8 [core] poll response fix
fix #356
2024-06-02 10:01:40 +07:00
devlikepro 8402ab896a [core] Fix ack for status@broadcast
Release / amd64 - chrome - chrome (push) Waiting to run
Release / amd64 - chromium - latest (push) Waiting to run
Release / linux/arm64 - chromium - arm (push) Waiting to run
Release / amd64 - none - noweb (push) Waiting to run
Release / linux/arm64 - none - noweb-arm (push) Waiting to run
fix #329
2024-06-01 16:16:19 +07:00
devlikepro d6dc7edf66 [core] WEBJS enable io from chrome if debug enabled 2024-05-31 18:16:34 +07:00
devlikepro 8f6dbf70f7 [core] WEBJS auto restart on FAILED
fix #355
2024-05-31 18:16:34 +07:00
devlikepro bd14d9a71e [core] 2024.5.14 2024-05-31 13:32:57 +07:00
devlikepro 61a0a38dd2 [core] Restart NOWEB every 30 minutes 2024-05-31 12:50:38 +07:00
devlikepro 102f438a99 [core] NOWEB fix for creds.json
fix #347
2024-05-31 12:18:11 +07:00
devlikepro 586a2d1c79 [core] rm import 2024-05-31 11:54:49 +07:00
devlikepro 5b04c39107 [core] move get default engine to EngineConfigService.ts 2024-05-31 11:54:49 +07:00
devlikepro d96e91ea70 [core] Better logging 2024-05-31 11:54:49 +07:00
141 changed files with 9089 additions and 3090 deletions

No files matched your search

+2 -2
View File
@@ -23,8 +23,8 @@ assignees: ''
```
> You can get the WAHA version by calling `GET /api/version` Try to update to
> [the latest version](https://github.com/devlikeapro/whatsapp-http-api/releases)
> before creating an issue!
> [the latest version](https://github.com/devlikeapro/waha/releases) before
> creating an issue!
**To Reproduce** Steps to reproduce the behavior:
+1 -1
View File
@@ -3,7 +3,7 @@
#
OWNER=devlikeapro
REPO=whatsapp-http-api-plus
REPO=waha-plus
# list workflows
gh api -X GET /repos/$OWNER/$REPO/actions/workflows | jq '.workflows[] | .name,.id'
+1
View File
@@ -55,6 +55,7 @@ jobs:
flavor: latest=false
tags: |
type=raw,value=${{ matrix.tag }}
type=raw,value=${{ matrix.tag }}-{{tag}}
- name: Login to image repository
if: github.ref_type == 'tag'
+7
View File
@@ -1,12 +1,19 @@
.yarn
example.ts
.*sessions
.secrets
.minio
.media
tokens
files
venv
.wwebjs_auth
src/test.ts
src/test.js
test.js
.wwebjs_cache
src/dashboard
.eslintcache
# compiled output
/dist
+27 -1
View File
@@ -6,12 +6,21 @@ exclude: |
)$
repos:
- repo: local
hooks:
- id: commit-message-prefix
name: Validate Commit Message for src/plus Changes
entry: python ./.precommit/validate_commit_message.py
language: python
stages: [commit-msg]
- repo: local
hooks:
- id: lint
stages: [pre-commit]
name: lint
language: system
entry: bash -c '$HOME/.nvm/nvm-exec npm run lint'
entry: bash -c '$HOME/.nvm/nvm-exec npm run lint-cache'
pass_filenames: false
files: |
(?x)^(
@@ -22,6 +31,7 @@ repos:
rev: 'v3.1.0'
hooks:
- id: prettier
stages: [pre-commit]
exclude: |
(?x)^(
docs/.*|
@@ -31,11 +41,13 @@ repos:
rev: v1.1.2
hooks:
- id: markdown-toc
stages: [pre-commit]
name: README.md
files: ^README.md$
- repo: local
hooks:
- id: no-plus-in-core
stages: [pre-commit]
name: No "plus" in core
language: pygrep
entry: 'plus'
@@ -46,3 +58,17 @@ repos:
src/structures/.*|
src/config.service.ts|
)$
- repo: local
hooks:
- id: no-console-log
stages: [pre-commit]
name: No console.log() calls
language: pygrep
entry: 'console\.log'
exclude: |
(?x)^(
entrypoint.sh|
.pre-commit-config.yaml|
^src/core/engines/webjs/session.webjs.core.ts|
)$
+38
View File
@@ -0,0 +1,38 @@
import subprocess
import sys
def get_staged_files():
result = subprocess.run(["git", "diff", "--cached", "--name-only"], stdout=subprocess.PIPE, text=True)
files = result.stdout.splitlines()
return files
def get_commit_message(commit_msg_filepath):
with open(commit_msg_filepath, 'r') as f:
return f.readline().strip()
def main():
commit_msg_filepath = sys.argv[1]
commit_message = get_commit_message(commit_msg_filepath)
staged_files = get_staged_files()
has_plus_changes = any(f.startswith('src/plus') for f in staged_files)
print(commit_message)
starts_with_plus = commit_message.startswith('[PLUS]')
if has_plus_changes and not starts_with_plus:
print("'[PLUS]' not found in commit message, but there's changes are from 'src/plus'.")
return 1
if starts_with_plus and not all(f.startswith('src/plus') for f in staged_files):
print("'[PLUS]' found in commit message, but there's changes from other directories. \n"
"Changes MUST be only from 'src/plus'.")
return 1
return 0
if __name__ == "__main__":
sys.exit(main())
+35 -10
View File
@@ -6,35 +6,51 @@ FROM node:${NODE_VERSION} as build
ENV PUPPETEER_SKIP_DOWNLOAD=True
# npm packages
RUN apt-get update \
&& apt-get install -y openssh-client
RUN apt install openssh-client
RUN mkdir -p -m 0700 ~/.ssh && ssh-keyscan github.com >> ~/.ssh/known_hosts
WORKDIR /src
COPY package.json .
COPY yarn.lock .
RUN yarn set version 3.6.3
RUN --mount=type=ssh yarn install
RUN yarn install
# App
WORKDIR /src
ADD . /src
RUN --mount=type=ssh yarn install
RUN yarn install
RUN yarn build && find ./dist -name "*.d.ts" -delete
#
# Dashboard
#
FROM node:${NODE_VERSION} as dashboard
# Download WAHA Dashboard
ENV WAHA_DASHBOARD_SHA c1a91f879a35f0ae32d46ddb2e061ac1819a3d4f
RUN \
wget https://github.com/devlikeapro/dashboard/archive/${WAHA_DASHBOARD_SHA}.zip \
&& unzip ${WAHA_DASHBOARD_SHA}.zip -d /tmp/dashboard \
&& mkdir -p /dashboard \
&& mv /tmp/dashboard/dashboard-${WAHA_DASHBOARD_SHA}/* /dashboard/ \
&& rm -rf ${WAHA_DASHBOARD_SHA}.zip \
&& rm -rf /tmp/dashboard/dashboard-${WAHA_DASHBOARD_SHA}
#
# Final
#
FROM node:${NODE_VERSION} as release
ENV PUPPETEER_SKIP_DOWNLOAD=True
# Quick fix for memory potential memory leaks
# https://github.com/devlikeapro/whatsapp-http-api/issues/347
# https://github.com/devlikeapro/waha/issues/347
ENV NODE_OPTIONS="--max-old-space-size=16384"
ARG USE_BROWSER=chromium
RUN echo "USE_BROWSER=$USE_BROWSER"
# Install ffmpeg to generate previews for videos
RUN apt-get update && apt-get install -y ffmpeg --no-install-recommends && rm -rf /var/lib/apt/lists/*
# Install zip and unzip
RUN apt-get update && apt-get install -y zip unzip --no-install-recommends && rm -rf /var/lib/apt/lists/*
# Install fonts if using either chromium or chrome
RUN if [ "$USE_BROWSER" = "chromium" ] || [ "$USE_BROWSER" = "chrome" ]; then \
apt-get update \
@@ -55,7 +71,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="122.0.6261.128-1"
ARG CHROME_VERSION="126.0.6478.182-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 \
@@ -69,7 +85,16 @@ WORKDIR /app
COPY package.json ./
COPY --from=build /src/node_modules ./node_modules
COPY --from=build /src/dist ./dist
COPY --from=dashboard /dashboard ./dist/dashboard
COPY entrypoint.sh /entrypoint.sh
# Chokidar options to monitor file changes
ENV CHOKIDAR_USEPOLLING=1
ENV CHOKIDAR_INTERVAL=5000
# WAHA variables
ENV WAHA_ZIPPER=ZIPUNZIP
# Run command, etc
EXPOSE 3000
CMD yarn start:prod
CMD ["/entrypoint.sh"]
+13 -12
View File
@@ -1,40 +1,41 @@
build:
docker build . -t devlikeapro/whatsapp-http-api
docker build . -t devlikeapro/waha
build-chrome:
docker build . -t devlikeapro/whatsapp-http-api-plus:chrome --build-arg USE_BROWSER=chrome
docker build . -t devlikeapro/waha-plus:chrome --build-arg USE_BROWSER=chrome
build-noweb:
docker build . -t devlikeapro/whatsapp-http-api:noweb --build-arg USE_BROWSER=none
docker build . -t devlikeapro/waha:noweb --build-arg USE_BROWSER=none
build-all: build build-chrome build-noweb
build-plus:
docker build . -t devlikeapro/whatsapp-http-api-plus
docker build . -t devlikeapro/waha-plus
build-ssh:
# check IMAGE provided
@[ "${IMAGE}" ] || ( echo "Add APP: make build-ssh image=devlikeapro/whatsapp-http-api"; exit 1 );
@[ "${IMAGE}" ] || ( echo "Add APP: make build-ssh image=devlikeapro/waha"; exit 1 );
eval $(ssh-agent) && \
ssh-add ~/.ssh/id_rsa && \
docker buildx build --ssh default=${SSH_AUTH_SOCK} . -t ${IMAGE} --build-arg USE_BROWSER=none
stop:
docker stop whatsapp-http-api
docker stop waha
clean: stop
sudo rm -rf .sessions
push:
docker push devlikeapro/whatsapp-http-api
docker push devlikeapro/waha
for-swagger:
WHATSAPP_SWAGGER_CONFIG_ADVANCED=true npm run start
WHATSAPP_SWAGGER_CONFIG_ADVANCED=true . ${NVM_DIR}/nvm.sh && nvm exec yarn start
update-dependencies:
. ${NVM_DIR}/nvm.sh && nvm exec yarn up whatsapp-web.js@https://github.com/devlikeapro/whatsapp-web.js#main-fork
. ${NVM_DIR}/nvm.sh && nvm exec yarn up @adiwajshing/baileys@github:WhiskeySockets/Baileys
. ${NVM_DIR}/nvm.sh && nvm exec yarn up venom-bot
up-noweb:
. ${NVM_DIR}/nvm.sh && nvm exec yarn up @adiwajshing/baileys@github:devlikeapro/Baileys#fork-master-channels-2
up-webjs:
. ${NVM_DIR}/nvm.sh && nvm exec yarn up whatsapp-web.js@github:devlikeapro/whatsapp-web.js#fork-webpack-exodus-channels
start-proxy:
docker run --rm -d --name squid-container -e TZ=UTC -p 3128:3128 ubuntu/squid:5.2-22.04_beta
+6 -6
View File
@@ -1,4 +1,4 @@
# WhatsApp HTTP API (WAHA)
# WAHA
<p align="center">
<img src="./logo.png" style='border-radius: 50%' width='150'/>
@@ -6,7 +6,7 @@
**WAHA** - **W**hats**A**pp **H**TTP **A**PI (REST API) that you can install on your own server and run in less than 5 minutes!
[![Docker Pulls](https://img.shields.io/docker/pulls/devlikeapro/whatsapp-http-api)](https://hub.docker.com/r/devlikeapro/whatsapp-http-api)
[![Docker Pulls](https://img.shields.io/docker/pulls/devlikeapro/whatsapp-http-api)](https://hub.docker.com/r/devlikeapro/waha)
- Documentation: [https://waha.devlike.pro/](https://waha.devlike.pro/)
- Swagger Example: [https://waha.devlike.pro/swagger](https://waha.devlike.pro/swagger)
@@ -104,13 +104,13 @@ Assuming you have installed [Docker](https://docs.docker.com/get-docker/), let's
```bash
docker pull devlikeapro/whatsapp-http-api
docker pull devlikeapro/waha
```
```bash
docker login -u devlikeapro -p {KEY}
docker pull devlikeapro/whatsapp-http-api-plus
docker pull devlikeapro/waha-plus
docker logout
```
@@ -121,7 +121,7 @@ Read more about how to get `PASSWORD` for [Plus Version →]({{< relref "plus-ve
Run WhatsApp HTTP API:
```bash
docker run -it --rm -p 3000:3000/tcp --name whatsapp-http-api devlikeapro/whatsapp-http-api
docker run -it --rm -p 3000:3000/tcp --name waha devlikeapro/waha
# It prints logs and the last line must be
# WhatsApp HTTP API is running on: http://[::1]:3000
@@ -141,7 +141,7 @@ later:
How to log in - the instruction on WhatsApp site
</a>
When your ready - find `POST /api/session/start`, click on **Try it out**, then **Execute** a bit below.
When your ready - find `POST /api/sessions`, click on **Try it out**, then **Execute** a bit below.
The example payload:
+114
View File
@@ -0,0 +1,114 @@
services:
waha:
# Check available Docker images:
# https://waha.devlike.pro/docs/how-to/engines/#docker-images
image: devlikeapro/waha
container_name: waha
ports:
- '3000:3000/tcp'
environment:
# Environment variables from https://waha.devlike.pro/docs/how-to/config/
# https://docs.webhook.site/
- WHATSAPP_HOOK_URL=https://webhook.site/11111111-1111-1111-1111-11111111
- WHATSAPP_HOOK_EVENTS=message
- WHATSAPP_DEFAULT_ENGINE=WEBJS
restart: always
waha-plus:
# Check available Docker images:
# https://waha.devlike.pro/docs/how-to/engines/#docker-images
image: devlikeapro/waha-plus:chrome
container_name: waha-plus
ports:
- '3000:3000/tcp'
volumes:
- './.sessions:/app/.sessions'
environment:
# Environment variables from https://waha.devlike.pro/docs/how-to/config/
# - WHATSAPP_RESTART_ALL_SESSIONS=True
# - WHATSAPP_HOOK_URL=https://webhook.site/11111111-1111-1111-1111-11111111
# - WHATSAPP_HOOK_EVENTS=message
# - WHATSAPP_API_HOSTNAME=localhost
- WHATSAPP_DEFAULT_ENGINE=WEBJS
- WHATSAPP_API_KEY=321
# Username and password for Swagger
- WHATSAPP_SWAGGER_USERNAME=swagger
- WHATSAPP_SWAGGER_PASSWORD=admin
# Username and password for WAHA Dashboard
# you can use the same username and password for both Swagger and Dashboard
# Default - waha / waha
- WAHA_DASHBOARD_USERNAME=waha
- WAHA_DASHBOARD_PASSWORD=waha
# Engine specific environment variables
# - WAHA_WEBJS_WEB_VERSION=2.2412.54-videofix
restart: always
waha-plus--mongodb:
# Check available Docker images:
# https://waha.devlike.pro/docs/how-to/engines/#docker-images
image: devlikeapro/waha-plus:chrome
container_name: waha-plus--mongodb
ports:
- '3000:3000/tcp'
volumes:
- './.sessions:/app/.sessions'
environment:
# Environment variables from https://waha.devlike.pro/docs/how-to/config/
# - WHATSAPP_RESTART_ALL_SESSIONS=True
# - WHATSAPP_HOOK_URL=https://webhook.site/11111111-1111-1111-1111-11111111
# - WHATSAPP_HOOK_EVENTS=message
# - WHATSAPP_API_KEY=321
# - WHATSAPP_SWAGGER_USERNAME=admin
# - WHATSAPP_SWAGGER_PASSWORD=123
# - WAHA_DASHBOARD_USERNAME=admin
# - WAHA_DASHBOARD_PASSWORD=123
# - WHATSAPP_API_HOSTNAME=localhost
- WHATSAPP_DEFAULT_ENGINE=WEBJS
- WHATSAPP_SESSIONS_MONGO_URL=mongodb://mongodb:27017
restart: always
# Let's Encrypt + Certbot
# https://waha.devlike.pro/docs/how-to/security/#https
waha-https--certbot:
image: devlikeapro/waha-plus
restart: unless-stopped
ports:
- '3000:3000'
volumes:
- './.sessions:/app/.sessions'
- '/etc/letsencrypt:/etc/letsencrypt'
environment:
- DOMAIN=waha.example.pro
- WHATSAPP_API_HOSTNAME=${DOMAIN}
- WAHA_HTTPS_ENABLED=true
- WAHA_HTTPS_PATH_KEY=/etc/letsencrypt/live/${DOMAIN}/privkey.pem
- WAHA_HTTPS_PATH_CERT=/etc/letsencrypt/live/${DOMAIN}/cert.pem
- WAHA_HTTPS_PATH_CA=/etc/letsencrypt/live/${DOMAIN}/chain.pem
# Self-signed certificates example
# https://waha.devlike.pro/docs/how-to/security/#https
waha-https--self-signed:
image: devlikeapro/waha-plus
restart: unless-stopped
ports:
- '3000:3000'
volumes:
- './.sessions:/app/.sessions'
- './.secrets:/app/.secrets'
environment:
- WAHA_HTTPS_ENABLED=true
- WAHA_HTTPS_PATH_KEY=.secrets/privkey.pem
- WAHA_HTTPS_PATH_CERT=.secrets/cert.pem
- WAHA_HTTPS_PATH_CA=.secrets/chain.pem
mongodb:
image: mongo
container_name: mongodb
ports:
- '27017:27017/tcp'
volumes:
- mongodb_data:/data/db
restart: always
volumes:
mongodb_data: {}
+217 -54
View File
@@ -1,72 +1,215 @@
version: '3'
#
# https://waha.devlike.pro/docs/how-to/install/
# Here's we provide a simple example of how to install WAHA using Docker Compose.
#
# We're assuming you have Docker and Docker Compose installed.
# If you don't have Docker Compose installed, you can follow the official guide:
# https://docs.docker.com/compose/install/
#
services:
whatsapp-http-api:
# Check available Docker images:
# https://waha.devlike.pro/docs/how-to/engines/#docker-images
image: devlikeapro/whatsapp-http-api
container_name: whatsapp-http-api
ports:
- '3000:3000/tcp'
environment:
# Environment variables from https://waha.devlike.pro/docs/how-to/config/
- WHATSAPP_HOOK_URL=https://httpbin.org/post
- WHATSAPP_HOOK_EVENTS=message
- WHATSAPP_DEFAULT_ENGINE=WEBJS
waha:
# Always restart the container on failure
restart: always
whatsapp-http-api-plus:
# Check available Docker images:
#
# Image
#
image: devlikeapro/waha-plus:latest # or devlikeapro/waha:latest if you're using WAHA Core
#
# We strongly recommend using the pinned version of WAHA
# https://waha.devlike.pro/docs/how-to/engines/#versions
# image: devlikeapro/waha-plus:latest-2024.7.6 # 'latest' is not mistaken here
#
# Multiple images are available
# https://waha.devlike.pro/docs/how-to/engines/#docker-images
image: devlikeapro/whatsapp-http-api-plus:chrome
container_name: whatsapp-http-api-plus
# image: devlikeapro/waha-plus:chrome
#
# The first port can be any free port you want
# The second port must be the same as WHATSAPP_API_PORT or 3000 by default
# Consider using the same port for simplicity
ports:
- '3000:3000/tcp'
# NOTE: if you're using HTTP, you can use the following port mapping
#- '80:3000/tcp'
# NOTE: if you're using HTTPS, you can use the following port mapping
#- '443:3000/tcp'
volumes:
# Store sessions in the .sessions folder (comment it if you're using MongoDB)
# Doesn't work with WAHA Core
- './.sessions:/app/.sessions'
# https://waha.devlike.pro/docs/how-to/storages/#save-media-files-between-the-container-restarts
- './.media:/app/.media'
# If you're using certbot for HTTPS, you can use the following volume mapping
# https://waha.devlike.pro/blog/setting-up-https-for-waha/#lets-encrypt-certbot--waha
# - '/etc/letsencrypt:/etc/letsencrypt'
# WAHA Configuration
# https://waha.devlike.pro/docs/how-to/config/
environment:
# Environment variables from https://waha.devlike.pro/docs/how-to/config/
# - WHATSAPP_RESTART_ALL_SESSIONS=True
# - WHATSAPP_HOOK_URL=https://httpbin.org/post
# - WHATSAPP_HOOK_EVENTS=message
# - WHATSAPP_API_HOSTNAME=localhost
- WHATSAPP_DEFAULT_ENGINE=WEBJS
#
# Common
#
# If you know the right URL, you can use it here.
# Use any port in WHATSAPP_API_PORT and domain in WHATSAPP_API_HOSTNAME
# https://waha.devlike.pro/docs/how-to/config/#common
#- WHATSAPP_API_SCHEMA=http
#- WHATSAPP_API_PORT=3000
#- WHATSAPP_API_HOSTNAME=localhost
#
# Security
#
# https://waha.devlike.pro/docs/how-to/security/
#
- WHATSAPP_API_KEY=321
# Username and password for Swagger
- WHATSAPP_SWAGGER_USERNAME=swagger
- WHATSAPP_SWAGGER_PASSWORD=admin
# Username and password for WAHA Dashboard
# you can use the same username and password for both Swagger and Dashboard
# Default - waha / waha
- WAHA_DASHBOARD_USERNAME=waha
- WAHA_DASHBOARD_PASSWORD=waha
# Engine specific environment variables
# - WAHA_WEBJS_WEB_VERSION=2.2412.54-videofix
restart: always
#- WHATSAPP_SWAGGER_USERNAME=admin
#- WHATSAPP_SWAGGER_PASSWORD=admin
whatsapp-http-api-plus--mongodb:
# Check available Docker images:
# https://waha.devlike.pro/docs/how-to/engines/#docker-images
image: devlikeapro/whatsapp-http-api-plus:chrome
container_name: whatsapp-http-api-plus--mongodb
ports:
- '3000:3000/tcp'
volumes:
- './.sessions:/app/.sessions'
environment:
# Environment variables from https://waha.devlike.pro/docs/how-to/config/
# - WHATSAPP_RESTART_ALL_SESSIONS=True
# - WHATSAPP_HOOK_URL=https://httpbin.org/post
# - WHATSAPP_HOOK_EVENTS=message
# - WHATSAPP_API_KEY=321
# - WHATSAPP_SWAGGER_USERNAME=admin
# - WHATSAPP_SWAGGER_PASSWORD=123
# - WAHA_DASHBOARD_USERNAME=admin
# - WAHA_DASHBOARD_PASSWORD=123
# - WHATSAPP_API_HOSTNAME=localhost
#
# Dashboard
#
# https://waha.devlike.pro/docs/how-to/waha-dashboard/#configuration
# Enabled by default
#- WAHA_DASHBOARD_ENABLED=true
#- WAHA_DASHBOARD_USERNAME=waha
#- WAHA_DASHBOARD_PASSWORD=waha
#
# Logging
# https://waha.devlike.pro/docs/how-to/observability/#logging
#
- WAHA_LOG_FORMAT=PRETTY
# Set log format to JSON if you consume logs with log management systems
#- WAHA_LOG_FORMAT=JSON
- WAHA_LOG_LEVEL=info
# 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
#
# Choose the right engine for your needs (WEBJS by default)
# https://waha.devlike.pro/docs/how-to/engines/
- WHATSAPP_DEFAULT_ENGINE=WEBJS
- WHATSAPP_SESSIONS_MONGO_URL=mongodb://mongodb:27017
restart: always
# Use NOWEB engine for the fastest performance
#- WHATSAPP_DEFAULT_ENGINE=NOWEB
#
# Sessions
#
# https://waha.devlike.pro/docs/how-to/config/#sessions
# Restart all sessions when container starts
- WHATSAPP_RESTART_ALL_SESSIONS=True
# 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
# Do not use both WHATSAPP_RESTART_ALL_SESSIONS and WHATSAPP_START_SESSION parameters
#
#
# Swagger
#
# https://waha.devlike.pro/docs/how-to/swagger/
#- WHATSAPP_SWAGGER_USERNAME=admin
#- WHATSAPP_SWAGGER_PASSWORD=admin
# Disable Swagger
#- WHATSAPP_SWAGGER_ENABLED=false
# Enable advanced Swagger configuration
#- WHATSAPP_SWAGGER_CONFIG_ADVANCED=true
#
# Swagger White Label customization
# https://waha.devlike.pro/docs/how-to/swagger/#white-label
#- WHATSAPP_SWAGGER_TITLE=WAHA API
#
#
# Global Proxy Settings
# https://waha.devlike.pro/docs/how-to/config/#global-proxy-configuration
# You can also set proxy settings for each session later when starting it
# https://waha.devlike.pro/docs/how-to/sessions/#configure-proxy
#- WHATSAPP_PROXY_SERVER=proxy.example.com:3128
#-WHATSAPP_PROXY_SERVER_USERNAME=user
#-WHATSAPP_PROXY_SERVER_PASSWORD=pass
#
#
# HTTPS Configuration
#
# https://waha.devlike.pro/docs/how-to/config/#https
# Consider using certbot for HTTPS
# https://waha.devlike.pro/blog/setting-up-https-for-waha/#lets-encrypt-certbot--waha
#- WAHA_HTTPS_ENABLED=true
#- WAHA_HTTPS_PATH_KEY=/etc/letsencrypt/live/waha.example.pro/privkey.pem
#- WAHA_HTTPS_PATH_CERT=/etc/letsencrypt/live/waha.example.pro/cert.pem \
#- WAHA_HTTPS_PATH_CA=/etc/letsencrypt/live/waha.example.pro/chain.pem
#- WHATSAPP_API_SCHEMA=https
#- WHATSAPP_API_PORT=3000
#- WHATSAPP_API_HOSTNAME=waha.example.pro
#
#
# Media Configuration
#
# 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
#
# 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
- 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
#
# 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
#
#
# MongoDB Configuration
# https://waha.devlike.pro/docs/how-to/storages/#sessions---mongodb
#- WHATSAPP_SESSIONS_MONGO_URL=mongodb://mongodb:27017
#
# NOTE: Only if you're using MongoDB
# https://waha.devlike.pro/docs/how-to/storages/#sessions---mongodb
# Comment this block if you're using MongoDB
mongodb:
image: mongo
container_name: mongodb
@@ -76,5 +219,25 @@ services:
- mongodb_data:/data/db
restart: always
# NOTE: Only if you're using S3 to save media files
# https://waha.devlike.pro/docs/how-to/storages/#media---s3
# Comment this block if you're using AWS S3
minio:
image: quay.io/minio/minio
container_name: minio
restart: always
ports:
- '9000:9000'
- '9001:9001'
environment:
MINIO_REGION: 'eu-west-2'
MINIO_ROOT_USER: 'minioadmin'
MINIO_ROOT_PASSWORD: 'minioadmin'
volumes:
- minio_data:/data
command: server /data --console-address ":9001"
volumes:
# NOTE: Only if you're using MongoDB
mongodb_data: {}
minio_data: {}
Executable
+11
View File
@@ -0,0 +1,11 @@
#!/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 * 2))
# Set UV_THREADPOOL_SIZE as an environment variable
export UV_THREADPOOL_SIZE="${UV_THREADPOOL_SIZE:-$uv_threadpool_size}"
# Start your application using yarn
yarn start:prod
Binary file not shown.
+14 -14
View File
@@ -19,9 +19,9 @@ Download and start WhatsApp HTTP API docker container
```bash
# Download the image
docker pull devlikeapro/whatsapp-http-api
docker pull devlikeapro/waha
# Run the docker container
docker run -it --rm --network=host -e WHATSAPP_HOOK_URL=http://localhost:5000/bot -e "WHATSAPP_HOOK_EVENTS=*" --name whatsapp-http-api devlikeapro/whatsapp-http-api
docker run -it --rm --network=host -e WHATSAPP_HOOK_URL=http://localhost:5000/bot -e "WHATSAPP_HOOK_EVENTS=*" --name waha devlikeapro/waha
# It prints logs and the last line must be
# WhatsApp HTTP API is running on: http://[::1]:3000
@@ -36,11 +36,11 @@ commands to download the image
```bash
# Download the image
docker pull devlikeapro/whatsapp-http-api:arm
# Rename it, so you can use devlikeapro/whatsapp-http-api image in other place
docker tag devlikeapro/whatsapp-http-api:arm devlikeapro/whatsapp-http-api
docker pull devlikeapro/waha:arm
# Rename it, so you can use devlikeapro/waha image in other place
docker tag devlikeapro/waha:arm devlikeapro/waha
# Run the docker container
docker run -it --rm --network=host -e WHATSAPP_HOOK_URL=http://localhost:5000/bot -e "WHATSAPP_HOOK_EVENTS=*" --name whatsapp-http-api devlikeapro/whatsapp-http-api
docker run -it --rm --network=host -e WHATSAPP_HOOK_URL=http://localhost:5000/bot -e "WHATSAPP_HOOK_EVENTS=*" --name waha devlikeapro/waha
# It prints logs and the last line must be
# WhatsApp HTTP API is running on: http://[::1]:3000
@@ -50,8 +50,8 @@ docker run -it --rm --network=host -e WHATSAPP_HOOK_URL=http://localhost:5000/bo
2. Open Swagger API in the browser http://localhost:3000/
3. Start session and scan QR code in swagger
1. Find `POST /api/session/start` request press **Try it out** and
**Execute** it with `default` session name
1. Find `POST /api/sessions/` request press **Try it out** and **Execute** it
with `default` session name
2. Find `GET /api/screenshot` and execute it - it must show QR code
3. Scan QR code on your mobile WhatsApp application (that installed on your
phone)
@@ -79,9 +79,9 @@ set up webhooks and reply to incoming messages.
```bash
# Clone the git repository with example
git clone https://github.com/devlikeapro/whatsapp-http-api.git
git clone https://github.com/devlikeapro/waha.git
# Open python example folder
cd whatsapp-http-api/examples/python
cd waha/examples/python
# Install requirements
python -mpip install -r requirements.txt
# Run the bot
@@ -106,18 +106,18 @@ Download **WAHA Plus** version:
```bash
# Download the image
docker pull devlikeapro/whatsapp-http-api-plus
docker pull devlikeapro/waha-plus
# Run the docker container
docker run -it --rm --network=host -e WHATSAPP_HOOK_URL=http://localhost:5000/bot -e "WHATSAPP_HOOK_EVENTS=*" --name whatsapp-http-api devlikeapro/whatsapp-http-api-plus
docker run -it --rm --network=host -e WHATSAPP_HOOK_URL=http://localhost:5000/bot -e "WHATSAPP_HOOK_EVENTS=*" --name waha devlikeapro/waha-plus
```
Run the WhatsApp Download Files bot:
```bash
# Clone the git repository with example
git clone https://github.com/devlikeapro/whatsapp-http-api.git
git clone https://github.com/devlikeapro/waha.git
# Open python example folder
cd whatsapp-http-api/examples/python
cd waha/examples/python
# Install requirements
python -mpip install -r requirements.txt
# Run the bot
+25 -5
View File
@@ -1,5 +1,5 @@
{
"name": "whatsapp-http-api",
"name": "waha",
"version": "0.0.1",
"description": "",
"author": "",
@@ -15,6 +15,7 @@
"start:prod": "node dist/main",
"start:prod-exit": "node dist/main",
"lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix --max-warnings=0",
"lint-cache": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix --cache --max-warnings=0",
"test": "jest",
"test:watch": "jest --watch",
"test:cov": "jest --coverage",
@@ -22,32 +23,50 @@
"test:e2e": "jest --config ./test/jest-e2e.json"
},
"dependencies": {
"@adiwajshing/baileys": "github:WhiskeySockets/Baileys",
"@adiwajshing/baileys": "github:devlikeapro/Baileys#fork-master-channels-2",
"@adiwajshing/keyed-db": "^0.2.4",
"@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/config": "^0.5.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/swagger": "^7.1.11",
"@nestjs/terminus": "^10.2.3",
"@nestjs/websockets": "^9.0.9",
"@types/better-sqlite3": "^7.6.10",
"@types/lodash": "^4.14.194",
"@types/ws": "^8.5.4",
"adm-zip": "0.5.10",
"async-lock": "^1.4.1",
"better-sqlite3": "9.6.0",
"check-disk-space": "^3.4.0",
"class-validator": "^0.12.2",
"chokidar": "^3.6.0",
"class-transformer": "^0.5.1",
"class-validator": "0.14.0",
"del": "^6.0.0",
"express-basic-auth": "^1.2.1",
"file-type": "16.5.4",
"fs-extra": "^11.2.0",
"https-proxy-agent": "^7.0.0",
"joi": "^17.13.3",
"knex": "^3.1.0",
"libphonenumber-js": "^1.10.36",
"link-preview-js": "^3.0.4",
"lodash": "^4.17.21",
"mime-types": "^2.1.27",
"mongodb": "^6.3.0",
"nestjs-pino": "^4.1.0",
"passport": "^0.6.0",
"passport-headerapikey": "^1.2.2",
"pino-http": "^10.2.0",
"pino-pretty": "^11.2.1",
"pretty-bytes": "5.6.0",
"promise-retry": "^2.0.1",
"puppeteer": "^21.0.0",
"qrcode": "^1.5.1",
"qrcode-terminal": "^0.12.0",
@@ -56,9 +75,10 @@
"requestretry": "^4.1.1",
"rimraf": "^3.0.2",
"rxjs": "^7.1.0",
"sharp": "^0.33.4",
"swagger-ui-express": "^4.1.4",
"venom-bot": "5.0.1",
"whatsapp-web.js": "https://github.com/devlikeapro/whatsapp-web.js#main-fork-update"
"whatsapp-web.js": "github:devlikeapro/whatsapp-web.js#fork-webpack-exodus-channels"
},
"devDependencies": {
"@nestjs/cli": "^9.0.0",
+23
View File
@@ -0,0 +1,23 @@
#!/bin/bash
set -e
echo "Starting WAHA..."
docker run -d \
--restart=always \
--name waha \
-p 3000:3000/tcp \
-v ./.sessions:/app/.sessions \
-v ./.media:/app/.media \
--env WHATSAPP_API_KEY=321 \
--env WAHA_DASHBOARD_USERNAME=waha \
--env WAHA_DASHBOARD_PASSWORD=waha \
--env WAHA_LOG_FORMAT=PRETTY \
--env WAHA_LOG_LEVEL=info \
--env WHATSAPP_DEFAULT_ENGINE=WEBJS \
--env WHATSAPP_RESTART_ALL_SESSIONS=True \
--env WAHA_PRINT_QR=False \
--env WHATSAPP_FILES_LIFETIME=0 \
--env WHATSAPP_FILES_FOLDER=/app/.media \
devlikeapro/waha-plus:latest
echo "WAHA started."
+21
View File
@@ -0,0 +1,21 @@
set -e
echo "Updating WAHA..."
echo "Logging in to Docker..."
docker login -u devlikeapro # -p {KEY} - you can hard code key here https://portal.devlike.pro/
echo "Pulling the latest image..."
docker pull devlikeapro/waha-plus
echo "Logging out of Docker..."
docker logout
# Just pull if you're using WAHA Core
# docker pull devlikeapro/waha
echo "Stopping the current container..."
docker stop waha || true
echo "Removing the current container..."
docker rm waha || true
echo "Running WAHA..."
sh ./waha-run.sh
+8 -4
View File
@@ -8,9 +8,15 @@ import {
} from '@nestjs/common';
import { UnprocessableEntityException } from '@nestjs/common/exceptions/unprocessable-entity.exception';
import { ApiOperation, ApiSecurity, ApiTags } from '@nestjs/swagger';
import { ApiFileAcceptHeader } from '@waha/nestjs/ApiFileAcceptHeader';
import {
SessionApiParam,
SessionParam,
} from '@waha/nestjs/params/SessionApiParam';
import { SessionManager } from '../core/abc/manager.abc';
import { WhatsappSession } from '../core/abc/session.abc';
import { BufferResponseInterceptor } from '../nestjs/BufferResponseInterceptor';
import {
CaptchaBody,
OTPRequest,
@@ -21,18 +27,16 @@ import {
} from '../structures/auth.dto';
import { WAHASessionStatus } from '../structures/enums.dto';
import { Base64File } from '../structures/files.dto';
import { BufferResponseInterceptor } from './BufferResponseInterceptor';
import { ApiFileAcceptHeader, SessionApiParam, SessionParam } from './helpers';
@ApiSecurity('api_key')
@Controller('api/:session/auth')
@ApiTags('auth')
@ApiTags('🔑 Auth')
class AuthController {
constructor(private manager: SessionManager) {}
@Get('qr')
@ApiOperation({
summary: 'Get QR code for pairing WhatsApp Web.',
summary: 'Get QR code for pairing WhatsApp API.',
})
@SessionApiParam
@ApiFileAcceptHeader(Base64File, QRCodeValue)
+125
View File
@@ -0,0 +1,125 @@
import {
Body,
Controller,
Delete,
Get,
Param,
Post,
Query,
} from '@nestjs/common';
import { ApiOperation, ApiSecurity, ApiTags } from '@nestjs/swagger';
import {
SessionApiParam,
SessionParam,
} from '@waha/nestjs/params/SessionApiParam';
import {
Channel,
CreateChannelRequest,
ListChannelsQuery,
NewsletterIdApiParam,
NewsletterIdOrInviteCodeApiParam,
} from '@waha/structures/channels.dto';
import { SessionManager } from '../core/abc/manager.abc';
import { isNewsletter, WhatsappSession } from '../core/abc/session.abc';
@ApiSecurity('api_key')
@Controller('api/:session/channels')
@ApiTags('📢 Channels')
export class ChannelsController {
constructor(private manager: SessionManager) {}
@Get('')
@SessionApiParam
@ApiOperation({ summary: 'Get list of know channels' })
async list(
@SessionParam session: WhatsappSession,
@Query() query: ListChannelsQuery,
): Promise<Channel[]> {
return session.channelsList(query);
}
@Post('')
@SessionApiParam
@ApiOperation({ summary: 'Create a new channel.' })
create(
@SessionParam session: WhatsappSession,
@Body() request: CreateChannelRequest,
): Promise<Channel> {
return session.channelsCreateChannel(request);
}
@Delete(':id')
@SessionApiParam
@NewsletterIdApiParam
@ApiOperation({ summary: 'Delete the channel.' })
delete(@SessionParam session: WhatsappSession, @Param('id') id: string) {
return session.channelsDeleteChannel(id);
}
@Get(':id')
@SessionApiParam
@NewsletterIdOrInviteCodeApiParam
@ApiOperation({
summary: 'Get the channel info',
description:
'You can use either id (123@newsletter) ' +
'OR invite code (https://www.whatsapp.com/channel/123)',
})
get(
@SessionParam session: WhatsappSession,
@Param('id') id: string,
): Promise<Channel> {
if (isNewsletter(id)) {
return session.channelsGetChannel(id);
} else {
// https://www.whatsapp.com/channel/123 => 123
const inviteCode = id.split('/').pop();
return session.channelsGetChannelByInviteCode(inviteCode);
}
}
@Post('follow')
@SessionApiParam
@NewsletterIdApiParam
@ApiOperation({ summary: 'Follow the channel.' })
follow(
@SessionParam session: WhatsappSession,
@Param('id') id: string,
): Promise<void> {
return session.channelsFollowChannel(id);
}
@Post('unfollow')
@SessionApiParam
@NewsletterIdApiParam
@ApiOperation({ summary: 'Unfollow the channel.' })
unfollow(
@SessionParam session: WhatsappSession,
@Param('id') id: string,
): Promise<void> {
return session.channelsUnfollowChannel(id);
}
@Post('mute')
@SessionApiParam
@NewsletterIdApiParam
@ApiOperation({ summary: 'Mute the channel.' })
mute(
@SessionParam session: WhatsappSession,
@Param('id') id: string,
): Promise<void> {
return session.channelsMuteChannel(id);
}
@Post('unmute')
@SessionApiParam
@NewsletterIdApiParam
@ApiOperation({ summary: 'Unmute the channel.' })
unmute(
@SessionParam session: WhatsappSession,
@Param('id') id: string,
): Promise<void> {
return session.channelsUnmuteChannel(id);
}
}
+42 -11
View File
@@ -7,37 +7,44 @@ import {
Post,
Put,
Query,
UsePipes,
ValidationPipe,
} from '@nestjs/common';
import { ApiOperation, ApiSecurity, ApiTags } from '@nestjs/swagger';
import { ChatIdApiParam } from '@waha/nestjs/params/ChatIdApiParam';
import { MessageIdApiParam } from '@waha/nestjs/params/MessageIdApiParam';
import {
SessionApiParam,
SessionParam,
} from '@waha/nestjs/params/SessionApiParam';
import { SessionManager } from '../core/abc/manager.abc';
import { WhatsappSession } from '../core/abc/session.abc';
import { parseBool } from '../helpers';
import { GetChatMessagesQuery } from '../structures/chats.dto';
import { GetChatMessagesQuery, GetChatsQuery } from '../structures/chats.dto';
import { EditMessageRequest } from '../structures/chatting.dto';
import {
ChatIdApiParam,
MessageIdApiParam,
SessionApiParam,
SessionParam,
} from './helpers';
@ApiSecurity('api_key')
@Controller('api/:session/chats')
@ApiTags('chats')
@ApiTags('💬 Chats')
@UsePipes(new ValidationPipe({ transform: true }))
class ChatsController {
constructor(private manager: SessionManager) {}
@Get('')
@SessionApiParam
@ApiOperation({ summary: 'Get all chats' })
getAllChats(@SessionParam session: WhatsappSession) {
return session.getChats();
@ApiOperation({ summary: 'Get chats' })
getChats(
@SessionParam session: WhatsappSession,
@Query() query: GetChatsQuery,
) {
return session.getChats(query);
}
@Delete(':chatId')
@SessionApiParam
@ApiOperation({ summary: 'Deletes the chat' })
@ChatIdApiParam
deleteChat(
@SessionParam session: WhatsappSession,
@Param('chatId') chatId: string,
@@ -48,6 +55,7 @@ class ChatsController {
@Get(':chatId/messages')
@SessionApiParam
@ApiOperation({ summary: 'Gets messages in the chat' })
@ChatIdApiParam
getChatMessages(
@Query() query: GetChatMessagesQuery,
@SessionParam session: WhatsappSession,
@@ -60,6 +68,7 @@ class ChatsController {
@Delete(':chatId/messages')
@SessionApiParam
@ApiOperation({ summary: 'Clears all messages from the chat' })
@ChatIdApiParam
clearMessages(
@SessionParam session: WhatsappSession,
@Param('chatId') chatId: string,
@@ -93,6 +102,28 @@ class ChatsController {
) {
return session.editMessage(chatId, messageId, body);
}
@Post(':chatId/archive')
@SessionApiParam
@ChatIdApiParam
@ApiOperation({ summary: 'Archive the chat' })
archiveChat(
@SessionParam session: WhatsappSession,
@Param('chatId') chatId: string,
) {
return session.chatsArchiveChat(chatId);
}
@Post(':chatId/unarchive')
@SessionApiParam
@ChatIdApiParam
@ApiOperation({ summary: 'Unarchive the chat' })
unarchiveChat(
@SessionParam session: WhatsappSession,
@Param('chatId') chatId: string,
) {
return session.chatsUnarchiveChat(chatId);
}
}
export { ChatsController };
+75 -62
View File
@@ -26,35 +26,10 @@ import { WAMessage } from '../structures/responses.dto';
@ApiSecurity('api_key')
@Controller('api')
@ApiTags('chatting')
@ApiTags('📤 Chatting')
export class ChattingController {
constructor(private manager: SessionManager) {}
@Get('/checkNumberStatus')
@ApiOperation({ summary: 'Check number status' })
async checkNumberStatus(
@Query() request: CheckNumberStatusQuery,
): Promise<WANumberExistResult> {
const whatsapp = this.manager.getSession(request.session);
return whatsapp.checkNumberStatus(request);
}
@Post('/sendContactVcard')
sendContactVcard(@Body() request: MessageContactVcardRequest) {
const whatsapp = this.manager.getSession(request.session);
return whatsapp.sendContactVCard(request);
}
@Get('/sendText')
@ApiOperation({ summary: 'Send a text message' })
sendTextGet(@Query() query: MessageTextQuery) {
const whatsapp = this.manager.getSession(query.session);
const msg = new MessageTextRequest();
msg.chatId = query.phone;
msg.text = query.text;
return whatsapp.sendText(msg);
}
@Post('/sendText')
@ApiOperation({ summary: 'Send a text message' })
sendText(@Body() request: MessageTextRequest): Promise<WAMessage> {
@@ -62,31 +37,11 @@ export class ChattingController {
return whatsapp.sendText(request);
}
@Post('/sendPoll')
@ApiOperation({
summary: 'Send a poll with options. You can use it as buttons replace',
})
sendPoll(@Body() request: MessagePollRequest) {
const whatsapp = this.manager.getSession(request.session);
return whatsapp.sendPoll(request);
}
@Post('/sendLocation')
sendLocation(@Body() request: MessageLocationRequest) {
const whatsapp = this.manager.getSession(request.session);
return whatsapp.sendLocation(request);
}
@Post('/sendLinkPreview')
sendLinkPreview(@Body() request: MessageLinkPreviewRequest) {
const whatsapp = this.manager.getSession(request.session);
return whatsapp.sendLinkPreview(request);
}
@Post('/sendImage')
@ApiOperation({
summary:
'Send an image. Either from an URL or base64 data - look at the request schemas for details.',
summary: 'Send an image',
description:
'Either from an URL or base64 data - look at the request schemas for details.',
})
sendImage(@Body() request: MessageImageRequest) {
const whatsapp = this.manager.getSession(request.session);
@@ -95,8 +50,9 @@ export class ChattingController {
@Post('/sendFile')
@ApiOperation({
summary:
'Send a file. Either from an URL or base64 data - look at the request schemas for details.',
summary: 'Send a file',
description:
'Either from an URL or base64 data - look at the request schemas for details.',
})
sendFile(@Body() request: MessageFileRequest) {
const whatsapp = this.manager.getSession(request.session);
@@ -105,8 +61,9 @@ export class ChattingController {
@Post('/sendVoice')
@ApiOperation({
summary:
'Send an voice message. Either from an URL or base64 data - look at the request schemas for details.',
summary: 'Send an voice message',
description:
'Either from an URL or base64 data - look at the request schemas for details.',
})
sendVoice(@Body() request: MessageVoiceRequest) {
const whatsapp = this.manager.getSession(request.session);
@@ -115,21 +72,15 @@ export class ChattingController {
@Post('/sendVideo')
@ApiOperation({
summary:
'Send a video. Either from an URL or base64 data - look at the request schemas for details.',
summary: 'Send a video',
description:
'Either from an URL or base64 data - look at the request schemas for details.',
})
sendVideo(@Body() request: MessageVideoRequest) {
const whatsapp = this.manager.getSession(request.session);
return whatsapp.sendVideo(request);
}
@Post('/reply')
@ApiOperation({ summary: 'Reply to a text message' })
reply(@Body() request: MessageReplyRequest) {
const whatsapp = this.manager.getSession(request.session);
return whatsapp.reply(request);
}
@Post('/sendSeen')
sendSeen(@Body() chat: SendSeenRequest) {
const whatsapp = this.manager.getSession(chat.session);
@@ -166,10 +117,72 @@ export class ChattingController {
return;
}
@Post('/sendPoll')
@ApiOperation({
summary: 'Send a poll with options',
description: 'You can use it as buttons or list replacement',
})
sendPoll(@Body() request: MessagePollRequest) {
const whatsapp = this.manager.getSession(request.session);
return whatsapp.sendPoll(request);
}
@Post('/sendLocation')
sendLocation(@Body() request: MessageLocationRequest) {
const whatsapp = this.manager.getSession(request.session);
return whatsapp.sendLocation(request);
}
@Post('/sendLinkPreview')
sendLinkPreview(@Body() request: MessageLinkPreviewRequest) {
const whatsapp = this.manager.getSession(request.session);
return whatsapp.sendLinkPreview(request);
}
@Get('/messages')
@ApiOperation({ summary: 'Get messages in a chat' })
getMessages(@Query() query: GetMessageQuery) {
const whatsapp = this.manager.getSession(query.session);
return whatsapp.getMessages(query);
}
@Get('/sendText')
@ApiOperation({ summary: 'Send a text message', deprecated: true })
sendTextGet(@Query() query: MessageTextQuery) {
const whatsapp = this.manager.getSession(query.session);
const msg = new MessageTextRequest();
msg.chatId = query.phone;
msg.text = query.text;
return whatsapp.sendText(msg);
}
@Post('/sendContactVcard')
sendContactVcard(@Body() request: MessageContactVcardRequest) {
const whatsapp = this.manager.getSession(request.session);
return whatsapp.sendContactVCard(request);
}
@Get('/checkNumberStatus')
@ApiOperation({
summary: 'Check number status',
description: 'DEPRECATED. Use "POST /contacts/check-exists" instead',
deprecated: true,
})
async DEPRECATED_checkNumberStatus(
@Query() request: CheckNumberStatusQuery,
): Promise<WANumberExistResult> {
const whatsapp = this.manager.getSession(request.session);
return whatsapp.checkNumberStatus(request);
}
@Post('/reply')
@ApiOperation({
summary:
'DEPRECATED - you can set "reply_to" field when sending text, image, etc',
deprecated: true,
})
reply(@Body() request: MessageReplyRequest) {
const whatsapp = this.manager.getSession(request.session);
return whatsapp.reply(request);
}
}
+10 -7
View File
@@ -11,14 +11,15 @@ import { ContactQuery, ContactRequest } from '../structures/contacts.dto';
@ApiSecurity('api_key')
@Controller('api/contacts')
@ApiTags('contacts')
@ApiTags('👤 Contacts')
export class ContactsController {
constructor(private manager: SessionManager) {}
@Get('/')
@ApiOperation({
summary:
'Get contact basic info. The method always return result, even if the phone number is not registered in WhatsApp. For that - use /check-exists endpoint below.',
summary: 'Get contact basic info',
description:
'The method always return result, even if the phone number is not registered in WhatsApp. For that - use /contacts/check-exists endpoint below.',
})
get(@Query() query: ContactQuery) {
const whatsapp = this.manager.getSession(query.session);
@@ -43,8 +44,9 @@ export class ContactsController {
@Get('/about')
@ApiOperation({
summary:
'Gets the Contact\'s current "about" info. Returns null if you don\'t have permission to read their status.',
summary: 'Gets the Contact\'s "about" info',
description:
'Returns null if you do not have permission to read their status.',
})
getAbout(@Query() query: ContactQuery) {
const whatsapp = this.manager.getSession(query.session);
@@ -53,8 +55,9 @@ export class ContactsController {
@Get('/profile-picture')
@ApiOperation({
summary:
"Returns the contact's profile picture URL, if privacy settings allow it.",
summary: "Get contact's profile picture URL",
description:
'If privacy settings do not allow to get the picture, the method will return null.',
})
getProfilePicture(@Query() query: ContactQuery) {
const whatsapp = this.manager.getSession(query.session);
+43 -19
View File
@@ -8,6 +8,11 @@ import {
Put,
} from '@nestjs/common';
import { ApiOperation, ApiSecurity, ApiTags } from '@nestjs/swagger';
import { GroupIdApiParam } from '@waha/nestjs/params/ChatIdApiParam';
import {
SessionApiParam,
SessionParam,
} from '@waha/nestjs/params/SessionApiParam';
import { SessionManager } from '../core/abc/manager.abc';
import { WhatsappSession } from '../core/abc/session.abc';
@@ -18,11 +23,10 @@ import {
SettingsSecurityChangeInfo,
SubjectRequest,
} from '../structures/groups.dto';
import { SessionApiParam, SessionParam } from './helpers';
@ApiSecurity('api_key')
@Controller('api/:session/groups')
@ApiTags('groups')
@ApiTags('👥 Groups')
export class GroupsController {
constructor(private manager: SessionManager) {}
@@ -44,6 +48,7 @@ export class GroupsController {
}
@Get(':id')
@GroupIdApiParam
@SessionApiParam
@ApiOperation({ summary: 'Get the group.' })
getGroup(@SessionParam session: WhatsappSession, @Param('id') id: string) {
@@ -52,9 +57,11 @@ export class GroupsController {
@Put(':id/settings/security/info-admin-only')
@SessionApiParam
@GroupIdApiParam
@ApiOperation({
summary:
'Updates the group settings to only allow admins to edit group info (title, description, photo).',
summary: 'Updates the group "info admin only" settings.',
description:
'You can allow only admins to edit group info (title, description, photo).',
})
setInfoAdminOnly(
@SessionParam session: WhatsappSession,
@@ -66,9 +73,11 @@ export class GroupsController {
@Get(':id/settings/security/info-admin-only')
@SessionApiParam
@GroupIdApiParam
@ApiOperation({
summary:
'Gets the group settings to only allow admins to edit group info (title, description, photo).',
summary: "Get the group's 'info admin only' settings.",
description:
'You can allow only admins to edit group info (title, description, photo).',
})
getInfoAdminOnly(
@SessionParam session: WhatsappSession,
@@ -79,8 +88,10 @@ export class GroupsController {
@Put(':id/settings/security/messages-admin-only')
@SessionApiParam
@GroupIdApiParam
@ApiOperation({
summary:
summary: 'Update settings - who can send messages',
description:
'Updates the group settings to only allow admins to send messages.',
})
setMessagesAdminOnly(
@@ -93,8 +104,10 @@ export class GroupsController {
@Get(':id/settings/security/messages-admin-only')
@SessionApiParam
@GroupIdApiParam
@ApiOperation({
summary: 'Gets the group settings to only allow admins to send messages.',
summary: 'Get settings - who can send messages',
description: 'The group settings to only allow admins to send messages.',
})
getMessagesAdminOnly(
@SessionParam session: WhatsappSession,
@@ -105,6 +118,7 @@ export class GroupsController {
@Delete(':id')
@SessionApiParam
@GroupIdApiParam
@ApiOperation({ summary: 'Delete the group.' })
deleteGroup(@SessionParam session: WhatsappSession, @Param('id') id: string) {
return session.deleteGroup(id);
@@ -112,6 +126,7 @@ export class GroupsController {
@Post(':id/leave')
@SessionApiParam
@GroupIdApiParam
@ApiOperation({ summary: 'Leave the group.' })
leaveGroup(@SessionParam session: WhatsappSession, @Param('id') id: string) {
return session.leaveGroup(id);
@@ -119,11 +134,12 @@ export class GroupsController {
@Put(':id/description')
@ApiOperation({
summary:
'Updates the group description.\n' +
'Returns true if the subject was properly updated. This can return false if the user does not have the necessary permissions.\n',
summary: 'Updates the group description.',
description:
'Returns "true" if the subject was properly updated. This can return "false" if the user does not have the necessary permissions.',
})
@SessionApiParam
@GroupIdApiParam
setDescription(
@SessionParam session: WhatsappSession,
@Param('id') id: string,
@@ -134,10 +150,11 @@ export class GroupsController {
@Put(':id/subject')
@SessionApiParam
@GroupIdApiParam
@ApiOperation({
summary:
'Updates the group subject.\n' +
'Returns true if the subject was properly updated. This can return false if the user does not have the necessary permissions.\n',
summary: 'Updates the group subject',
description:
'Returns "true" if the subject was properly updated. This can return "false" if the user does not have the necessary permissions.',
})
setSubject(
@SessionParam session: WhatsappSession,
@@ -149,7 +166,8 @@ export class GroupsController {
@Get(':id/invite-code')
@SessionApiParam
@ApiOperation({ summary: 'Gets the invite code for a specific group.' })
@GroupIdApiParam
@ApiOperation({ summary: 'Gets the invite code for the group.' })
getInviteCode(
@SessionParam session: WhatsappSession,
@Param('id') id: string,
@@ -159,6 +177,7 @@ export class GroupsController {
@Post(':id/invite-code/revoke')
@SessionApiParam
@GroupIdApiParam
@ApiOperation({
summary:
'Invalidates the current group invite code and generates a new one.',
@@ -172,7 +191,8 @@ export class GroupsController {
@Get(':id/participants/')
@SessionApiParam
@ApiOperation({ summary: 'Get a list of participants by in the group.' })
@GroupIdApiParam
@ApiOperation({ summary: 'Get participants' })
getParticipants(
@SessionParam session: WhatsappSession,
@Param('id') id: string,
@@ -182,7 +202,8 @@ export class GroupsController {
@Post(':id/participants/add')
@SessionApiParam
@ApiOperation({ summary: 'Adds a list of participants by ID to the group.' })
@GroupIdApiParam
@ApiOperation({ summary: 'Add participants' })
addParticipants(
@SessionParam session: WhatsappSession,
@Param('id') id: string,
@@ -193,8 +214,9 @@ export class GroupsController {
@Post(':id/participants/remove')
@SessionApiParam
@GroupIdApiParam
@ApiOperation({
summary: 'Removes a list of participants by ID to the group.',
summary: 'Remove participants',
})
removeParticipants(
@SessionParam session: WhatsappSession,
@@ -206,6 +228,7 @@ export class GroupsController {
@Post(':id/admin/promote')
@SessionApiParam
@GroupIdApiParam
@ApiOperation({ summary: 'Promote participants to admin users.' })
promoteToAdmin(
@SessionParam session: WhatsappSession,
@@ -217,7 +240,8 @@ export class GroupsController {
@Post(':id/admin/demote')
@SessionApiParam
@ApiOperation({ summary: 'Demotes participants by to regular users.' })
@GroupIdApiParam
@ApiOperation({ summary: 'Demotes participants to regular users.' })
demoteToAdmin(
@SessionParam session: WhatsappSession,
@Param('id') id: string,
+8 -2
View File
@@ -1,16 +1,22 @@
import { Controller, Get } from '@nestjs/common';
import { ApiTags } from '@nestjs/swagger';
import { ApiOperation, ApiSecurity, ApiTags } from '@nestjs/swagger';
import { HealthCheck } from '@nestjs/terminus';
import { WAHAHealthCheckService } from '../core/abc/WAHAHealthCheckService';
@ApiSecurity('api_key')
@Controller('health')
@ApiTags('other')
@ApiTags('🔍 Observability')
export class HealthController {
constructor(private wahaHealth: WAHAHealthCheckService) {}
@Get()
@HealthCheck()
@ApiOperation({
summary: 'Check the health of the server',
description:
"Perform all health checks and return the server's health status.",
})
async check() {
return this.wahaHealth.check();
}
-96
View File
@@ -1,96 +0,0 @@
import {
applyDecorators,
Injectable,
Param,
PipeTransform,
} from '@nestjs/common';
import {
ApiExtraModels,
ApiParam,
ApiResponse,
getSchemaPath,
} from '@nestjs/swagger';
import { SessionManager } from '../core/abc/manager.abc';
import { WhatsappSession } from '../core/abc/session.abc';
import { Base64File } from '../structures/files.dto';
/**
* Get session name and return Whatsapp session back (if exists)
* use it as
@Param('session', SessionPipe) session: WhatsappSession,
*/
@Injectable()
export class SessionPipe implements PipeTransform<WhatsappSession> {
constructor(private manager: SessionManager) {}
async transform(value: any) {
return this.manager.getSession(value);
}
}
/**
* Decorator for a method that uses SessionPipe above
*/
export const SessionApiParam = ApiParam({
name: 'session',
required: true,
type: 'string',
schema: {
default: 'default',
},
description: 'WhatsApp session name',
});
/**
* Session param
@SessionParam session: WhatsappSession,
*/
export const SessionParam = Param('session', SessionPipe);
export const ChatIdApiParam = ApiParam({
name: 'chatId',
required: true,
type: 'string',
description: 'Chat ID',
example: '123456789@c.us',
});
export const MessageIdApiParam = ApiParam({
name: 'messageId',
required: true,
type: 'string',
description: 'Message ID',
example: 'true_123456789@c.us_BAE6A33293978B16',
});
function getRefSchemaPaths(models) {
return models.map((model) => {
return { $ref: getSchemaPath(model) };
});
}
export function ApiFileAcceptHeader(...models) {
models = models.length ? models : [Base64File];
return applyDecorators(
// Add extra models, otherwise it'll give a error
// $ref not found
ApiExtraModels(...models),
ApiResponse({
status: 200,
content: {
'image/png': {
schema: {
type: 'string',
format: 'binary',
},
},
'application/json': {
schema: {
oneOf: getRefSchemaPaths(models),
},
},
},
}),
);
}
+58
View File
@@ -0,0 +1,58 @@
import { Body, Controller, Get, Param, Put } from '@nestjs/common';
import { ApiOperation, ApiSecurity, ApiTags } from '@nestjs/swagger';
import { WhatsappSession } from '@waha/core/abc/session.abc';
import { ChatIdApiParam } from '@waha/nestjs/params/ChatIdApiParam';
import {
SessionApiParam,
SessionParam,
} from '@waha/nestjs/params/SessionApiParam';
import { Label, SetLabelsRequest } from '@waha/structures/labels.dto';
import { SessionManager } from '../core/abc/manager.abc';
@ApiSecurity('api_key')
@Controller('api/:session/labels')
@ApiTags('🏷️ Labels')
export class LabelsController {
constructor(private manager: SessionManager) {}
@Get('/')
@SessionApiParam
@ApiOperation({ summary: 'Get all labels' })
getAll(@SessionParam session: WhatsappSession): Promise<Label[]> {
return session.getLabels();
}
@Get('/chats/:chatId')
@SessionApiParam
@ChatIdApiParam
@ApiOperation({ summary: 'Get labels for the chat' })
getChatLabels(
@SessionParam session: WhatsappSession,
@Param('chatId') chatId: string,
): Promise<Label[]> {
return session.getChatLabels(chatId);
}
@Put('/chats/:chatId')
@SessionApiParam
@ChatIdApiParam
@ApiOperation({ summary: 'Save labels for the chat' })
putChatLabels(
@SessionParam session: WhatsappSession,
@Param('chatId') chatId: string,
@Body() request: SetLabelsRequest,
) {
return session.putLabelsToChat(chatId, request.labels);
}
@Get('/:labelId/chats')
@SessionApiParam
@ApiOperation({ summary: 'Get chats by label' })
getChatsByLabel(
@SessionParam session: WhatsappSession,
@Param('labelId') labelId: string,
) {
return session.getChatsByLabelId(labelId);
}
}
+17
View File
@@ -0,0 +1,17 @@
import { Controller, Get } from '@nestjs/common';
import { ApiOperation, ApiSecurity, ApiTags } from '@nestjs/swagger';
import { PingResponse } from '@waha/structures/ping.dto';
@ApiSecurity('api_key')
@Controller('ping')
@ApiTags('🔍 Observability')
export class PingController {
@Get()
@ApiOperation({
summary: 'Ping the server',
description: 'Check if the server is alive and responding to requests.',
})
ping(): PingResponse {
return { message: 'pong' };
}
}
+8 -2
View File
@@ -7,6 +7,11 @@ import {
Post,
} from '@nestjs/common';
import { ApiOperation, ApiSecurity, ApiTags } from '@nestjs/swagger';
import { ChatIdApiParam } from '@waha/nestjs/params/ChatIdApiParam';
import {
SessionApiParam,
SessionParam,
} from '@waha/nestjs/params/SessionApiParam';
import { SessionManager } from '../core/abc/manager.abc';
import { WhatsappSession } from '../core/abc/session.abc';
@@ -15,11 +20,10 @@ import {
WAHAChatPresences,
WAHASessionPresence,
} from '../structures/presence.dto';
import { SessionApiParam, SessionParam } from './helpers';
@ApiSecurity('api_key')
@Controller('api/:session/presence')
@ApiTags('presence')
@ApiTags('✅ Presence')
export class PresenceController {
constructor(private manager: SessionManager) {}
@@ -64,6 +68,7 @@ export class PresenceController {
@Get(':chatId')
@SessionApiParam
@ChatIdApiParam
@ApiOperation({
summary:
"Get the presence for the chat id. If it hasn't been subscribed - it also subscribes to it.",
@@ -77,6 +82,7 @@ export class PresenceController {
@Post(':chatId/subscribe')
@SessionApiParam
@ChatIdApiParam
@ApiOperation({
summary: 'Subscribe to presence events for the chat.',
})
+3 -3
View File
@@ -7,16 +7,16 @@ import {
UseInterceptors,
} from '@nestjs/common';
import { ApiSecurity, ApiTags } from '@nestjs/swagger';
import { ApiFileAcceptHeader } from '@waha/nestjs/ApiFileAcceptHeader';
import { Response } from 'express';
import { SessionManager } from '../core/abc/manager.abc';
import { BufferResponseInterceptor } from '../nestjs/BufferResponseInterceptor';
import { SessionQuery } from '../structures/base.dto';
import { BufferResponseInterceptor } from './BufferResponseInterceptor';
import { ApiFileAcceptHeader } from './helpers';
@ApiSecurity('api_key')
@Controller('api')
@ApiTags('screenshot')
@ApiTags('🖼️ Screenshot')
export class ScreenshotController {
constructor(private manager: SessionManager) {}
+107
View File
@@ -0,0 +1,107 @@
import * as process from 'node:process';
import {
Body,
Controller,
Get,
Logger,
Post,
Query,
UsePipes,
} from '@nestjs/common';
import { ApiOperation, ApiSecurity, ApiTags } from '@nestjs/swagger';
import { getApp } from '@waha/main';
import { WAHAValidationPipe } from '@waha/nestjs/pipes/WAHAValidationPipe';
import { WAHAEnvironment } from '@waha/structures/environment.dto';
import {
EnvironmentQuery,
ServerStatusResponse,
StopRequest,
} from '@waha/structures/server.dto';
import { VERSION } from '@waha/version';
import * as lodash from 'lodash';
@ApiSecurity('api_key')
@Controller('api/server')
@ApiTags('🔍 Observability')
export class ServerController {
private logger: Logger;
constructor() {
this.logger = new Logger('ServerController');
}
@Get('version')
@ApiOperation({ summary: 'Get the version of the server' })
get(): WAHAEnvironment {
return VERSION;
}
@Get('environment')
@ApiOperation({ summary: 'Get the server environment' })
environment(
@Query(new WAHAValidationPipe()) query: EnvironmentQuery,
// eslint-disable-next-line @typescript-eslint/ban-types
): object {
let result = process.env;
if (!query.all) {
result = lodash.pickBy(result, (value, key) => {
return (
key.startsWith('WAHA_') ||
key.startsWith('WHATSAPP_') ||
key === 'DEBUG'
);
});
}
const map = new Map<string, string>();
// sort and set
Object.keys(result)
.sort()
.forEach((key) => {
map.set(key, result[key]);
});
return Object.fromEntries(map);
}
@Get('status')
@ApiOperation({ summary: 'Get the server status' })
async status(): Promise<ServerStatusResponse> {
const now = Date.now();
const uptime = Math.floor(process.uptime() * 1000);
const startTimestamp = now - uptime;
return {
startTimestamp: startTimestamp,
uptime: uptime,
};
}
@Post('stop')
@ApiOperation({
summary: 'Stop (and restart) the server',
description:
"If you're using docker, after calling this endpoint Docker will start a new container, " +
'so you can use this endpoint to restart the server',
})
@UsePipes(new WAHAValidationPipe())
async stop(@Body() request: StopRequest) {
const timeout = 1_000;
if (request.force) {
this.logger.log(`Force stopping the server in ${timeout}ms`);
setTimeout(() => {
this.logger.log('Force stopping the server');
process.kill(process.pid, 'SIGKILL');
}, timeout);
} else {
this.logger.log(`Gracefully stopping the server in ${timeout}ms`);
setTimeout(async () => {
this.logger.log('Gracefully closing the application...');
const app = getApp();
if (app) {
await app.close();
}
this.logger.log('Application closed');
process.exit(0);
}, timeout);
}
}
}
+256 -48
View File
@@ -6,69 +6,66 @@ import {
NotFoundException,
Param,
Post,
Put,
Query,
UsePipes,
} from '@nestjs/common';
import { UnprocessableEntityException } from '@nestjs/common/exceptions/unprocessable-entity.exception';
import { ApiOperation, ApiSecurity, ApiTags } from '@nestjs/swagger';
import {
ApiExtraModels,
ApiOperation,
ApiSecurity,
ApiTags,
} from '@nestjs/swagger';
SessionApiParam,
SessionParam,
} from '@waha/nestjs/params/SessionApiParam';
import { WAHAValidationPipe } from '@waha/nestjs/pipes/WAHAValidationPipe';
import { WAHASessionStatus } from '@waha/structures/enums.dto';
import {
SessionLogoutDeprecatedRequest,
SessionStartDeprecatedRequest,
SessionStopDeprecatedRequest,
} from '@waha/structures/sessions.deprecated.dto';
import { generatePrefixedId } from '@waha/utils/ids';
import { sleep } from '@waha/utils/promiseTimeout';
import { SessionManager } from '../core/abc/manager.abc';
import { WhatsappSession } from '../core/abc/session.abc';
import { parseBool } from '../helpers';
import {
ListSessionsQuery,
MeInfo,
SessionCreateRequest,
SessionDTO,
SessionInfo,
SessionLogoutRequest,
SessionStartRequest,
SessionStopRequest,
SessionUpdateRequest,
} from '../structures/sessions.dto';
import { SessionApiParam, SessionParam } from './helpers';
// eslint-disable-next-line @typescript-eslint/no-var-requires
const AsyncLock = require('async-lock');
@ApiSecurity('api_key')
@Controller('api/sessions')
@ApiTags('sessions')
@ApiTags('🖥️ Sessions')
class SessionsController {
constructor(private manager: SessionManager) {}
private lock: any;
@Post('/start/')
async start(@Body() request: SessionStartRequest): Promise<SessionDTO> {
const result = await this.manager.start(request);
await this.manager.sessionConfigRepository.save(
request.name,
request.config || null,
);
return result;
constructor(private manager: SessionManager) {
this.lock = new AsyncLock({ maxPending: Infinity });
}
@Post('/stop/')
@ApiOperation({ summary: 'Stop session' })
async stop(@Body() request: SessionStopRequest): Promise<void> {
if (request.logout) {
await this.manager.logout(request);
} else {
await this.manager.stop(request);
}
return;
}
@Post('/logout/')
@ApiOperation({ summary: 'Logout from session.' })
clean(@Body() request: SessionLogoutRequest): Promise<void> {
return this.manager.logout(request);
private withLock(name: string, fn: () => any) {
return this.lock.acquire(name, fn);
}
@Get('/')
async list(@Query() query: ListSessionsQuery): Promise<SessionInfo[]> {
const all = parseBool(query.all);
return this.manager.getSessions(all);
@ApiOperation({ summary: 'List all sessions' })
list(
@Query(new WAHAValidationPipe()) query: ListSessionsQuery,
): Promise<SessionInfo[]> {
return this.manager.getSessions(query.all);
}
@Get('/:session')
@ApiOperation({ summary: 'Get session information' })
@SessionApiParam
@UsePipes(new WAHAValidationPipe())
async get(@Param('session') name: string): Promise<SessionInfo> {
const session = this.manager.getSessionInfo(name);
if (session === null) {
@@ -76,20 +73,231 @@ class SessionsController {
}
return session;
}
}
@ApiSecurity('api_key')
@Controller('api/sessions/:session')
@ApiTags('sessions')
class SessionController {
constructor(private manager: SessionManager) {}
@Get('me')
@Get(':session/me')
@SessionApiParam
@ApiOperation({ summary: 'Get information about the authenticated account' })
getMe(@SessionParam session: WhatsappSession): Promise<MeInfo | null> {
getMe(@SessionParam session: WhatsappSession): MeInfo | null {
return session.getSessionMeInfo();
}
@Post('')
@ApiOperation({
summary: 'Create a session',
description:
'Create session a new session (and start it at the same time if required).',
})
@UsePipes(new WAHAValidationPipe())
async create(@Body() request: SessionCreateRequest): Promise<SessionDTO> {
const name = request.name || generatePrefixedId('session');
await this.withLock(name, async () => {
if (await this.manager.exists(name)) {
const msg = `Session '${name}' already exists. Use PUT to update it.`;
throw new UnprocessableEntityException(msg);
}
const config = request.config;
const start = request.start || false;
await this.manager.upsert(name, config);
if (start) {
await this.manager.start(name);
}
});
return await this.manager.getSessionInfo(name);
}
@Put(':session')
@ApiOperation({
summary: 'Update a session',
description: '',
})
@SessionApiParam
@UsePipes(new WAHAValidationPipe())
async update(
@Param('session') name: string,
@Body() request: SessionUpdateRequest,
): Promise<SessionDTO> {
await this.withLock(name, async () => {
if (!(await this.manager.exists(name))) {
throw new NotFoundException('Session not found');
}
const config = request.config;
const isRunning = this.manager.isRunning(name);
await this.manager.stop(name, true);
await this.manager.upsert(name, config);
if (isRunning) {
await this.manager.start(name);
}
});
return await this.manager.getSessionInfo(name);
}
@Delete(':session')
@SessionApiParam
@ApiOperation({
summary: 'Delete the session',
description:
'Delete the session with the given name. Stop and logout as well. Idempotent operation.',
})
@UsePipes(new WAHAValidationPipe())
async delete(@Param('session') name: string): Promise<void> {
await this.withLock(name, async () => {
await this.manager.stop(name, true);
await this.manager.logout(name);
await this.manager.delete(name);
});
}
@Post(':session/start')
@SessionApiParam
@ApiOperation({
summary: 'Start the session',
description:
'Start the session with the given name. The session must exist. Identity operation.',
})
@UsePipes(new WAHAValidationPipe())
async start(@Param('session') name: string): Promise<SessionDTO> {
await this.withLock(name, async () => {
const exists = await this.manager.exists(name);
if (!exists) {
throw new NotFoundException('Session not found');
}
const isRunning = this.manager.isRunning(name);
if (!isRunning) {
await this.manager.start(name);
}
});
return await this.manager.getSessionInfo(name);
}
@Post(':session/stop')
@SessionApiParam
@ApiOperation({
summary: 'Stop the session',
description: 'Stop the session with the given name. Idempotent operation.',
})
@UsePipes(new WAHAValidationPipe())
async stop(@Param('session') name: string): Promise<SessionDTO> {
await this.withLock(name, async () => {
const exists = await this.manager.exists(name);
if (!exists) {
throw new NotFoundException('Session not found');
}
await this.manager.stop(name, false);
});
return await this.manager.getSessionInfo(name);
}
@Post(':session/logout')
@SessionApiParam
@ApiOperation({
summary: 'Logout from the session',
description: 'Logout the session, restart a session if it was not STOPPED',
})
@UsePipes(new WAHAValidationPipe())
async logout(@Param('session') name: string): Promise<SessionDTO> {
await this.withLock(name, async () => {
const exists = await this.manager.exists(name);
if (!exists) {
throw new NotFoundException('Session not found');
}
const isRunning = this.manager.isRunning(name);
await this.manager.stop(name, true);
await this.manager.logout(name);
if (isRunning) {
await this.manager.start(name);
}
});
return await this.manager.getSessionInfo(name);
}
@Post(':session/restart')
@SessionApiParam
@ApiOperation({
summary: 'Restart the session',
description: 'Restart the session with the given name.',
})
@UsePipes(new WAHAValidationPipe())
async restart(@Param('session') name: string): Promise<SessionDTO> {
await this.withLock(name, async () => {
const exists = await this.manager.exists(name);
if (!exists) {
throw new NotFoundException('Session not found');
}
await this.manager.stop(name, true);
await this.manager.start(name);
});
return await this.manager.getSessionInfo(name);
}
@Post('/start/')
@ApiOperation({
summary: 'Upsert and Start session',
description:
'Create session (if not exists) or update a config (if exists) and start it.',
deprecated: true,
})
async DEPRACATED_start(
@Body() request: SessionStartDeprecatedRequest,
): Promise<SessionDTO> {
const name = request.name;
if (this.manager.isRunning(name)) {
const msg = `Session '${name}' is already started.`;
throw new UnprocessableEntityException(msg);
}
return await this.withLock(name, async () => {
const config = request.config;
if (config) {
await this.manager.upsert(name, config);
}
return await this.manager.start(name);
});
}
@Post('/stop/')
@ApiOperation({
summary: 'Stop (and Logout if asked) session',
description: 'Stop session and Logout by default.',
deprecated: true,
})
async DEPRECATED_stop(
@Body() request: SessionStopDeprecatedRequest,
): Promise<void> {
const name = request.name;
if (request.logout) {
// Old API did remove the session complete
await this.withLock(name, async () => {
await this.manager.stop(name, true);
await this.manager.logout(name);
await this.manager.delete(name);
});
} else {
await this.withLock(name, async () => {
await this.manager.stop(name, false);
});
}
return;
}
@Post('/logout/')
@ApiOperation({
summary: 'Logout and Delete session.',
description: 'Stop, Logout and Delete session.',
deprecated: true,
})
async DEPRECATED_logout(
@Body() request: SessionLogoutDeprecatedRequest,
): Promise<void> {
const name = request.name;
await this.withLock(name, async () => {
await this.manager.stop(name, true);
await this.manager.logout(name);
await this.manager.delete(name);
});
return;
}
}
export { SessionController, SessionsController };
export { SessionsController };
+21 -15
View File
@@ -1,33 +1,29 @@
import {
Body,
Controller,
Delete,
Get,
Param,
Post,
Query,
} from '@nestjs/common';
import { Body, Controller, Post } from '@nestjs/common';
import { ApiOperation, ApiSecurity, ApiTags } from '@nestjs/swagger';
import {
SessionApiParam,
SessionParam,
} from '@waha/nestjs/params/SessionApiParam';
import { SessionManager } from '../core/abc/manager.abc';
import { WhatsappSession } from '../core/abc/session.abc';
import {
DeleteStatusRequest,
ImageStatus,
TextStatus,
VideoStatus,
VoiceStatus,
} from '../structures/status.dto';
import { SessionApiParam, SessionParam } from './helpers';
@ApiSecurity('api_key')
@Controller('api/:session/status')
@ApiTags('status')
@ApiTags('🟢 Status')
class StatusController {
constructor(private manager: SessionManager) {}
@Post('text')
@SessionApiParam
@ApiOperation({ summary: 'Send text status.' })
@ApiOperation({ summary: 'Send text status' })
sendTextStatus(
@SessionParam session: WhatsappSession,
@Body() status: TextStatus,
@@ -37,7 +33,7 @@ class StatusController {
@Post('image')
@SessionApiParam
@ApiOperation({ summary: 'Send image status.' })
@ApiOperation({ summary: 'Send image status' })
sendImageStatus(
@SessionParam session: WhatsappSession,
@Body() status: ImageStatus,
@@ -47,7 +43,7 @@ class StatusController {
@Post('voice')
@SessionApiParam
@ApiOperation({ summary: 'Send voice status.' })
@ApiOperation({ summary: 'Send voice status' })
sendVoiceStatus(
@SessionParam session: WhatsappSession,
@Body() status: VoiceStatus,
@@ -57,13 +53,23 @@ class StatusController {
@Post('video')
@SessionApiParam
@ApiOperation({ summary: 'Send video status.' })
@ApiOperation({ summary: 'Send video status' })
sendVideoStatus(
@SessionParam session: WhatsappSession,
@Body() status: VideoStatus,
) {
return session.sendVideoStatus(status);
}
@Post('delete')
@SessionApiParam
@ApiOperation({ summary: 'DELETE sent status' })
deleteStatus(
@SessionParam session: WhatsappSession,
@Body() status: DeleteStatusRequest,
) {
return session.deleteStatus(status);
}
}
export { StatusController };
+12 -2
View File
@@ -1,14 +1,24 @@
import { Controller, Get } from '@nestjs/common';
import { ApiExtraModels, ApiSecurity, ApiTags } from '@nestjs/swagger';
import {
ApiExtraModels,
ApiOperation,
ApiSecurity,
ApiTags,
} from '@nestjs/swagger';
import { WAHAEnvironment } from '../structures/environment.dto';
import { VERSION } from '../version';
@ApiSecurity('api_key')
@Controller('api/version')
@ApiTags('other')
@ApiTags('🔍 Observability')
export class VersionController {
@Get('')
@ApiOperation({
summary: 'Get the server version ',
deprecated: true,
description: "Use 'GET /api/server/version' instead ",
})
get(): WAHAEnvironment {
return VERSION;
}
+17 -32
View File
@@ -1,20 +1,16 @@
import { Injectable } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { WAHAEvents } from '@waha/structures/enums.dto';
import { parseBool } from './helpers';
import { WAHAEngine } from './structures/enums.dto';
import { WebhookConfig } from './structures/webhooks.config.dto';
import { getEngineName } from './version';
@Injectable()
export class WhatsappConfigService {
public filesUri = '/api/files';
public schema = 'http';
constructor(private configService: ConfigService) {}
get filesURL(): string {
return `${this.schema}://${this.hostname}:${this.port}${this.filesUri}/`;
get schema() {
return this.configService.get('WHATSAPP_API_SCHEMA', 'http');
}
get hostname(): string {
@@ -25,17 +21,6 @@ export class WhatsappConfigService {
return this.configService.get('WHATSAPP_API_PORT', '3000');
}
get filesFolder(): string {
return this.configService.get(
'WHATSAPP_FILES_FOLDER',
'/tmp/whatsapp-files',
);
}
get filesLifetime(): number {
return this.configService.get<number>('WHATSAPP_FILES_LIFETIME', 180);
}
get mimetypes(): string[] {
if (!this.shouldDownloadMedia) {
return ['mimetype/ignore-all-media'];
@@ -66,8 +51,11 @@ export class WhatsappConfigService {
}
get proxyServer(): string[] | string | undefined {
const single = this.configService.get('WHATSAPP_PROXY_SERVER', undefined);
const multipleValues = this.configService.get(
const single = this.configService.get<string>(
'WHATSAPP_PROXY_SERVER',
undefined,
);
const multipleValues = this.configService.get<string>(
'WHATSAPP_PROXY_SERVER_LIST',
undefined,
);
@@ -103,7 +91,7 @@ export class WhatsappConfigService {
return this.get('WHATSAPP_HOOK_URL');
}
private getWebhookEvents(): string[] {
private getWebhookEvents(): WAHAEvents[] {
const value = this.get('WHATSAPP_HOOK_EVENTS', '');
return value ? value.split(',') : [];
}
@@ -112,17 +100,6 @@ export class WhatsappConfigService {
return this.configService.get('WHATSAPP_SESSIONS_MONGO_URL', undefined);
}
getDefaultEngineName(): WAHAEngine {
const value = getEngineName();
if (value in WAHAEngine) {
return WAHAEngine[value];
}
console.log(
`Unknown WhatsApp default engine WHATSAPP_DEFAULT_ENGINE=${value}. Using WEBJS`,
);
return WAHAEngine.WEBJS;
}
get(name: string, defaultValue: any = undefined): any {
return this.configService.get(name, defaultValue);
}
@@ -131,6 +108,14 @@ export class WhatsappConfigService {
return this.configService.get('WHATSAPP_API_KEY', '');
}
getExcludedPaths(): string[] {
const value = this.configService.get('WHATSAPP_API_KEY_EXCLUDE_PATH', '');
if (!value) {
return [];
}
return value.split(',');
}
getHealthMediaFilesThreshold(): number {
return this.configService.get<number>(
'WHATSAPP_HEALTH_MEDIA_FILES_THRESHOLD_MB',
+26 -15
View File
@@ -1,13 +1,18 @@
import { INestApplication } from '@nestjs/common';
import { DocumentBuilder, OpenAPIObject, SwaggerModule } from '@nestjs/swagger';
import { DECORATORS } from '@nestjs/swagger/dist/constants';
import { Logger } from 'nestjs-pino';
import { WhatsappConfigService } from '../config.service';
import { VERSION } from '../version';
import { SwaggerConfigServiceCore } from './config/SwaggerConfigServiceCore';
export class SwaggerConfiguratorCore {
constructor(protected app: INestApplication) {}
protected logger: any;
constructor(protected app: INestApplication) {
this.logger = app.get(Logger);
}
get title() {
return 'WAHA - WhatsApp HTTP API';
@@ -21,11 +26,11 @@ export class SwaggerConfiguratorCore {
'<ul>' +
'<li><a href="https://waha.devlike.pro/" target="_blank">Documentation</a></li>' +
'<li><a href="https://waha.devlike.pro/docs/how-to/engines/#features" target="_blank">Supported features in engines</a></li>' +
'<li><a href="https://github.com/devlikeapro/whatsapp-http-api" target="_blank">GitHub - WAHA Core</a></li>' +
'<li><a href="https://github.com/devlikeapro/whatsapp-http-api-plu' + // Separate line to pass pre-commit check
'<li><a href="https://github.com/devlikeapro/waha" target="_blank">GitHub - WAHA Core</a></li>' +
'<li><a href="https://github.com/devlikeapro/waha-plu' + // Separate line to pass pre-commit check
's" target="_blank">GitHub - WAHA Plus</a></li>' +
'<li><b>NEW!</b> - <a href="/dashboard">WAHA Dashboard</a></li>' +
'<li><b>NEW!</b> - <a href="https://waha.devlike.pro/docs/how-to/waha-dashboard">WAHA Dashboard Documentation</a></li>' +
'<li><a href="/dashboard">WAHA Dashboard</a></li>' +
'<li><a href="https://waha.devlike.pro/docs/how-to/waha-dashboard">WAHA Dashboard Documentation</a></li>' +
'</ul>' +
'<p>Support the project and get WAHA Plus version!</p>' +
'<ul>' +
@@ -51,24 +56,30 @@ export class SwaggerConfiguratorCore {
.setDescription(this.description)
.setExternalDoc(this.title, this.externalDocUrl)
.setVersion(VERSION.version)
.addTag('sessions', 'Control WhatsApp sessions')
.addTag('auth', 'Authentication')
.addTag('screenshot', 'Get screenshot of WhatsApp and show QR code')
.addTag('chatting', 'Chatting methods')
.addTag('🖥️ Sessions', 'Control WhatsApp sessions (accounts)')
.addTag('🔑 Auth', 'Authentication')
.addTag('🖼️ Screenshot', 'Get screenshot of WhatsApp and show QR code')
.addTag('📤 Chatting', 'Chatting methods')
.addTag('📢 Channels', 'Channels (newsletters) methods')
.addTag(
'status',
'🟢 Status',
'Status (aka stories) methods. <b>NOWEB</b> engine only!',
)
.addTag('chats', `Chats methods`)
.addTag('💬 Chats', `Chats methods`)
.addTag(
'contacts',
'👤 Contacts',
`Contacts methods.<br>
Use phone number (without +) or phone number and \`@c.us\` at the end as \`contactId\`.<br>
'E.g: \`12312312310\` OR \`12312312310@c.us\`<br>`,
)
.addTag('groups', `Groups methods.<br>`)
.addTag('presence', `Presence information`)
.addTag('other', 'Other methods')
.addTag('👥 Groups', `Groups methods.<br>`)
.addTag('✅ Presence', `Presence information`)
.addTag(
'🏷️ Labels',
'Labels - available only for WhatsApp Business accounts',
)
.addTag('🔍 Observability', 'Other methods')
.addTag('🗄️ Storage', 'Storage methods')
.addApiKey({
type: 'apiKey',
description: 'Your secret api key',
+3 -1
View File
@@ -1 +1,3 @@
export abstract class DataStore {}
export abstract class DataStore {
abstract init(sessionName?: string): Promise<void>;
}
+6 -4
View File
@@ -1,5 +1,5 @@
import { ConsoleLogger, Injectable } from '@nestjs/common';
import { DiskHealthIndicator, HealthCheckService } from '@nestjs/terminus';
import { Injectable, Logger, LoggerService } from '@nestjs/common';
import { HealthCheckService } from '@nestjs/terminus';
import type { HealthCheckResult } from '@nestjs/terminus/dist/health-check/health-check-result.interface';
import { WhatsappConfigService } from '../../config.service';
@@ -7,12 +7,14 @@ import { SessionManager } from './manager.abc';
@Injectable()
export abstract class WAHAHealthCheckService {
protected logger: LoggerService;
constructor(
protected sessionManager: SessionManager,
protected health: HealthCheckService,
protected log: ConsoleLogger,
protected config: WhatsappConfigService,
) {}
) {
this.logger = new Logger('WAHAHealthCheckService');
}
abstract check(): Promise<HealthCheckResult>;
}
+39 -10
View File
@@ -1,22 +1,24 @@
import { OnApplicationShutdown } from '@nestjs/common';
import { BeforeApplicationShutdown } from '@nestjs/common';
import { WAHAWebhook } from '@waha/structures/webhooks.dto';
import { VERSION } from '@waha/version';
import { EventEmitter } from 'events';
import { WAHAEngine } from '../../structures/enums.dto';
import { WAHAEngine, WAHAEvents } from '../../structures/enums.dto';
import {
SessionConfig,
SessionDTO,
SessionInfo,
SessionLogoutRequest,
SessionStartRequest,
SessionStopRequest,
} from '../../structures/sessions.dto';
import { ISessionAuthRepository } from '../storage/ISessionAuthRepository';
import { ISessionConfigRepository } from '../storage/ISessionConfigRepository';
import { WhatsappSession } from './session.abc';
import { WebhookConductor } from './webhooks.abc';
export abstract class SessionManager implements OnApplicationShutdown {
export abstract class SessionManager implements BeforeApplicationShutdown {
public store: any;
public sessionAuthRepository: ISessionAuthRepository;
public sessionConfigRepository: ISessionConfigRepository;
public events: EventEmitter;
protected abstract getEngine(engine: WAHAEngine): typeof WhatsappSession;
@@ -24,20 +26,47 @@ export abstract class SessionManager implements OnApplicationShutdown {
protected abstract get WebhookConductorClass(): typeof WebhookConductor;
abstract onApplicationShutdown(signal?: string);
abstract beforeApplicationShutdown(signal?: string);
//
// API Methods
//
abstract start(request: SessionStartRequest): Promise<SessionDTO>;
/**
* Either create or update
*/
abstract exists(name: string): Promise<boolean>;
abstract stop(request: SessionStopRequest): Promise<void>;
abstract isRunning(name: string): boolean;
abstract logout(request: SessionLogoutRequest): Promise<void>;
abstract upsert(name: string, config?: SessionConfig): Promise<void>;
abstract delete(name: string): Promise<void>;
abstract start(name: string): Promise<SessionDTO>;
abstract stop(name: string, silent: boolean): Promise<void>;
abstract logout(name: string): Promise<void>;
abstract getSession(name: string): WhatsappSession;
abstract getSessionInfo(name: string): Promise<SessionInfo | null>;
abstract getSessions(all: boolean): Promise<SessionInfo[]>;
handleSessionEvent(event: WAHAEvents, session: WhatsappSession) {
return (payload: any) => {
const me = session.getSessionMeInfo();
const data: WAHAWebhook = {
event: event,
session: session.name,
metadata: session.sessionConfig?.metadata,
me: me,
payload: payload,
engine: session.engine,
environment: VERSION,
};
this.events.emit(event, data);
};
}
}
-28
View File
@@ -1,28 +0,0 @@
abstract class MediaStorage {
abstract save(
messageId: string,
mimetype: string,
buffer: Buffer,
): Promise<string>;
}
interface IEngineMediaProcessor<Message> {
hasMedia(message: Message): boolean;
getFilename(message: Message): string | null;
getMimetype(message: Message): string;
getMessageId(message: Message): string;
getMediaBuffer(message: Message): Promise<Buffer | null>;
}
interface MediaManager {
processMedia<Message>(
processor: IEngineMediaProcessor<Message>,
message: Message,
): Promise<Message>;
}
export { IEngineMediaProcessor, MediaManager, MediaStorage };
+139 -36
View File
@@ -1,6 +1,14 @@
import { ConsoleLogger } from '@nestjs/common';
import {
Channel,
CreateChannelRequest,
ListChannelsQuery,
} from '@waha/structures/channels.dto';
import { GetChatsQuery } from '@waha/structures/chats.dto';
import { Label, LabelID } from '@waha/structures/labels.dto';
import { LoggerBuilder } from '@waha/utils/logging';
import { EventEmitter } from 'events';
import * as fs from 'fs';
import { Logger } from 'pino';
import { MessageId } from 'whatsapp-web.js';
import {
@@ -40,6 +48,7 @@ import {
SessionConfig,
} from '../../structures/sessions.dto';
import {
DeleteStatusRequest,
ImageStatus,
TextStatus,
VideoStatus,
@@ -47,9 +56,9 @@ import {
} from '../../structures/status.dto';
import { WASessionStatusBody } from '../../structures/webhooks.dto';
import { NotImplementedByEngineError } from '../exceptions';
import { IMediaManager } from '../media/IMediaManager';
import { QR } from '../QR';
import { DataStore } from './DataStore';
import { MediaManager } from './media.abc';
// eslint-disable-next-line @typescript-eslint/no-var-requires
const qrcode = require('qrcode-terminal');
@@ -74,14 +83,13 @@ export function ensureSuffix(phone) {
export enum WAHAInternalEvent {
ENGINE_START = 'engine.start',
SESSION_STATUS_CHANGED = 'session.status.changed',
}
export interface SessionParams {
name: string;
printQR: boolean;
mediaManager: MediaManager;
log: ConsoleLogger;
mediaManager: IMediaManager;
loggerBuilder: LoggerBuilder;
sessionStore: DataStore;
proxyConfig?: ProxyConfig;
sessionConfig?: SessionConfig;
@@ -93,8 +101,9 @@ export abstract class WhatsappSession {
public engine: WAHAEngine;
public name: string;
protected mediaManager: MediaManager;
protected log: ConsoleLogger;
protected mediaManager: IMediaManager;
public loggerBuilder: LoggerBuilder;
protected logger: Logger;
protected sessionStore: DataStore;
protected proxyConfig?: ProxyConfig;
public sessionConfig?: SessionConfig;
@@ -106,7 +115,7 @@ export abstract class WhatsappSession {
public constructor({
name,
printQR,
log,
loggerBuilder,
sessionStore,
proxyConfig,
mediaManager,
@@ -116,7 +125,8 @@ export abstract class WhatsappSession {
this.events = new EventEmitter();
this.name = name;
this.proxyConfig = proxyConfig;
this.log = log;
this.loggerBuilder = loggerBuilder;
this.logger = loggerBuilder.child({ name: 'WhatsappSession' });
this.sessionStore = sessionStore;
this.mediaManager = mediaManager;
this.sessionConfig = sessionConfig;
@@ -126,7 +136,8 @@ export abstract class WhatsappSession {
protected set status(value: WAHASessionStatus) {
this._status = value;
this.events.emit(WAHAInternalEvent.SESSION_STATUS_CHANGED, value);
const body: WASessionStatusBody = { name: this.name, status: value };
this.events.emit(WAHAEvents.SESSION_STATUS, body);
}
public get status() {
@@ -141,34 +152,48 @@ export abstract class WhatsappSession {
// Run optimized version of Chrome
// References:
// https://github.com/pedroslopez/whatsapp-web.js/issues/1420
// https://github.com/wppconnect-team/wppconnect/issues/1326
// https://www.bannerbear.com/blog/ways-to-speed-up-puppeteer-screenshots/
return [
'--no-sandbox',
'--aggressive-cache-discard',
'--disable-accelerated-2d-canvas',
'--disable-application-cache',
'--disable-background-networking',
'--disable-cache',
'--disable-client-side-phishing-detection',
'--disable-setuid-sandbox',
'--disable-component-update',
'--disable-default-apps',
'--disable-popup-blocking',
'--disable-offer-store-unmasked-wallet-cards',
'--disable-speech-api',
'--hide-scrollbars',
'--mute-audio',
'--disable-extensions',
'--disable-dev-shm-usage',
'--disable-accelerated-2d-canvas',
'--no-first-run',
'--no-default-browser-check',
'--no-pings',
'--password-store=basic',
'--use-mock-keychain',
'--no-zygote',
'--single-process',
'--disable-extensions',
'--disable-gpu',
'--disable-offer-store-unmasked-wallet-cards',
'--disable-offline-load-stale-cache',
'--disable-popup-blocking',
'--disable-setuid-sandbox',
'--disable-speech-api',
'--disable-sync',
'--disable-translate',
'--disable-web-security',
'--disk-cache-size=0',
'--hide-scrollbars',
'--ignore-certificate-errors',
'--ignore-ssl-errors',
'--metrics-recording-only',
'--mute-audio',
'--no-default-browser-check',
'--no-first-run',
'--no-pings',
'--no-sandbox',
'--no-zygote',
'--password-store=basic',
'--safebrowsing-disable-auto-update',
'--single-process',
'--use-mock-keychain',
];
}
protected isDebugEnabled() {
return this.log.isLevelEnabled('debug');
return this.logger.isLevelEnabled('debug');
}
/** Start the session */
@@ -184,10 +209,7 @@ export abstract class WhatsappSession {
): boolean {
switch (hook) {
case WAHAEvents.SESSION_STATUS:
this.events.on(WAHAInternalEvent.SESSION_STATUS_CHANGED, (value) => {
const body: WASessionStatusBody = { name: this.name, status: value };
handler(body);
});
this.events.on(WAHAEvents.SESSION_STATUS, handler);
return true;
default:
return false;
@@ -221,8 +243,8 @@ export abstract class WhatsappSession {
abstract getScreenshot(): Promise<Buffer>;
public getSessionMeInfo(): Promise<MeInfo | null> {
throw new NotImplementedByEngineError();
public getSessionMeInfo(): MeInfo | null {
return null;
}
public getCaptcha(): Promise<QR> {
@@ -285,7 +307,7 @@ export abstract class WhatsappSession {
/**
* Chats methods
*/
public getChats() {
public getChats(query: GetChatsQuery) {
throw new NotImplementedByEngineError();
}
@@ -317,6 +339,34 @@ export abstract class WhatsappSession {
throw new NotImplementedByEngineError();
}
public chatsArchiveChat(chatId: string): Promise<any> {
throw new NotImplementedByEngineError();
}
public chatsUnarchiveChat(chatId: string): Promise<any> {
throw new NotImplementedByEngineError();
}
/**
* Labels methods
*/
public getLabels(): Promise<Label[]> {
throw new NotImplementedByEngineError();
}
public getChatsByLabelId(labelId: string) {
throw new NotImplementedByEngineError();
}
public getChatLabels(chatId: string): Promise<Label[]> {
throw new NotImplementedByEngineError();
}
public putLabelsToChat(chatId: string, labels: LabelID[]) {
throw new NotImplementedByEngineError();
}
/**
* Contacts methods
*/
@@ -435,6 +485,47 @@ export abstract class WhatsappSession {
throw new NotImplementedByEngineError();
}
/**
* Channels methods
*/
public channelsList(query: ListChannelsQuery): Promise<Channel[]> {
throw new NotImplementedByEngineError();
}
public channelsCreateChannel(
request: CreateChannelRequest,
): Promise<Channel> {
throw new NotImplementedByEngineError();
}
public channelsGetChannel(id: string): Promise<Channel> {
throw new NotImplementedByEngineError();
}
public channelsGetChannelByInviteCode(inviteCode: string): Promise<Channel> {
throw new NotImplementedByEngineError();
}
public channelsDeleteChannel(id: string): Promise<void> {
throw new NotImplementedByEngineError();
}
public channelsFollowChannel(id: string): Promise<void> {
throw new NotImplementedByEngineError();
}
public channelsUnfollowChannel(id: string): Promise<void> {
throw new NotImplementedByEngineError();
}
public channelsMuteChannel(id: string): Promise<void> {
throw new NotImplementedByEngineError();
}
public channelsUnmuteChannel(id: string): Promise<void> {
throw new NotImplementedByEngineError();
}
/**
* Status methods
*/
@@ -454,6 +545,10 @@ export abstract class WhatsappSession {
throw new NotImplementedByEngineError();
}
public deleteStatus(request: DeleteStatusRequest) {
throw new NotImplementedByEngineError();
}
public async getEngineInfo(): Promise<any> {
return {};
}
@@ -485,14 +580,22 @@ export abstract class WhatsappSession {
return;
}
if (!qr.raw) {
this.log.error(
this.logger.error(
'QR.raw is not available, can not print it in the console',
);
return;
}
this.log.log(
this.logger.info(
"You can disable QR in console by setting 'WAHA_PRINT_QR=false' in your environment variables.",
);
qrcode.generate(qr.raw, { small: true });
}
}
export function isNewsletter(jid: string) {
return jid.endsWith('@newsletter');
}
export function getChannelInviteLink(code: string) {
return `https://whatsapp.com/channel/${code}`;
}
+6 -2
View File
@@ -1,15 +1,19 @@
import { ConsoleLogger } from '@nestjs/common';
import { LoggerBuilder } from '@waha/utils/logging';
import { Logger } from 'pino';
import { WebhookConfig } from '../../structures/webhooks.config.dto';
import { WhatsappSession } from './session.abc';
export abstract class WebhookSender {
protected url: string;
protected logger: Logger;
constructor(
protected log: ConsoleLogger,
loggerBuilder: LoggerBuilder,
protected webhookConfig: WebhookConfig,
) {
this.url = webhookConfig.url;
this.logger = loggerBuilder.child({ name: WebhookSender.name });
}
abstract send(json);
+140
View File
@@ -0,0 +1,140 @@
import {
BeforeApplicationShutdown,
Logger,
LoggerService,
} from '@nestjs/common';
import { sleep } from '@nestjs/terminus/dist/utils';
import {
OnGatewayConnection,
OnGatewayDisconnect,
OnGatewayInit,
WebSocketGateway,
WebSocketServer,
} from '@nestjs/websockets';
import { SessionManager } from '@waha/core/abc/manager.abc';
import { WebsocketHeartbeatJob } from '@waha/nestjs/ws/WebsocketHeartbeatJob';
import { WebSocket } from '@waha/nestjs/ws/ws';
import { WAHAEvents } from '@waha/structures/enums.dto';
import { generatePrefixedId } from '@waha/utils/ids';
import { IncomingMessage } from 'http';
import * as lodash from 'lodash';
import * as url from 'url';
import { Server } from 'ws';
@WebSocketGateway({
path: '/ws',
cors: true,
})
export class WebsocketGatewayCore
implements
OnGatewayInit,
OnGatewayConnection,
OnGatewayDisconnect,
BeforeApplicationShutdown
{
HEARTBEAT_INTERVAL = 10_000;
@WebSocketServer()
server: Server;
private listeners: Map<WebSocket, { session: string; events: string[] }> =
new Map();
private readonly logger: LoggerService;
private heartbeat: WebsocketHeartbeatJob;
constructor(private manager: SessionManager) {
this.logger = new Logger('WebsocketGateway');
this.heartbeat = new WebsocketHeartbeatJob(
this.logger,
this.HEARTBEAT_INTERVAL,
);
}
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}`);
const params = this.getParams(id, request, socket);
if (!params) {
return;
}
const { session, events } = params;
this.logger.debug(
`Client connected to session: '${session}', events: ${events}, ${id}`,
);
this.listeners.set(socket, { session, events });
}
private getParams(id: string, request: IncomingMessage, socket: WebSocket) {
const query = url.parse(request.url, true).query;
const session = (query.session as string) || '*';
if (session !== '*') {
this.logger.warn(
`Only connecting to all sessions is allowed for now, use session=*, ${id}`,
);
const error =
'Only connecting to all sessions is allowed for now, use session=*';
socket.close(4001, JSON.stringify({ error }));
return null;
}
const events = ((query.events as string) || '*').split(',');
if (
!lodash.isEqual(events, ['*']) &&
!lodash.isEqual(events, [WAHAEvents.SESSION_STATUS])
) {
this.logger.warn(
`Only \'session.status\' event is allowed for now, use events=session.status or events=*, ${id}`,
);
const error =
"Only 'session.status' event is allowed for now, use events=session.status or events=*";
socket.close(4001, JSON.stringify({ error }));
return null;
}
return { session, events };
}
handleDisconnect(socket: WebSocket): any {
this.logger.debug(`Client disconnected - ${socket.id}`);
this.listeners.delete(socket);
}
async beforeApplicationShutdown(signal?: string) {
this.logger.log('Shutting down websocket server');
// Allow pending messages to be sent, it can be even 1ms, just to release the event loop
await sleep(100);
this.server?.clients.forEach((options, client) => {
// @ts-ignore
this.logger.debug(`Closing client connection - ${client.id}...`);
client.close(1001, 'Server is shutting down');
});
// Do not turn off heartbeat service here,
// it's responsible for terminating the connection that is not alive
this.logger.log('Websocket server is down');
}
afterInit(server: Server) {
this.logger.debug('Websocket server initialized');
this.manager.events.on(
WAHAEvents.SESSION_STATUS,
this.sendToAll.bind(this),
);
this.logger.debug('Subscribed to manager events');
this.logger.debug('Starting heartbeat service...');
this.heartbeat.start(server);
this.logger.debug('Heartbeat service started');
}
sendToAll(data: any) {
this.listeners.forEach((options, client) => {
if (options.events.length === 0) {
return;
}
this.logger.debug('Sending data to client', data);
client.send(JSON.stringify(data));
});
}
}
+84 -26
View File
@@ -1,9 +1,23 @@
import { ConsoleLogger, Module } from '@nestjs/common';
import { INestApplication, Module } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';
import { APP_INTERCEPTOR } from '@nestjs/core';
import { PassportModule } from '@nestjs/passport';
import { ServeStaticModule } from '@nestjs/serve-static';
import { TerminusModule } from '@nestjs/terminus';
import { ChannelsController } from '@waha/api/channels.controller';
import { ServerController } from '@waha/api/server.controller';
import { WebsocketGatewayCore } from '@waha/core/api/websocket.gateway.core';
import { MediaLocalStorageConfig } from '@waha/core/media/local/MediaLocalStorageConfig';
import { BufferJsonReplacerInterceptor } from '@waha/nestjs/BufferJsonReplacerInterceptor';
import {
getPinoHttpUseLevel,
getPinoLogLevel,
getPinoTransport,
} from '@waha/utils/logging';
import * as Joi from 'joi';
import { LoggerModule } from 'nestjs-pino';
import { join } from 'path';
import { Logger } from 'pino';
import { AuthController } from '../api/auth.controller';
import { ChatsController } from '../api/chats.controller';
@@ -11,12 +25,11 @@ import { ChattingController } from '../api/chatting.controller';
import { ContactsController } from '../api/contacts.controller';
import { GroupsController } from '../api/groups.controller';
import { HealthController } from '../api/health.controller';
import { LabelsController } from '../api/labels.controller';
import { PingController } from '../api/ping.controller';
import { PresenceController } from '../api/presence.controller';
import { ScreenshotController } from '../api/screenshot.controller';
import {
SessionController,
SessionsController,
} from '../api/sessions.controller';
import { SessionsController } from '../api/sessions.controller';
import { StatusController } from '../api/status.controller';
import { VersionController } from '../api/version.controller';
import { WhatsappConfigService } from '../config.service';
@@ -29,31 +42,56 @@ import { WAHAHealthCheckServiceCore } from './health/WAHAHealthCheckServiceCore'
import { SessionManagerCore } from './manager.core';
export const IMPORTS = [
LoggerModule.forRoot({
renameContext: 'name',
pinoHttp: {
quietReqLogger: true,
level: getPinoLogLevel(),
useLevel: getPinoHttpUseLevel(),
transport: getPinoTransport(),
autoLogging: {
ignore: (req) => {
return (
req.url.startsWith('/dashboard/') ||
req.url.startsWith('/api/files/') ||
req.url.startsWith('/api/s3/')
);
},
},
serializers: {
req: (req) => ({
id: req.id,
method: req.method,
url: req.url,
query: req.query,
params: req.params,
}),
res: (res) => ({
statusCode: res.statusCode,
}),
},
},
}),
ConfigModule.forRoot({
isGlobal: true,
validationSchema: Joi.object({
WHATSAPP_API_SCHEMA: Joi.string().valid('http', 'https').default('http'),
}),
}),
ServeStaticModule.forRootAsync({
imports: [],
extraProviders: [WhatsappConfigService, DashboardConfigServiceCore],
inject: [WhatsappConfigService, DashboardConfigServiceCore],
useFactory: (
config: WhatsappConfigService,
dashboardConfig: DashboardConfigServiceCore,
) => {
const options = [
// Serve files (media)
extraProviders: [DashboardConfigServiceCore],
inject: [DashboardConfigServiceCore],
useFactory: (dashboardConfig: DashboardConfigServiceCore) => {
if (!dashboardConfig.enabled) {
return [];
}
return [
{
rootPath: config.filesFolder,
serveRoot: config.filesUri,
},
];
if (dashboardConfig.enabled) {
options.push({
rootPath: join(__dirname, '..', 'dashboard'),
serveRoot: dashboardConfig.dashboardUri,
});
}
return options;
},
];
},
}),
PassportModule,
@@ -62,16 +100,19 @@ export const IMPORTS = [
export const CONTROLLERS = [
AuthController,
SessionsController,
SessionController,
ChattingController,
ChatsController,
ChannelsController,
StatusController,
LabelsController,
ContactsController,
GroupsController,
PresenceController,
ScreenshotController,
VersionController,
PingController,
HealthController,
ServerController,
VersionController,
];
const PROVIDERS = [
{
@@ -82,11 +123,16 @@ const PROVIDERS = [
provide: WAHAHealthCheckService,
useClass: WAHAHealthCheckServiceCore,
},
{
provide: APP_INTERCEPTOR,
useClass: BufferJsonReplacerInterceptor,
},
DashboardConfigServiceCore,
SwaggerConfigServiceCore,
WhatsappConfigService,
EngineConfigService,
ConsoleLogger,
WebsocketGatewayCore,
MediaLocalStorageConfig,
];
@Module({
@@ -95,5 +141,17 @@ const PROVIDERS = [
providers: PROVIDERS,
})
export class AppModuleCore {
constructor(protected config: WhatsappConfigService) {}
public startTimestamp: number;
constructor(protected config: WhatsappConfigService) {
this.startTimestamp = Date.now();
}
static getHttpsOptions(logger: Logger) {
return undefined;
}
static appReady(app: INestApplication, logger: Logger) {
return;
}
}
@@ -1,5 +1,6 @@
import { Injectable } from '@nestjs/common';
import { Injectable, Logger } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { InjectPinoLogger, PinoLogger } from 'nestjs-pino';
import { parseBool } from '../../helpers';
@@ -7,7 +8,11 @@ import { parseBool } from '../../helpers';
export class DashboardConfigServiceCore {
public dashboardUri = '/dashboard';
constructor(protected configService: ConfigService) {}
constructor(
protected configService: ConfigService,
@InjectPinoLogger('DashboardConfigService')
protected logger: PinoLogger,
) {}
get enabled(): boolean {
const value = this.configService.get('WAHA_DASHBOARD_ENABLED', 'true');
+19 -2
View File
@@ -1,11 +1,28 @@
import { Injectable } from '@nestjs/common';
import { Injectable, Logger } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { parseBool } from '../../helpers';
import { WAHAEngine } from '../../structures/enums.dto';
import { getEngineName } from '../../version';
@Injectable()
export class EngineConfigService {
constructor(protected configService: ConfigService) {}
private logger: Logger;
constructor(protected configService: ConfigService) {
this.logger = new Logger('EngineConfigService');
}
getDefaultEngineName(): WAHAEngine {
const value = getEngineName();
if (value in WAHAEngine) {
return WAHAEngine[value];
}
this.logger.warn(
`Unknown WhatsApp default engine WHATSAPP_DEFAULT_ENGINE=${value}. Using WEBJS`,
);
return WAHAEngine.WEBJS;
}
get shouldPrintQR(): boolean {
const value = this.configService.get('WAHA_PRINT_QR', true);
+6 -1
View File
@@ -1,11 +1,16 @@
import { Injectable } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { InjectPinoLogger, PinoLogger } from 'nestjs-pino';
import { parseBool } from '../../helpers';
@Injectable()
export class SwaggerConfigServiceCore {
constructor(protected configService: ConfigService) {}
constructor(
protected configService: ConfigService,
@InjectPinoLogger('SwaggerConfigService')
protected logger: PinoLogger,
) {}
get advancedConfigEnabled(): boolean {
const value = this.configService.get(
@@ -1,8 +1,6 @@
import { useMultiFileAuthState } from '@adiwajshing/baileys';
import { Store } from 'whatsapp-web.js';
import { DataStore } from '../../abc/DataStore';
import { LocalStore } from '../../storage/LocalStore';
import { useMultiFileAuthState } from './useMultiFileAuthState';
export class NowebAuthFactoryCore {
buildAuth(store: DataStore, name: string) {
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,17 @@
import { Chat } from '@adiwajshing/baileys';
export interface IChatRepository {
getAll(): Promise<Chat[]>;
getAllByIds(ids: string[]): Promise<Chat[]>;
getAllWithMessages(limit?: number, offset?: number): Promise<Chat[]>;
getById(id: string): Promise<Chat | null>;
deleteAll(): Promise<void>;
deleteById(id: string): Promise<void>;
save(chat: Chat): Promise<void>;
}
@@ -0,0 +1,13 @@
import { Contact } from '@adiwajshing/baileys';
export interface IContactRepository {
getAll(): Promise<Contact[]>;
getById(id: string): Promise<Contact | null>;
deleteAll(): Promise<void>;
deleteById(id: string): Promise<void>;
save(contact: Contact): Promise<void>;
}
@@ -0,0 +1,19 @@
import {
LabelAssociation,
LabelAssociationType,
} from '@adiwajshing/baileys/lib/Types/LabelAssociation';
export interface ILabelAssociationRepository {
deleteOne(association: LabelAssociation): Promise<void>;
save(association: LabelAssociation): Promise<void>;
deleteByLabelId(labelId: string): Promise<void>;
getAssociationsByLabelId(
labelId: string,
type: LabelAssociationType,
): Promise<LabelAssociation[]>;
getAssociationsByChatId(chatId: string): Promise<LabelAssociation[]>;
}
@@ -0,0 +1,14 @@
import { Contact } from '@adiwajshing/baileys';
import { Label } from '@adiwajshing/baileys/lib/Types/Label';
export interface ILabelsRepository {
getById(id: string): Promise<Label | null>;
getAll(): Promise<Label[]>;
getAllByIds(ids: string[]): Promise<Label[]>;
deleteById(id: string): Promise<void>;
save(label: Label): Promise<void>;
}
@@ -0,0 +1,17 @@
export interface IMessagesRepository {
deleteAll(): Promise<void>;
upsert(messages: any[]): Promise<void>;
upsertOne(message: any): Promise<void>;
getAllByJid(jid: string, limit: number): Promise<any[]>;
getByJidById(jid: string, id: string): Promise<any | null>;
updateByJidAndId(jid: string, id: string, update: any): Promise<boolean>;
deleteByJidByIds(jid: string, ids: string[]): Promise<void>;
deleteAllByJid(jid: string): Promise<void>;
}
@@ -0,0 +1,40 @@
import { WAMessage } from '@adiwajshing/baileys';
import { LabelAssociation } from '@adiwajshing/baileys/lib/Types/LabelAssociation';
import { ILabelAssociationRepository } from '@waha/core/engines/noweb/store/ILabelAssociationsRepository';
import { ILabelsRepository } from '@waha/core/engines/noweb/store/ILabelsRepository';
import { IChatRepository } from './IChatRepository';
import { IContactRepository } from './IContactRepository';
import { IMessagesRepository } from './IMessagesRepository';
export abstract class INowebStorage {
abstract init(): Promise<void>;
abstract close(): Promise<void>;
abstract getContactsRepository(): IContactRepository;
abstract getChatRepository(): IChatRepository;
abstract getMessagesRepository(): IMessagesRepository;
abstract getLabelsRepository(): ILabelsRepository;
abstract getLabelAssociationRepository(): ILabelAssociationRepository;
protected getMessagesMetadata(): Map<string, any> {
return new Map()
.set('jid', (msg: WAMessage) => msg.key.remoteJid)
.set('id', (msg: WAMessage) => msg.key.id)
.set('messageTimestamp', (msg: WAMessage) => msg.messageTimestamp);
}
protected getLabelAssociationMetadata() {
return new Map().set(
'id',
(a: LabelAssociation) =>
// @ts-ignore
`${a.type}_${a.labelId}_${a.chatId}_${a.messageId}`,
);
}
}
@@ -0,0 +1,35 @@
import {
BaileysEventEmitter,
Chat,
Contact,
proto,
} from '@adiwajshing/baileys';
import { Label } from '@adiwajshing/baileys/lib/Types/Label';
export interface INowebStore {
presences: any;
init(): Promise<void>;
close(): Promise<void>;
bind(ev: BaileysEventEmitter, socket: any): void;
loadMessage(jid: string, id: string): Promise<proto.IWebMessageInfo>;
getMessagesByJid(chatId: string, limit: number): Promise<any>;
getChats(limit?: number, offset?: number): Promise<Chat[]>;
getContacts(): Promise<Contact[]>;
getContactById(jid: string): Promise<Contact>;
getLabels(): Promise<Label[]>;
getLabelById(labelId: string): Promise<Label | null>;
getChatsByLabelId(labelId: string): Promise<Chat[]>;
getChatLabels(chatId: string): Promise<Label[]>;
}
@@ -0,0 +1,71 @@
import { Chat, Contact, makeInMemoryStore, proto } from '@adiwajshing/baileys';
import { Label } from '@adiwajshing/baileys/lib/Types/Label';
import { BadRequestException } from '@nestjs/common';
import { INowebStore } from './INowebStore';
// eslint-disable-next-line @typescript-eslint/no-var-requires
const logger = require('pino')();
export class NowebInMemoryStore implements INowebStore {
private store: ReturnType<typeof makeInMemoryStore>;
errorMessage =
'Enable NOWEB store "config.noweb.store.enabled=True" and "config.noweb.store.full_sync=True" when starting a new session. ' +
'Read more: https://waha.devlike.pro/docs/engines/noweb#store';
constructor() {
this.store = makeInMemoryStore({ logger: logger });
}
init(): Promise<void> {
return;
}
close(): Promise<void> {
return;
}
get presences() {
return this.store.presences;
}
bind(ev: any, socket: any) {
this.store.bind(ev);
}
loadMessage(jid: string, id: string): Promise<proto.IWebMessageInfo> {
return this.store.loadMessage(jid, id);
}
getMessagesByJid(chatId: string, limit: number): Promise<any> {
throw new BadRequestException(this.errorMessage);
}
getChats(limit?: number, offset?: number): Promise<Chat[]> {
throw new BadRequestException(this.errorMessage);
}
getContacts(): Promise<Contact[]> {
throw new BadRequestException(this.errorMessage);
}
getContactById(jid: string): Promise<Contact> {
throw new BadRequestException(this.errorMessage);
}
getLabels(): Promise<Label[]> {
throw new BadRequestException(this.errorMessage);
}
getLabelById(labelId: string): Promise<Label | null> {
throw new BadRequestException(this.errorMessage);
}
getChatsByLabelId(labelId: string): Promise<Chat[]> {
throw new BadRequestException(this.errorMessage);
}
getChatLabels(chatId: string): Promise<Label[]> {
throw new BadRequestException(this.errorMessage);
}
}
@@ -0,0 +1,365 @@
import {
BaileysEventEmitter,
Chat,
ChatUpdate,
Contact,
isRealMessage,
jidNormalizedUser,
proto,
updateMessageWithReaction,
updateMessageWithReceipt,
} from '@adiwajshing/baileys';
import { Label } from '@adiwajshing/baileys/lib/Types/Label';
import {
LabelAssociation,
LabelAssociationType,
} from '@adiwajshing/baileys/lib/Types/LabelAssociation';
import { ILabelAssociationRepository } from '@waha/core/engines/noweb/store/ILabelAssociationsRepository';
import { ILabelsRepository } from '@waha/core/engines/noweb/store/ILabelsRepository';
import { toNumber } from 'lodash';
import { Logger } from 'pino';
import { toJID } from '../session.noweb.core';
import { IChatRepository } from './IChatRepository';
import { IContactRepository } from './IContactRepository';
import { IMessagesRepository } from './IMessagesRepository';
import { INowebStorage } from './INowebStorage';
import { INowebStore } from './INowebStore';
// eslint-disable-next-line @typescript-eslint/no-var-requires
const AsyncLock = require('async-lock');
export class NowebPersistentStore implements INowebStore {
private socket: any;
private chatRepo: IChatRepository;
private contactRepo: IContactRepository;
private messagesRepo: IMessagesRepository;
private labelsRepo: ILabelsRepository;
private labelAssociationsRepo: ILabelAssociationRepository;
public presences: any;
private lock: any;
constructor(
private logger: Logger,
public storage: INowebStorage,
) {
this.socket = null;
this.chatRepo = storage.getChatRepository();
this.contactRepo = storage.getContactsRepository();
this.messagesRepo = storage.getMessagesRepository();
this.labelsRepo = storage.getLabelsRepository();
this.labelAssociationsRepo = storage.getLabelAssociationRepository();
this.presences = {};
this.lock = new AsyncLock({ maxPending: Infinity });
}
init(): Promise<void> {
return this.storage.init();
}
bind(ev: BaileysEventEmitter, socket: any) {
// All
ev.on('messaging-history.set', (data) => this.onMessagingHistorySet(data));
// Messages
ev.on('messages.upsert', (data) =>
this.withLock('messages', () => this.onMessagesUpsert(data)),
);
ev.on('messages.update', (data) =>
this.withLock('messages', () => this.onMessageUpdate(data)),
);
ev.on('messages.delete', (data) =>
this.withLock('messages', () => this.onMessageDelete(data)),
);
ev.on('messages.reaction', (data) =>
this.withLock('messages', () => this.onMessageReaction(data)),
);
ev.on('message-receipt.update', (data) =>
this.withLock('messages', () => this.onMessageReceiptUpdate(data)),
);
// Chats
ev.on('chats.upsert', (data) =>
this.withLock('chats', () => this.onChatUpsert(data)),
);
ev.on('chats.update', (data) =>
this.withLock('chats', () => this.onChatUpdate(data)),
);
ev.on('chats.delete', (data) =>
this.withLock('chats', () => this.onChatDelete(data)),
);
// Contacts
ev.on('contacts.upsert', (data) =>
this.withLock('contacts', () => this.onContactsUpsert(data)),
);
ev.on('contacts.update', (data) =>
this.withLock('contacts', () => this.onContactUpdate(data)),
);
ev.on('labels.edit', (data) => this.onLabelsEdit(data));
ev.on('labels.association', ({ association, type }) =>
this.onLabelsAssociation(association, type),
);
// Presence
ev.on('presence.update', (data) => this.onPresenceUpdate(data));
this.socket = socket;
}
async close(): Promise<void> {
await this.storage?.close().catch((error) => {
this.logger.warn(`Failed to close storage: ${error}`);
});
return;
}
private async onMessagingHistorySet(history) {
const { contacts, chats, messages, isLatest } = history;
if (isLatest) {
this.logger.debug(
'history sync - clearing all entities, got latest history',
);
await Promise.all([
this.withLock('contacts', () => this.contactRepo.deleteAll()),
this.withLock('chats', () => this.chatRepo.deleteAll()),
this.withLock('messages', () => this.messagesRepo.deleteAll()),
]);
}
await Promise.all([
this.withLock('contacts', async () => {
await this.onContactsUpsert(contacts);
this.logger.info(`history sync - '${contacts.length}' synced contacts`);
}),
this.withLock('chats', () => this.onChatUpsert(chats)),
this.withLock('messages', () => this.syncMessagesHistory(messages)),
]);
}
private async syncMessagesHistory(messages) {
const realMessages = messages.filter(isRealMessage);
await this.messagesRepo.upsert(realMessages);
this.logger.info(
`history sync - '${messages.length}' got messages, '${realMessages.length}' real messages`,
);
}
private async onMessagesUpsert(update) {
const { messages, type } = update;
if (type !== 'notify' && type !== 'append') {
this.logger.debug(`unexpected type for messages.upsert: '${type}'`);
return;
}
const realMessages = messages.filter(isRealMessage);
await this.messagesRepo.upsert(realMessages);
this.logger.debug(
`messages.upsert - ${messages.length} got messages, ${realMessages.length} real messages`,
);
}
private async onMessageUpdate(updates) {
for (const update of updates) {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const jid = jidNormalizedUser(update.key.remoteJid!);
const message = await this.messagesRepo.getByJidById(jid, update.key.id);
if (!message) {
continue;
}
const fields = { ...update.update };
// It can overwrite the key, so we need to delete it
delete fields['key'];
Object.assign(message, fields);
// In case of revoked messages - remove it
// TODO: May be we should save the flag instead of completely removing the message
const isYetRealMessage =
isRealMessage(message, this.socket?.authState?.creds?.me?.id) || false;
if (isYetRealMessage) {
await this.messagesRepo.upsertOne(message);
} else {
await this.messagesRepo.deleteByJidByIds(jid, [update.key.id]);
}
}
}
private async onMessageDelete(item) {
if ('all' in item) {
await this.messagesRepo.deleteAllByJid(item.jid);
return;
}
const jid = jidNormalizedUser(item.keys[0].remoteJid);
const ids = item.keys.map((key) => key.id);
await this.messagesRepo.deleteByJidByIds(jid, ids);
}
private async onChatUpsert(chats: Chat[]) {
for (const chat of chats) {
delete chat['messages'];
chat.conversationTimestamp = toNumber(chat.conversationTimestamp);
await this.chatRepo.save(chat);
}
this.logger.info(`history sync - '${chats.length}' synced chats`);
}
private async onChatUpdate(updates: ChatUpdate[]) {
for (const update of updates) {
const chat = (await this.chatRepo.getById(update.id)) || ({} as Chat);
Object.assign(chat, update);
chat.conversationTimestamp = toNumber(chat.conversationTimestamp);
delete chat['messages'];
await this.chatRepo.save(chat);
}
}
private async onChatDelete(ids: string[]) {
for (const id of ids) {
await this.chatRepo.deleteById(id);
await this.messagesRepo.deleteAllByJid(id);
}
}
private withLock(key, fn) {
return this.lock.acquire(key, fn);
}
private async onContactsUpsert(contacts: Contact[]) {
for (const update of contacts) {
const contact = await this.contactRepo.getById(update.id);
// remove undefined from data
Object.keys(update).forEach(
(key) => update[key] === undefined && delete update[key],
);
const result = { ...(contact || {}), ...update };
await this.contactRepo.save(result);
}
}
private async onContactUpdate(updates: Partial<Contact>[]) {
for (const update of updates) {
const contact = await this.contactRepo.getById(update.id);
if (!contact) {
this.logger.warn(
`got update for non-existent contact. update: '${JSON.stringify(
update,
)}'`,
);
continue;
// TODO: Find contact by hash if not found
// find contact by attrs.hash, when user is not saved as a contact
// check the in-memory for that
}
Object.assign(contact, update);
if (update.imgUrl === 'changed') {
contact.imgUrl = this.socket
? await this.socket?.profilePictureUrl(contact.id)
: undefined;
} else if (update.imgUrl === 'removed') {
delete contact.imgUrl;
}
await this.onContactsUpsert([contact]);
}
}
private async onMessageReaction(reactions) {
for (const { key, reaction } of reactions) {
const msg = await this.messagesRepo.getByJidById(key.remoteJid, key.id);
if (!msg) {
this.logger.warn(
`got reaction update for non-existent message. key: '${JSON.stringify(
key,
)}'`,
);
continue;
}
updateMessageWithReaction(msg, reaction);
await this.messagesRepo.upsertOne(msg);
}
}
private async onMessageReceiptUpdate(updates) {
for (const { key, receipt } of updates) {
const msg = await this.messagesRepo.getByJidById(key.remoteJid, key.id);
if (!msg) {
this.logger.warn(
`got receipt update for non-existent message. key: '${JSON.stringify(
key,
)}'`,
);
continue;
}
updateMessageWithReceipt(msg, receipt);
await this.messagesRepo.upsertOne(msg);
}
}
private async onLabelsEdit(label: Label) {
if (label.deleted) {
await this.labelsRepo.deleteById(label.id);
await this.labelAssociationsRepo.deleteByLabelId(label.id);
} else {
await this.labelsRepo.save(label);
}
}
private async onLabelsAssociation(
association: LabelAssociation,
type: 'add' | 'remove',
) {
if (type === 'remove') {
await this.labelAssociationsRepo.deleteOne(association);
} else {
await this.labelAssociationsRepo.save(association);
}
}
private async onPresenceUpdate({ id, presences: update }) {
this.presences[id] = this.presences[id] || {};
Object.assign(this.presences[id], update);
}
async loadMessage(jid: string, id: string) {
const data = await this.messagesRepo.getByJidById(jid, id);
if (!data) {
return null;
}
return proto.WebMessageInfo.fromObject(data);
}
getMessagesByJid(chatId: string, limit: number) {
return this.messagesRepo.getAllByJid(toJID(chatId), toNumber(limit));
}
getChats(limit?: number, offset?: number): Promise<Chat[]> {
return this.chatRepo.getAllWithMessages(limit, offset);
}
getContactById(jid) {
return this.contactRepo.getById(jid);
}
getContacts() {
return this.contactRepo.getAll();
}
getLabels(): Promise<Label[]> {
return this.labelsRepo.getAll();
}
getLabelById(labelId: string): Promise<Label | null> {
return this.labelsRepo.getById(labelId);
}
async getChatsByLabelId(labelId: string): Promise<Chat[]> {
const associations =
await this.labelAssociationsRepo.getAssociationsByLabelId(
labelId,
LabelAssociationType.Chat,
);
const ids = associations.map((association) => association.chatId);
return await this.chatRepo.getAllByIds(ids);
}
async getChatLabels(chatId: string): Promise<Label[]> {
const associations =
await this.labelAssociationsRepo.getAssociationsByChatId(chatId);
const ids = associations.map((association) => association.labelId);
return await this.labelsRepo.getAllByIds(ids);
}
}
@@ -0,0 +1,9 @@
import { DataStore } from '../../../abc/DataStore';
import { INowebStorage } from './INowebStorage';
import { Sqlite3Storage } from './sqlite3/Sqlite3Storage';
export class NowebStorageFactoryCore {
createStorage(store: DataStore, name: string): INowebStorage {
return new Sqlite3Storage(':memory:');
}
}
+78
View File
@@ -0,0 +1,78 @@
export class Field {
constructor(
public fieldName: string,
public type: string,
) {}
}
export class Index {
constructor(
public name: string,
public columns: string[],
) {}
}
export class Schema {
constructor(
public name: string,
public columns: Field[],
public indexes: Index[],
) {}
}
export const NOWEB_STORE_SCHEMA = [
new Schema(
'contacts',
[new Field('id', 'TEXT'), new Field('data', 'TEXT')],
[new Index('contacts_id_index', ['id'])],
),
new Schema(
'chats',
[
new Field('id', 'TEXT'),
new Field('conversationTimestamp', 'INTEGER'),
new Field('data', 'TEXT'),
],
[
new Index('chats_id_index', ['id']),
new Index('chats_conversationTimestamp_index', ['conversationTimestamp']),
],
),
new Schema(
'messages',
[
new Field('jid', 'TEXT'),
new Field('id', 'TEXT'),
new Field('messageTimestamp', 'INTEGER'),
new Field('data', 'TEXT'),
],
[
new Index('messages_id_index', ['id']),
new Index('messages_jid_id_index', ['jid', 'id']),
new Index('messages_jid_timestamp_index', ['jid', 'messageTimestamp']),
new Index('timestamp_index', ['messageTimestamp']),
],
),
new Schema(
'labels',
[new Field('id', 'TEXT'), new Field('data', 'TEXT')],
[new Index('labels_id_index', ['id'])],
),
new Schema(
'labelAssociations',
[
new Field('id', 'TEXT'),
new Field('type', 'TEXT'),
new Field('labelId', 'TEXT'),
new Field('chatId', 'TEXT'),
new Field('messageId', 'TEXT'),
new Field('data', 'TEXT'),
],
[
new Index('label_assoc_id_index', ['id']),
new Index('label_assoc_type_label_index', ['type', 'labelId']),
new Index('label_assoc_type_chat_index', ['type', 'chatId']),
new Index('label_assoc_type_message_index', ['type', 'messageId']),
],
),
];
@@ -0,0 +1,23 @@
import { Chat } from '@adiwajshing/baileys';
import { IChatRepository } from '../IChatRepository';
import { Sqlite3KVRepository } from './Sqlite3KVRepository';
export class Sqlite3ChatRepository
extends Sqlite3KVRepository<Chat>
implements IChatRepository
{
async getAllWithMessages(limit?: number, offset?: number): Promise<Chat[]> {
// Get chats with conversationTimestamp is not Null
let query = this.select()
.whereNotNull('conversationTimestamp')
.orderBy('conversationTimestamp', 'desc');
if (limit != null) {
query = query.limit(limit);
}
if (offset != null) {
query = query.offset(offset);
}
return await this.all(query);
}
}
@@ -0,0 +1,8 @@
import { Contact } from '@adiwajshing/baileys';
import { IContactRepository } from '../IContactRepository';
import { Sqlite3KVRepository } from './Sqlite3KVRepository';
export class Sqlite3ContactRepository
extends Sqlite3KVRepository<Contact>
implements IContactRepository {}
@@ -0,0 +1,164 @@
import { BufferJSON } from '@adiwajshing/baileys/lib/Utils';
import {
convertProtobufToPlainObject,
replaceLongsWithNumber,
} from '@waha/core/engines/noweb/utils';
import { Database } from 'better-sqlite3';
import Knex from 'knex';
import { Field, Schema } from '../Schema';
/**
* Key value repository with extra metadata
*/
export class Sqlite3KVRepository<Entity> {
private UPSERT_BATCH_SIZE = 100;
protected db: Database;
private readonly metadata: Map<string, (entity: Entity) => any>;
protected readonly table: string;
private readonly columns: Field[];
private knex: Knex.Knex;
constructor(
db: Database,
schema: Schema,
metadata: Map<string, (entity: Entity) => any> | null = null,
) {
this.db = db;
this.columns = schema.columns;
this.table = schema.name;
this.metadata = metadata || new Map();
// sqlite does not support inserting default values. Set the `useNullAsDefault` flag to hide this warning. (see docs https://knexjs.org/guide/query-builder.html#insert).
this.knex = Knex({ client: 'better-sqlite3', useNullAsDefault: true });
}
getAll() {
return this.all(this.select());
}
getAllByIds(ids: string[]) {
return this.all(this.select().whereIn('id', ids));
}
protected getAllBy(filters: any) {
const query = this.select().where(filters);
return this.all(query);
}
protected async getBy(filters: any) {
const query = this.select().where(filters).limit(1);
return this.get(query);
}
private dump(entity: Entity) {
const data = {};
const raw = convertProtobufToPlainObject(entity);
replaceLongsWithNumber(raw);
for (const field of this.columns) {
const fn = this.metadata.get(field.fieldName);
if (fn) {
data[field.fieldName] = fn(raw);
} else if (field.fieldName == 'data') {
data['data'] = JSON.stringify(raw, BufferJSON.replacer);
} else {
data[field.fieldName] = raw[field.fieldName];
}
}
return data;
}
save(entity: Entity) {
return this.upsertOne(entity);
}
async getById(id: string): Promise<Entity | null> {
return this.getBy({ id: id });
}
async upsertOne(entity: Entity): Promise<void> {
await this.upsertMany([entity]);
}
async upsertMany(entities: Entity[]): Promise<void> {
if (entities.length === 0) {
return;
}
const batchSize = this.UPSERT_BATCH_SIZE;
for (let i = 0; i < entities.length; i += batchSize) {
const batch = entities.slice(i, i + batchSize);
await this.upsertBatch(batch);
}
}
private async upsertBatch(entities: Entity[]): Promise<void> {
const data = entities.map((entity) => this.dump(entity));
const keys = this.columns.map((c) => c.fieldName);
const values = data.map((d) => Object.values(d)).flat();
try {
this.db
.prepare(
`INSERT INTO ${this.table} (${keys.join(', ')})
VALUES ${data
.map(() => `(${keys.map(() => '?').join(', ')})`)
.join(', ')}
ON CONFLICT(id) DO UPDATE
SET ${keys.map((key) => `${key} = excluded.${key}`).join(', ')}`,
)
.run(values);
} catch (err) {
console.error(`Error upserting data: ${err}, values: ${values}`);
throw err;
}
}
protected async deleteBy(filters: any) {
const query = this.delete().where(filters);
await this.run(query);
}
async deleteAll() {
const query = this.delete();
await this.run(query);
}
async deleteById(id: string) {
await this.deleteBy({ id: id });
}
/**
* SQL helpers
*/
protected select() {
return this.knex.select().from(this.table);
}
protected delete() {
return this.knex.delete().from(this.table);
}
protected async all(query: any) {
const sql = query.toSQL().sql;
const bind = query.toSQL().bindings;
const rows: any[] = this.db.prepare(sql).all(bind);
return rows.map((row) => JSON.parse(row.data, BufferJSON.reviver));
}
protected async get(query: any) {
const sql = query.toSQL().sql;
const bind = query.toSQL().bindings;
const row: any = this.db.prepare(sql).get(bind);
if (!row) {
return null;
}
return JSON.parse(row.data, BufferJSON.reviver);
}
protected async run(query: any) {
const sql = query.toSQL().sql;
const bind = query.toSQL().bindings;
return this.db.prepare(sql).run(bind);
}
}
@@ -0,0 +1,40 @@
import {
LabelAssociation,
LabelAssociationType,
} from '@adiwajshing/baileys/lib/Types/LabelAssociation';
import { ILabelAssociationRepository } from '@waha/core/engines/noweb/store/ILabelAssociationsRepository';
import { Sqlite3KVRepository } from './Sqlite3KVRepository';
export class Sqlite3LabelAssociationsRepository
extends Sqlite3KVRepository<LabelAssociation>
implements ILabelAssociationRepository
{
async deleteOne(association: LabelAssociation): Promise<void> {
await this.deleteBy({
type: association.type,
chatId: association.chatId,
labelId: association.labelId,
// @ts-ignore: messageId doesn't existing in ChatLabelAssociation
messageId: association.messageId || null,
});
}
async deleteByLabelId(labelId: string): Promise<void> {
await this.deleteBy({ labelId: labelId });
}
getAssociationsByLabelId(
labelId: string,
type: LabelAssociationType,
): Promise<LabelAssociation[]> {
return this.getAllBy({
type: type,
labelId: labelId,
});
}
getAssociationsByChatId(chatId: string): Promise<LabelAssociation[]> {
return this.getAllBy({ chatId: chatId, type: LabelAssociationType.Chat });
}
}
@@ -0,0 +1,8 @@
import { Label } from '@adiwajshing/baileys/lib/Types/Label';
import { ILabelsRepository } from '@waha/core/engines/noweb/store/ILabelsRepository';
import { Sqlite3KVRepository } from './Sqlite3KVRepository';
export class Sqlite3LabelsRepository
extends Sqlite3KVRepository<Label>
implements ILabelsRepository {}
@@ -0,0 +1,48 @@
import { IMessagesRepository } from '../IMessagesRepository';
import { Sqlite3KVRepository } from './Sqlite3KVRepository';
export class Sqlite3MessagesRepository
extends Sqlite3KVRepository<any>
implements IMessagesRepository
{
upsert(messages: any[]): Promise<void> {
return this.upsertMany(messages);
}
getAllByJid(jid: string, limit: number): Promise<any[]> {
const query = this.select()
.where({ jid: jid })
.limit(limit)
.orderBy('messageTimestamp', 'DESC');
return this.all(query);
}
async getByJidById(jid: string, id: string): Promise<any> {
return this.getBy({ jid: jid, id: id });
}
async updateByJidAndId(
jid: string,
id: string,
update: any,
): Promise<boolean> {
const entity = await this.getByJidById(jid, id);
if (!entity) {
return false;
}
Object.assign(entity, update);
await this.upsertOne(entity);
}
async deleteByJidByIds(jid: string, ids: string[]): Promise<void> {
const query = `DELETE
FROM ${this.table}
WHERE jid = ?
AND id IN (${ids.map(() => '?').join(', ')})`;
this.db.prepare(query).run([jid, ...ids]);
}
deleteAllByJid(jid: string): Promise<void> {
return this.deleteBy({ jid: jid });
}
}
@@ -0,0 +1,50 @@
import { Schema } from '../Schema';
// eslint-disable-next-line @typescript-eslint/no-var-requires
const Database = require('better-sqlite3');
export class Sqlite3SchemaValidation {
constructor(
private table: Schema,
private db,
) {}
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
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}`,
);
}
// Check column + type
for (const column of table.columns) {
const columnInfo = columns.find((c) => c.name === column.fieldName);
if (!columnInfo) {
throw new Error(
`Table '${table.name}' does not have column '${column.fieldName}'`,
);
}
if (columnInfo.type !== column.type) {
throw new Error(
`Table '${table.name}' column '${column.fieldName}' has type '${columnInfo.type}' but expected '${column.type}'`,
);
}
}
// Check table has expected indexes
const indexes = this.db.prepare(`PRAGMA index_list(${table.name})`).all();
const indexNames = indexes.map((index) => index.name);
for (const index of table.indexes) {
if (!indexNames.includes(index.name)) {
throw new Error(
`Table '${table.name}' does not have index '${index.name}'`,
);
}
}
}
}
@@ -0,0 +1,150 @@
import { WAMessage } from '@adiwajshing/baileys';
import { LabelAssociation } from '@adiwajshing/baileys/lib/Types/LabelAssociation';
import { ILabelAssociationRepository } from '@waha/core/engines/noweb/store/ILabelAssociationsRepository';
import { ILabelsRepository } from '@waha/core/engines/noweb/store/ILabelsRepository';
import { Sqlite3LabelAssociationsRepository } from '@waha/core/engines/noweb/store/sqlite3/Sqlite3LabelAssociationsRepository';
import { Sqlite3LabelsRepository } from '@waha/core/engines/noweb/store/sqlite3/Sqlite3LabelsRepository';
import { INowebStorage } from '../INowebStorage';
import { Field, Index, NOWEB_STORE_SCHEMA, Schema } from '../Schema';
import { Sqlite3ChatRepository } from './Sqlite3ChatRepository';
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[];
constructor(filePath: string) {
super();
this.db = new Database(filePath);
this.tables = NOWEB_STORE_SCHEMA;
}
async init() {
this.db.pragma('journal_mode = WAL;');
this.migrate();
this.validateSchema();
}
private migrate() {
this.migration0001init();
}
private validateSchema() {
for (const table of this.tables) {
new Sqlite3SchemaValidation(table, this.db).validate();
}
}
private migration0001init() {
// Contacts
this.db.exec(
'CREATE TABLE IF NOT EXISTS contacts (id TEXT PRIMARY KEY, data TEXT)',
);
this.db.exec(
'CREATE UNIQUE INDEX IF NOT EXISTS contacts_id_index ON contacts (id)',
);
// Chats
this.db.exec(
'CREATE TABLE IF NOT EXISTS chats (id TEXT PRIMARY KEY, conversationTimestamp INTEGER, data TEXT)',
);
this.db.exec(
'CREATE UNIQUE INDEX IF NOT EXISTS chats_id_index ON chats (id)',
);
this.db.exec(
'CREATE INDEX IF NOT EXISTS chats_conversationTimestamp_index ON chats (conversationTimestamp)',
);
// Messages
this.db.exec(
'CREATE TABLE IF NOT EXISTS messages (jid TEXT, id TEXT, messageTimestamp INTEGER, data TEXT)',
);
this.db.exec(
'CREATE UNIQUE INDEX IF NOT EXISTS messages_id_index ON messages (id)',
);
this.db.exec(
'CREATE INDEX IF NOT EXISTS messages_jid_id_index ON messages (jid, id)',
);
this.db.exec(
'CREATE INDEX IF NOT EXISTS messages_jid_timestamp_index ON messages (jid, messageTimestamp)',
);
this.db.exec(
'CREATE INDEX IF NOT EXISTS timestamp_index ON messages (messageTimestamp)',
);
//
// Labels
//
this.db.exec(
'CREATE TABLE IF NOT EXISTS labels (id TEXT PRIMARY KEY, data TEXT)',
);
this.db.exec(
'CREATE UNIQUE INDEX IF NOT EXISTS labels_id_index ON labels (id)',
);
// Label associations
this.db.exec(
'CREATE TABLE IF NOT EXISTS labelAssociations (id TEXT PRIMARY KEY, type TEXT, labelId TEXT, chatId TEXT, messageId TEXT, data TEXT)',
);
this.db.exec(
'CREATE UNIQUE INDEX IF NOT EXISTS label_assoc_id_index ON labelAssociations (id)',
);
this.db.exec(
'CREATE INDEX IF NOT EXISTS label_assoc_type_label_index ON labelAssociations (type, labelId)',
);
this.db.exec(
'CREATE INDEX IF NOT EXISTS label_assoc_type_chat_index ON labelAssociations (type, chatId)',
);
this.db.exec(
'CREATE INDEX IF NOT EXISTS label_assoc_type_message_index ON labelAssociations (type, messageId)',
);
}
async close() {
this.db.close();
}
getContactsRepository() {
return new Sqlite3ContactRepository(this.db, this.getSchema('contacts'));
}
getChatRepository() {
return new Sqlite3ChatRepository(this.db, this.getSchema('chats'));
}
getLabelsRepository(): ILabelsRepository {
return new Sqlite3LabelsRepository(this.db, this.getSchema('labels'));
}
getLabelAssociationRepository(): ILabelAssociationRepository {
const metadata = this.getLabelAssociationMetadata();
return new Sqlite3LabelAssociationsRepository(
this.db,
this.getSchema('labelAssociations'),
metadata,
);
}
getMessagesRepository() {
const metadata = this.getMessagesMetadata();
return new Sqlite3MessagesRepository(
this.db,
this.getSchema('messages'),
metadata,
);
}
getSchema(name: string) {
const schema = this.tables.find((table) => table.name === name);
if (!schema) {
throw new Error(`Schema not found: ${name}`);
}
return schema;
}
}
@@ -0,0 +1,115 @@
import {
AuthenticationCreds,
AuthenticationState,
proto,
} from '@adiwajshing/baileys';
import { BufferJSON, initAuthCreds } from '@adiwajshing/baileys/lib/Utils';
import { mkdir, readFile, stat, unlink, writeFile } from 'fs/promises';
import { join } from 'path';
// eslint-disable-next-line @typescript-eslint/no-var-requires
const AsyncLock = require('async-lock');
// We need to lock files due to the fact that we are using async functions to read and write files
// https://github.com/WhiskeySockets/Baileys/issues/794
// https://github.com/nodejs/node/issues/26338
// Default pending is 1000, set it to infinity
// https://github.com/rogierschouten/async-lock/issues/63
const fileLock = new AsyncLock({ maxPending: Infinity });
/**
* stores the full authentication state in a single folder.
* Far more efficient than singlefileauthstate
*
* Again, I wouldn't endorse this for any production level use other than perhaps a bot.
* Would recommend writing an auth state for use with a proper SQL or No-SQL DB
* */
export const useMultiFileAuthState = async (
folder: string,
): Promise<{
state: AuthenticationState;
saveCreds: () => Promise<void>;
}> => {
const writeData = (data: any, file: string) => {
const filePath = join(folder, fixFileName(file));
return fileLock.acquire(filePath, () =>
writeFile(join(filePath), JSON.stringify(data, BufferJSON.replacer)),
);
};
const readData = async (file: string) => {
try {
const filePath = join(folder, fixFileName(file));
const data = await fileLock.acquire(filePath, () =>
readFile(filePath, { encoding: 'utf-8' }),
);
return JSON.parse(data, BufferJSON.reviver);
} catch (error) {
return null;
}
};
const removeData = async (file: string) => {
try {
const filePath = join(folder, fixFileName(file));
await fileLock.acquire(filePath, () => unlink(filePath));
} catch {}
};
const folderInfo = await stat(folder).catch(() => {
return null;
});
if (folderInfo) {
if (!folderInfo.isDirectory()) {
throw new Error(
`found something that is not a directory at ${folder}, either delete it or specify a different location`,
);
}
} else {
await mkdir(folder, { recursive: true });
}
const fixFileName = (file?: string) =>
file?.replace(/\//g, '__')?.replace(/:/g, '-') || '';
const creds: AuthenticationCreds =
(await readData('creds.json')) || initAuthCreds();
return {
state: {
creds,
keys: {
get: async (type, ids) => {
const data = {};
await Promise.all(
ids.map(async (id) => {
let value = await readData(`${type}-${id}.json`);
if (type === 'app-state-sync-key' && value) {
value = proto.Message.AppStateSyncKeyData.fromObject(value);
}
data[id] = value;
}),
);
return data;
},
set: async (data) => {
const tasks: Promise<void>[] = [];
for (const category in data) {
for (const id in data[category]) {
const value = data[category][id];
const file = `${category}-${id}.json`;
tasks.push(value ? writeData(value, file) : removeData(file));
}
}
await Promise.all(tasks);
},
},
},
saveCreds: () => {
return writeData(creds, 'creds.json');
},
};
};
+71
View File
@@ -12,3 +12,74 @@ export function extractMediaContent(
content?.stickerMessage;
return mediaContent;
}
interface Long {
low: number;
high: number;
unsigned: boolean;
toNumber?(): number;
}
type AnyObject = { [key: string]: any };
export const replaceLongsWithNumber = (obj: AnyObject): void => {
for (const key in obj) {
if (obj.hasOwnProperty(key)) {
if (isObjectALong(obj[key])) {
obj[key] = toNumber(obj[key]);
} else if (typeof obj[key] === 'object' && obj[key] !== null) {
replaceLongsWithNumber(obj[key]);
}
}
}
};
export function convertProtobufToPlainObject(obj: any): any {
if (typeof obj !== 'object' || obj === null) {
return obj;
}
// Ignore Buffer
if (
Buffer.isBuffer(obj) ||
obj?.type == 'Buffer' ||
obj instanceof Uint8Array
) {
return obj;
}
// Remove empty array
if (Array.isArray(obj) && obj.length === 0) {
return undefined;
}
if (Array.isArray(obj)) {
return obj.map((item) => convertProtobufToPlainObject(item));
}
const plainObject: any = {};
Object.keys(obj).forEach((key) => {
const value = obj[key];
plainObject[key] = convertProtobufToPlainObject(value);
});
return { ...plainObject };
}
const isObjectALong = (value: any): value is Long => {
return (
value &&
typeof value === 'object' &&
'low' in value &&
'high' in value &&
'unsigned' in value
);
};
const toNumber = (longValue: Long): number => {
const { low, high, unsigned } = longValue;
const result = unsigned ? low >>> 0 : low + high * 0x100000000;
return result;
};
+7 -6
View File
@@ -1,4 +1,5 @@
import { UnprocessableEntityException } from '@nestjs/common/exceptions/unprocessable-entity.exception';
import { IMediaEngineProcessor } from '@waha/core/media/IMediaEngineProcessor';
import * as Buffer from 'buffer';
import { create, CreateConfig, Message, Whatsapp } from 'venom-bot';
@@ -21,7 +22,6 @@ import {
WAHASessionStatus,
} from '../../../structures/enums.dto';
import { WAMessage } from '../../../structures/responses.dto';
import { IEngineMediaProcessor } from '../../abc/media.abc';
import { WAHAInternalEvent, WhatsappSession } from '../../abc/session.abc';
import { NotImplementedByEngineError } from '../../exceptions';
import { QR } from '../../QR';
@@ -69,7 +69,7 @@ export class WhatsappSessionVenomCore extends WhatsappSession {
this.qr.save(base64Qrimg, urlCode);
this.printQR(this.qr);
this.status = WAHASessionStatus.SCAN_QR_CODE;
this.log.debug('Number of attempts to read the qrcode: ', attempts);
this.logger.debug('Number of attempts to read the qrcode: ', attempts);
};
}
@@ -79,7 +79,7 @@ export class WhatsappSessionVenomCore extends WhatsappSession {
this.whatsapp = await this.buildClient();
} catch (error) {
this.status = WAHASessionStatus.FAILED;
this.log.error(error);
this.logger.error(error);
this.qr.save('');
return;
}
@@ -90,8 +90,9 @@ export class WhatsappSessionVenomCore extends WhatsappSession {
}
async stop() {
await this.whatsapp.close();
this.status = WAHASessionStatus.STOPPED;
this.events.removeAllListeners();
await this.whatsapp.close();
}
subscribeEngineEvent(event: WAHAEvents | string, handler: (message) => void) {
@@ -247,7 +248,7 @@ export class WhatsappSessionVenomCore extends WhatsappSession {
protected downloadMedia(message: Message) {
const processor = new EngineMediaProcessor(this);
return this.mediaManager.processMedia(processor, message);
return this.mediaManager.processMedia(processor, message, this.name);
}
private processIncomingMessage(message: Message) {
@@ -279,7 +280,7 @@ export class WhatsappSessionVenomCore extends WhatsappSession {
}
}
export class EngineMediaProcessor implements IEngineMediaProcessor<Message> {
export class EngineMediaProcessor implements IMediaEngineProcessor<Message> {
constructor(public session: WhatsappSessionVenomCore) {}
hasMedia(message: any): boolean {
-535
View File
@@ -1,535 +0,0 @@
<!doctype html>
<html class="no-js" dir="ltr" loc="en">
<head>
<meta charset="utf-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1" />
<title>WhatsApp Web</title>
<meta name="viewport" content="width=device-width" />
<meta name="google" content="notranslate" />
<meta name="format-detection" content="telephone=no" />
<meta
name="description"
content="Quickly send and receive WhatsApp messages right from your computer."
/>
<meta
name="og:description"
content="Quickly send and receive WhatsApp messages right from your computer."
/>
<meta name="og:url" content="https://web.whatsapp.com/" />
<meta name="og:title" content="WhatsApp Web" />
<meta
name="og:image"
content="https://static.facebook.com/images/whatsapp/www/whatsapp-promo.png"
/>
<link
id="favicon"
rel="shortcut icon"
href="/img/favicon/1x/favicon.png"
type="image/png"
/>
<link
rel="apple-touch-icon"
sizes="192x192"
href="/apple-touch-icon.png"
type="image/png"
/>
<meta
name="theme-color"
content="#111b21"
media="(prefers-color-scheme: dark)"
/>
<meta name="theme-color" content="#f0f2f5" />
<link
id="whatsapp-pwa-manifest-link"
rel="manifest"
href="/manifest.json"
crossorigin="use-credentials"
/>
<style>
#initial_startup {
--startup-background: #f0f2f5;
--startup-background-rgb: 240, 242, 245;
--startup-icon: #bbc5cb;
--secondary-lighter: #8696a0;
--primary-title: #41525d;
--progress-primary: #00c298;
--progress-background: #e9edef;
}
.dark #initial_startup {
--startup-background: #111b21;
--startup-background-rgb: 17, 27, 33;
--startup-icon: #676f73;
--primary-title: rgba(233, 237, 239, 0.88);
--secondary-lighter: #667781;
--progress-primary: #0b846d;
--progress-background: #233138;
}
#app,
body,
html {
width: 100%;
height: 100%;
padding: 0;
margin: 0;
overflow: hidden;
}
#app {
position: absolute;
top: 0;
left: 0;
}
#initial_startup {
position: fixed;
top: 0;
left: 0;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
width: 100%;
height: 100%;
user-select: none;
background-color: var(--startup-background);
}
#initial_startup .graphic {
margin-top: -40px;
color: var(--startup-icon);
}
#initial_startup .graphic .resume-logo {
transform: translateX(calc(50% - 52px / 2));
}
#initial_startup .graphic::after {
position: relative;
top: -100%;
left: calc(50% - 72px * 2 - 72px / 2);
display: block;
width: calc(72px * 3);
height: 100%;
content: '';
background: linear-gradient(
to right,
rgba(var(--startup-background-rgb), 0.5) 0,
rgba(var(--startup-background-rgb), 0.5) 33.33%,
rgba(var(--startup-background-rgb), 0) 44.1%,
rgba(var(--startup-background-rgb), 0) 55.8%,
rgba(var(--startup-background-rgb), 0.5) 66.66%,
rgba(var(--startup-background-rgb), 0.5) 100%
);
opacity: 1;
animation: shimmer 1.5s linear 0.6s infinite;
}
html[dir='rtl'] #initial_startup .graphic::after {
animation-direction: reverse;
}
@keyframes shimmer {
from {
left: calc(50% - 72px * 2 - 72px / 2);
}
to {
left: calc(50% - 72px / 2);
}
}
#initial_startup .progress {
position: relative;
width: 420px;
height: 3px;
margin-top: 40px;
}
#initial_startup .progress progress {
vertical-align: top;
}
#initial_startup .main {
margin-top: 40px;
font-size: 17px;
color: var(--primary-title);
}
#initial_startup .secondary {
margin-top: 12px;
font-size: 14px;
color: var(--secondary-lighter);
}
#initial_startup .secondary span {
display: inline-block;
margin-bottom: 2px;
vertical-align: middle;
}
progress {
-webkit-appearance: none;
appearance: none;
width: 100%;
height: 3px;
margin: 0;
color: var(--progress-primary);
background-color: var(--progress-background);
border: none;
}
progress[value]::-webkit-progress-bar {
background-color: var(--progress-background);
}
progress[value]::-moz-progress-bar,
progress[value]::-webkit-progress-value {
background-color: var(--progress-primary);
transition: width 0.45s ease;
}
</style>
<link
href="/stylex-f0601c57714fe972a8c4d2c2562f8082.css"
rel="stylesheet"
/>
<link href="/app-6113f1562d101247269b.css" rel="stylesheet" />
</head>
<body class="web">
<script
data-binary-transparency-hash-key="inline-js-d11c72a06542fc3b43af600028cf0f996e4b9b178dfa047e18730efaf74aab0b"
>
try {
var systemThemeDark,
theme = window.localStorage.getItem('theme'),
systemThemeMode = window.localStorage.getItem('system-theme-mode');
if (('true' === systemThemeMode || !theme) && window.matchMedia) {
var systemTheme = window.matchMedia('(prefers-color-scheme: dark)');
systemThemeDark = systemTheme && systemTheme.matches;
}
var darkTheme = '"dark"' === theme || Boolean(systemThemeDark);
darkTheme && document.body.classList.add('dark');
} catch (e) {}
</script>
<div id="app"></div>
<div id="hard_expire_time" data-time="1719543128.405"></div>
<div id="initial_startup">
<div class="graphic">
<span
><svg width="250" height="52" xmlns="http://www.w3.org/2000/svg">
<path
class="resume-logo"
d="M37.7 31.2c-.6-.4-3.8-2-4.4-2.1-.6-.2-1-.4-1.4.3l-2 2.5c-.4.4-.8.5-1.5.2-.6-.3-2.7-1-5.1-3.2-2-1.7-3.2-3.8-3.6-4.5-.4-.6 0-1 .3-1.3l1-1.1.6-1.1c.2-.4 0-.8 0-1.1l-2-4.8c-.6-1.3-1.1-1-1.5-1.1h-1.2c-.5 0-1.2.1-1.8.8-.5.6-2.2 2.2-2.2 5.3 0 3.2 2.3 6.3 2.6 6.7.3.4 4.6 7 11 9.7l3.7 1.4c1.5.5 3 .4 4 .2 1.3-.1 3.9-1.5 4.4-3 .5-1.5.5-2.8.4-3-.2-.4-.6-.5-1.3-.8M26 47.2c-3.9 0-7.6-1-11-3l-.7-.4-8.1 2L8.4 38l-.6-.8A21.4 21.4 0 0126 4.4a21.3 21.3 0 0121.4 21.4c0 11.8-9.6 21.4-21.4 21.4M44.2 7.6a25.8 25.8 0 00-40.6 31L0 52l13.7-3.6A25.8 25.8 0 0044.3 7.5"
fill="currentColor"
></path></svg
></span>
</div>
<div class="progress">
<progress value="0" max="100" dir="ltr"></progress>
</div>
<div class="main">WhatsApp</div>
<div class="secondary">
<span
><svg width="10" height="12" xmlns="http://www.w3.org/2000/svg">
<path
d="M5 1.6c1.4 0 2.5 1 2.6 2.4v1.5h.2c.5 0 1 .4 1 1V10c0 .6-.5 1-1 1H2.3a1 1 0 01-1.1-1V6.5c0-.6.5-1 1-1h.2V4.2c0-1.4 1-2.5 2.4-2.6H5zm0 1.2c-.7 0-1.3.6-1.3 1.3v1.4h2.6V4.2c0-.7-.4-1.2-1-1.3H5z"
fill="currentColor"
></path>
</svg> </span
>&nbsp;End-to-end encrypted
</div>
</div>
<link
rel="preload"
crossorigin="anonymous"
as="fetch"
href="/binary-transparency-manifest-2.2412.54.json"
id="binary-transparency-manifest-preload"
/>
<script src="/libsignal-protocol-ee5b8ba.min.js"></script>
<script defer="defer" src="/runtime.295f0230b6dbd8983513.js"></script>
<script
data-binary-transparency-hash-key="inline-js-cdf06293c99d00ee0c1bc0fcfdbd76a7ae9aef6290926dbff0cba3a7994b7fca"
>
/*! Copyright (c) 2024 WhatsApp Inc. All Rights Reserved. */
(self.webpackChunkwhatsapp_web_client =
self.webpackChunkwhatsapp_web_client || []).push([
[5617],
{
307914: (e) => {
e.exports = function (e) {
return e && e.__esModule ? e : { default: e };
};
},
595318: (e) => {
(e.exports = function (e) {
return e && e.__esModule ? e : { default: e };
}),
(e.exports.default = e.exports),
(e.exports.__esModule = !0);
},
415227: (e, t) => {
'use strict';
Object.defineProperty(t, '__esModule', { value: !0 }),
(t.default = function (e) {
const t = new Error(e);
if (void 0 === t.stack)
try {
throw t;
} catch (e) {}
return t;
});
},
670983: (e, t, r) => {
'use strict';
var n = r(307914);
Object.defineProperty(t, '__esModule', { value: !0 }),
(t.default = function (e) {
let t =
arguments.length > 1 && void 0 !== arguments[1]
? arguments[1]
: '?';
if (null == e)
throw (0, o.default)('Unexpected null or undefined: ' + t);
return e;
});
var o = n(r(415227));
},
250473: (e, t, r) => {
'use strict';
Object.defineProperty(t, '__esModule', { value: !0 }),
(t.UNIQUE_UPLOADS_ALLOWED_BEFORE_THROTTLE =
t.TICKET_URL =
t.CLB_URL =
t.CLB_TOKEN =
t.CLB_CHECK_URL =
void 0);
var n = r(508247);
(t.CLB_CHECK_URL =
'https://crashlogs.whatsapp.net/wa_fls_upload_check'),
(t.CLB_TOKEN =
'1063127757113399|745146ffa34413f9dbb5469f5370b7af'),
(t.CLB_URL = 'https://crashlogs.whatsapp.net/wa_clb_data');
const o = n.DYN_ORIGIN + 'web-contact-us';
(t.TICKET_URL = o), (t.UNIQUE_UPLOADS_ALLOWED_BEFORE_THROTTLE = 0);
},
207024: (e, t, r) => {
'use strict';
var n = r(595318);
Object.defineProperty(t, '__esModule', { value: !0 }),
(t.getDistribution = function () {
let e = 'unknown';
return (e = 'prod'), 'web_prod';
}),
(t.getLogUserAgent = function (e) {
let t,
{ appVersion: r, browser: n, device: o } = e;
return (t = 'Web/' + n), `WhatsApp/${r} ${t} Device/${o}`;
}),
n(r(472514)),
n(r(556869));
},
794858: (e, t, r) => {
'use strict';
var n = r(595318),
o = n(r(670983)),
a = r(508247),
s = r(250473),
c = r(207024),
_ = n(r(174285)),
u = r(425017);
function i(e) {
!(function (e, t) {
const r = window.navigator.userAgent;
if (r === p && e.includes('getElementsByTagName')) return;
const n = new FormData(),
i = new Blob([e], { type: 'text/plain' });
n.append(
'from_jid',
(function () {
if (f) return f;
try {
(f = JSON.parse(
(0, o.default)(_.default, 'localStorage').getItem(
d.UNKNOWN_ID,
),
)),
f && (f = f.replace('-', ''));
} catch (e) {}
if (!f) {
f = 'unknown' + Math.floor(1e10 * Math.random());
const e = f;
try {
(0, o.default)(_.default, 'localStorage').setItem(
d.UNKNOWN_ID,
JSON.stringify(e),
);
} catch (e) {}
}
return (0, o.default)(f, 'id');
})(),
),
n.append(
'agent',
(0, c.getLogUserAgent)(
(0, u.parseUASimple)(r, a.VERSION_BASE),
),
),
n.append('file', i, 'logs.txt'),
n.append('tags', 'load');
const l = new XMLHttpRequest(),
I =
s.CLB_URL +
'?access_token=' +
encodeURIComponent(s.CLB_TOKEN);
l.open('POST', I, !0), l.send(n);
})(e);
}
function l(e) {
let { error: t, reason: r, stack: n } = e;
const o = new Date().toISOString();
return `${o}: error: ${t}\n${o}: reason for logs: ${r}\n${o}: userAgent: ${window.navigator.userAgent}\n${n}`;
}
null == window.onerror &&
(window.onerror = function (e, t, r) {
const n = t.split('?')[0];
return "Uncaught SyntaxError: Unexpected token '<'" === e
? ((function (e) {
i(
l({
error: 'failed to load a js or css bundle',
reason: `failed to load [${e
.split('/')[3]
.replace(/^\//, '')}]`,
stack: '',
}),
);
})(n),
!0)
: (i(
l({
error: e,
reason: `Error at [${n}:${r}]`,
stack: '',
}),
),
!1);
});
const d = { UNKNOWN_ID: a.UNKNOWN_ID },
p =
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/60.0.3112.113 Safari/537.36';
let f;
},
425017: (e, t) => {
'use strict';
function r(e) {
return e.includes('windows')
? 'Windows'
: e.includes('mac')
? 'Mac OS'
: e.includes('linux')
? 'Linux'
: 'Unparsed';
}
Object.defineProperty(t, '__esModule', { value: !0 }),
(t.parseUASimple = function (e, t) {
const n = e.toLowerCase();
return { browser: s(n), device: r(n), appVersion: t };
});
const n = /(chrome|firefox)\/([\w\.]+)/i,
o = /(edge|opr)\/([\w\.]+)/i,
a = {
chrome: 'Chrome',
edge: 'Edge',
opr: 'Opera',
firefox: 'Firefox',
};
function s(e) {
const t = e.match(o) || e.match(n);
return null == t ? 'Unparsed' : `${a[t[1]]} ${t[2]}`;
}
},
174285: (e, t) => {
'use strict';
let r;
Object.defineProperty(t, '__esModule', { value: !0 }),
(t.default = void 0);
try {
r = window.localStorage;
} catch (e) {}
var n = r;
t.default = n;
},
508247: (e, t) => {
'use strict';
Object.defineProperty(t, '__esModule', { value: !0 }),
(t.WS_ORIGIN =
t.WEB_PUBLIC_PATH =
t.VERSION_TERTIARY =
t.VERSION_STR =
t.VERSION_SECONDARY =
t.VERSION_PRIMARY =
t.VERSION_BASE =
t.USER_TIMINGS =
t.UNKNOWN_ID =
t.LOCALHOST_ENV_CONFIG =
t.HASH_PLACEHOLDER =
t.FLB_PLATFORM =
t.FB_APP_ID =
t.DYN_ORIGIN =
t.DIFF_VERSION =
t.DIFF_ID =
t.BUILD_URL =
t.BUILD_TIMESTAMP =
t.BUILD_ID =
void 0);
t.DIFF_ID = '';
(t.DIFF_VERSION = ''),
(t.LOCALHOST_ENV_CONFIG = !1),
(t.HASH_PLACEHOLDER = '507042ed75b89fb00e8ef35da3541122cbc261e6'),
(t.VERSION_PRIMARY = '2'),
(t.VERSION_SECONDARY = '2412'),
(t.VERSION_TERTIARY = '54'),
(t.VERSION_BASE = '2.2412.54'),
(t.VERSION_STR = '2.2412.54'),
(t.DYN_ORIGIN = 'https://web.whatsapp.com/'),
(t.WS_ORIGIN = ''),
(t.FLB_PLATFORM = 'web'),
(t.FB_APP_ID = ''),
(t.UNKNOWN_ID = 'WAUnknownID'),
(t.BUILD_ID = '583059957'),
(t.BUILD_TIMESTAMP = 'March 29, 2024'),
(t.WEB_PUBLIC_PATH = '/'),
(t.BUILD_URL = 'https://web.whatsapp.com/'),
(t.USER_TIMINGS = !1);
},
472514: (e, t) => {
'use strict';
Object.defineProperty(t, '__esModule', { value: !0 }),
(t.default = function (e) {
return e.default;
});
},
556869: (e, t) => {
'use strict';
Object.defineProperty(t, '__esModule', { value: !0 }),
(t.default = function e(t) {
const r = new Error(t);
if (void 0 === r.stack)
try {
throw e;
} catch (e) {}
return r;
});
},
},
(e) => {
'use strict';
e((e.s = 794858));
},
]);
</script>
<script defer="defer" src="/vendor1~app.264a01b6133c4a120327.js"></script>
<script defer="defer" src="/app.2c7f8dd150ae88037cde.js"></script>
<script
data-binary-transparency-hash-key="inline-js-9c8cd3d0f4d5af8c7f5cd1c45208edd308bfe472e353a50e4cbacbf6cb58627c"
>
((i) => {
const l = document.getElementById(i);
l &&
fetch(l.href)
.then((b) => b.text())
.then((code) => {
const script = document.createElement('script');
script.id = 'binary-transparency-manifest';
script.type = 'application/json';
script.innerHTML = code;
document.body.append(script);
});
})('binary-transparency-manifest-preload');
</script>
</body>
</html>
+15
View File
@@ -0,0 +1,15 @@
exports.LoadUtils = () => {
window.WAHA = {};
window.WAHA.getChats = async (limit, offset) => {
let chats = window.Store.Chat.getModelsArray();
if (limit || offset) {
offset = offset || 0;
limit = limit || Infinity;
chats = chats.slice(offset, offset + limit);
}
const chatPromises = chats.map((chat) => window.WWebJS.getChatModel(chat));
return await Promise.all(chatPromises);
};
};
+38
View File
@@ -0,0 +1,38 @@
import { Client } from 'whatsapp-web.js';
// eslint-disable-next-line @typescript-eslint/no-var-requires
const { LoadUtils } = require('./WahaInjected');
// eslint-disable-next-line @typescript-eslint/no-var-requires
const ChatFactory = require('whatsapp-web.js/src/factories/ChatFactory');
export class WebjsClient extends Client {
async inject() {
// Even tho this.inject is not defined as interface in Client.ts
// We can still call and override it
// @ts-ignore
await super.inject();
//Load util functions (serializers, helper functions)
await this.pupPage.evaluate(LoadUtils);
}
async getChats(limit?: number, offset?: number) {
if (limit == null && offset == null) {
return await super.getChats();
}
return await this.getChats2(limit, offset);
}
private async getChats2(limit?: number, offset?: number) {
const chats = await this.pupPage.evaluate(
async (limit, offset) => {
// @ts-ignore
return await window.WAHA.getChats(limit, offset);
},
limit,
offset,
);
return chats.map((chat) => ChatFactory.create(this, chat));
}
}
+364 -76
View File
@@ -1,17 +1,25 @@
import { UnprocessableEntityException } from '@nestjs/common/exceptions/unprocessable-entity.exception';
import {
Chat,
Client,
ClientOptions,
Contact,
Events,
GroupChat,
Location,
Message,
Reaction,
} from 'whatsapp-web.js';
import { Message as MessageInstance } from 'whatsapp-web.js/src/structures';
getChannelInviteLink,
WAHAInternalEvent,
WhatsappSession,
} from '@waha/core/abc/session.abc';
import { WebjsClient } from '@waha/core/engines/webjs/WebjsClient';
import {
AvailableInPlusVersion,
NotImplementedByEngineError,
} from '@waha/core/exceptions';
import { IMediaEngineProcessor } from '@waha/core/media/IMediaEngineProcessor';
import { QR } from '@waha/core/QR';
import { parseBool } from '@waha/helpers';
import { CallData } from '@waha/structures/calls.dto';
import {
Channel,
ChannelRole,
CreateChannelRequest,
ListChannelsQuery,
} from '@waha/structures/channels.dto';
import { ChatArchiveEvent, GetChatsQuery } from '@waha/structures/chats.dto';
import {
ChatRequest,
CheckNumberStatusQuery,
@@ -27,55 +35,74 @@ import {
MessageVoiceRequest,
SendSeenRequest,
WANumberExistResult,
} from '../../../structures/chatting.dto';
import { ContactQuery, ContactRequest } from '../../../structures/contacts.dto';
} from '@waha/structures/chatting.dto';
import { ContactQuery, ContactRequest } from '@waha/structures/contacts.dto';
import {
ACK_UNKNOWN,
SECOND,
WAHAEngine,
WAHAEvents,
WAHAPresenceStatus,
WAHASessionStatus,
WAMessageAck,
} from '../../../structures/enums.dto';
} from '@waha/structures/enums.dto';
import {
CreateGroupRequest,
ParticipantsRequest,
SettingsSecurityChangeInfo,
} from '../../../structures/groups.dto';
} from '@waha/structures/groups.dto';
import { Label, LabelID } from '@waha/structures/labels.dto';
import { ReplyToMessage } from '@waha/structures/message.dto';
import { WAMessage, WAMessageReaction } from '@waha/structures/responses.dto';
import { MeInfo } from '@waha/structures/sessions.dto';
import { WAMessageRevokedBody } from '@waha/structures/webhooks.dto';
import { waitUntil } from '@waha/utils/promiseTimeout';
import { SingleDelayedJobRunner } from '@waha/utils/SingleDelayedJobRunner';
import {
WAMessage,
WAMessageReaction,
} from '../../../structures/responses.dto';
import { MeInfo } from '../../../structures/sessions.dto';
import { WAMessageRevokedBody } from '../../../structures/webhooks.dto';
import { IEngineMediaProcessor } from '../../abc/media.abc';
import { WAHAInternalEvent, WhatsappSession } from '../../abc/session.abc';
import {
AvailableInPlusVersion,
NotImplementedByEngineError,
} from '../../exceptions';
import { QR } from '../../QR';
Call,
Channel as WEBJSChannel,
Chat,
ClientOptions,
Contact,
Events,
GroupChat,
Label as WEBJSLabel,
Location,
Message,
Reaction,
} from 'whatsapp-web.js';
import { Message as MessageInstance } from 'whatsapp-web.js/src/structures';
// eslint-disable-next-line @typescript-eslint/no-var-requires
const QRCode = require('qrcode');
// eslint-disable-next-line @typescript-eslint/no-var-requires
const qrcode = require('qrcode-terminal');
export interface WebJSConfig {
webVersion?: string;
}
export class WhatsappSessionWebJSCore extends WhatsappSession {
private START_ATTEMPT_DELAY_SECONDS = 2;
engine = WAHAEngine.WEBJS;
protected engineConfig?: WebJSConfig;
whatsapp: Client;
private startDelayedJob: SingleDelayedJobRunner;
private shouldRestart: boolean;
whatsapp: WebjsClient;
protected qr: QR;
public constructor(config) {
super(config);
this.qr = new QR();
this.shouldRestart = true;
// Restart job if session failed
this.startDelayedJob = new SingleDelayedJobRunner(
'start-engine',
this.START_ATTEMPT_DELAY_SECONDS * SECOND,
this.logger,
);
}
/**
@@ -87,19 +114,21 @@ export class WhatsappSessionWebJSCore extends WhatsappSession {
protected getClientOptions(): ClientOptions {
const path = this.getClassDirName();
const webVersion = this.engineConfig?.webVersion || '2.2412.54';
this.log.debug(`Using web version: '${webVersion}'`);
const webVersion =
this.engineConfig?.webVersion || '2.3000.1014522270-alpha';
// this.log.debug(`Using web version: '${webVersion}'`);
return {
puppeteer: {
headless: true,
executablePath: this.getBrowserExecutablePath(),
args: this.getBrowserArgsForPuppeteer(),
dumpio: this.isDebugEnabled(),
},
webVersion: webVersion,
webVersionCache: {
type: 'local',
path: path,
strict: true,
type: 'none',
// path: path,
// strict: true,
},
};
}
@@ -107,7 +136,27 @@ export class WhatsappSessionWebJSCore extends WhatsappSession {
protected async buildClient() {
const clientOptions = this.getClientOptions();
this.addProxyConfig(clientOptions);
return new Client(clientOptions);
return new WebjsClient(clientOptions);
}
private restartClient() {
if (!this.shouldRestart) {
this.logger.debug(
'Should not restart the client, ignoring restart request',
);
return;
}
this.startDelayedJob.schedule(async () => {
if (!this.shouldRestart) {
this.logger.warn(
'Should not restart the client, ignoring restart request',
);
return;
}
await this.end();
await this.start();
});
}
protected addProxyConfig(clientOptions: ClientOptions) {
@@ -127,8 +176,8 @@ export class WhatsappSessionWebJSCore extends WhatsappSession {
}
}
async start() {
this.status = WAHASessionStatus.STARTING;
protected async init() {
this.shouldRestart = true;
this.whatsapp = await this.buildClient();
this.whatsapp
.initialize()
@@ -136,17 +185,21 @@ export class WhatsappSessionWebJSCore extends WhatsappSession {
// Listen for browser disconnected event
this.whatsapp.pupBrowser.on('disconnected', () => {
this.status = WAHASessionStatus.FAILED;
this.log.error('The browser has been disconnected');
this.logger.error('The browser has been disconnected');
this.restartClient();
});
// Listen for page close event
this.whatsapp.pupPage.on('close', () => {
this.status = WAHASessionStatus.FAILED;
this.log.error('The WhatsApp Web page has been closed');
this.logger.error('The WhatsApp Web page has been closed');
this.restartClient();
});
// Listen for page error event
if (this.isDebugEnabled()) {
this.log.debug("Logging 'console' event for web page");
this.logger.debug("Logging 'console' event for web page");
this.whatsapp.pupPage.on('console', (msg) =>
this.log.debug(`WEBJS page log: ${msg.text()}`),
this.logger.debug(`WEBJS page log: ${msg.text()}`),
);
this.whatsapp.pupPage.evaluate(() =>
console.log(`url is ${location.href}`),
@@ -155,7 +208,8 @@ export class WhatsappSessionWebJSCore extends WhatsappSession {
})
.catch((error) => {
this.status = WAHASessionStatus.FAILED;
this.log.error(error);
this.logger.error(error);
this.restartClient();
return;
});
if (this.isDebugEnabled()) {
@@ -163,25 +217,53 @@ export class WhatsappSessionWebJSCore extends WhatsappSession {
}
this.listenConnectionEvents();
this.events.emit(WAHAInternalEvent.ENGINE_START);
}
async start() {
this.status = WAHASessionStatus.STARTING;
await this.init();
return this;
}
async stop() {
await this.whatsapp.destroy();
this.shouldRestart = false;
this.status = WAHASessionStatus.STOPPED;
this.events.removeAllListeners();
await this.end();
}
async getSessionMeInfo(): Promise<MeInfo | null> {
private async end() {
try {
this.whatsapp?.removeAllListeners();
this.startDelayedJob.cancel();
// It's possible that browser yet starting
await waitUntil(
async () => {
const result = !!this.whatsapp.pupBrowser;
this.logger.debug(`Browser is ready to be closed: ${result}`);
return result;
},
1_000,
10_000,
);
this.whatsapp?.destroy().catch((error) => {
this.logger.warn(error, 'Failed to destroy the client');
});
} catch (error) {
this.logger.error(error);
}
}
getSessionMeInfo(): MeInfo | null {
const clientInfo = this.whatsapp?.info;
if (!clientInfo) {
return null;
}
const wid = clientInfo.wid;
const meInfo: MeInfo = {
id: wid._serialized,
pushName: clientInfo.pushname,
return {
id: wid?._serialized,
pushName: clientInfo?.pushname,
};
return meInfo;
}
protected listenEngineEventsInDebugMode() {
@@ -191,14 +273,14 @@ export class WhatsappSessionWebJSCore extends WhatsappSession {
const event = Events[key];
this.whatsapp.on(event, (...data: any[]) => {
const log = { event: event, data: data };
this.log.debug(`WEBJS event: ${JSON.stringify(log)}`);
this.logger.debug({ event: log }, `WEBJS event`);
});
}
}
protected listenConnectionEvents() {
this.whatsapp.on(Events.QR_RECEIVED, async (qr) => {
this.log.debug('QR received');
this.logger.debug('QR received');
// Convert to image and save
const url = await QRCode.toDataURL(qr);
this.qr.save(url, qr);
@@ -209,13 +291,25 @@ export class WhatsappSessionWebJSCore extends WhatsappSession {
this.whatsapp.on(Events.READY, () => {
this.status = WAHASessionStatus.WORKING;
this.qr.save('');
this.log.log(`Session '${this.name}' has been authenticated!`);
this.logger.info(`Session '${this.name}' is ready!`);
});
this.whatsapp.on(Events.AUTHENTICATED, () => {
this.status = WAHASessionStatus.WORKING;
this.qr.save('');
this.logger.info(`Session '${this.name}' has been authenticated!`);
});
this.whatsapp.on(Events.AUTHENTICATION_FAILURE, () => {
this.status = WAHASessionStatus.FAILED;
this.qr.save('');
this.logger.info(`Session '${this.name}' has been disconnected!`);
});
this.whatsapp.on(Events.DISCONNECTED, () => {
this.status = WAHASessionStatus.FAILED;
this.qr.save('');
this.log.log(`Session '${this.name}' has been disconnected!`);
this.logger.info(`Session '${this.name}' has been disconnected!`);
});
}
@@ -259,10 +353,7 @@ export class WhatsappSessionWebJSCore extends WhatsappSession {
}
sendText(request: MessageTextRequest) {
const options = {
// It's fine to sent just ids instead of Contact object
mentions: request.mentions as unknown as string[],
};
const options = this.getMessageOptions(request);
return this.whatsapp.sendMessage(
this.ensureSuffix(request.chatId),
request.text,
@@ -289,11 +380,7 @@ export class WhatsappSessionWebJSCore extends WhatsappSession {
}
reply(request: MessageReplyRequest) {
const options = {
quotedMessageId: request.reply_to,
// It's fine to sent just ids instead of Contact object
mentions: request.mentions as unknown as string[],
};
const options = this.getMessageOptions(request);
return this.whatsapp.sendMessage(request.chatId, request.text, options);
}
@@ -313,7 +400,8 @@ export class WhatsappSessionWebJSCore extends WhatsappSession {
const location = new Location(request.latitude, request.longitude, {
name: request.title,
});
return this.whatsapp.sendMessage(request.chatId, location);
const options = this.getMessageOptions(request);
return this.whatsapp.sendMessage(request.chatId, location, options);
}
async sendSeen(request: SendSeenRequest) {
@@ -363,23 +451,24 @@ export class WhatsappSessionWebJSCore extends WhatsappSession {
/**
* Chats methods
*/
getChats() {
return this.whatsapp.getChats();
getChats(query: GetChatsQuery) {
return this.whatsapp.getChats(query.limit, query.offset);
}
async getChatMessages(chatId: string, limit: number, downloadMedia: boolean) {
downloadMedia = parseBool(downloadMedia);
const chat: Chat = await this.whatsapp.getChatById(
this.ensureSuffix(chatId),
);
const messages = await chat.fetchMessages({
limit: limit,
});
// Go over messages, download media, and convert to right format.
const result = [];
for (const message of messages) {
const msg = await this.processIncomingMessage(message, downloadMedia);
result.push(msg);
const promises = [];
for (const msg of messages) {
promises.push(this.processIncomingMessage(msg, downloadMedia));
}
let result = await Promise.all(promises);
result = result.filter(Boolean);
return result;
}
@@ -393,6 +482,52 @@ export class WhatsappSessionWebJSCore extends WhatsappSession {
return chat.clearMessages();
}
public chatsArchiveChat(chatId: string): Promise<any> {
const id = this.ensureSuffix(chatId);
return this.whatsapp.archiveChat(id);
}
public chatsUnarchiveChat(chatId: string): Promise<any> {
const id = this.ensureSuffix(chatId);
return this.whatsapp.unarchiveChat(id);
}
/**
*
* Label methods
*/
public async getLabels(): Promise<Label[]> {
const labels = await this.whatsapp.getLabels();
return labels.map(this.toLabel);
}
public getChatsByLabelId(labelId: string) {
return this.whatsapp.getChatsByLabelId(labelId);
}
public async getChatLabels(chatId: string): Promise<Label[]> {
const id = this.ensureSuffix(chatId);
const labels = await this.whatsapp.getChatLabels(id);
return labels.map(this.toLabel);
}
public async putLabelsToChat(chatId: string, labels: LabelID[]) {
const labelIds = labels.map((label) => label.id);
const chatIds = [this.ensureSuffix(chatId)];
await this.whatsapp.addOrRemoveLabels(labelIds, chatIds);
}
protected toLabel(label: WEBJSLabel): Label {
const color = label.colorIndex;
return {
id: label.id,
name: label.name,
color: color,
colorHex: Label.toHex(color),
};
}
/**
* Contacts methods
*/
@@ -553,6 +688,109 @@ export class WhatsappSessionWebJSCore extends WhatsappSession {
return groupChat.demoteParticipants(participantIds);
}
/**
* Channels methods
*/
protected ChatToChannel(chat: WEBJSChannel): Channel {
// @ts-ignore
const metadata = chat.channelMetadata;
let role = metadata.membershipType.toUpperCase();
if (role === 'VIEWER') {
role = ChannelRole.GUEST;
}
return {
id: chat.id._serialized,
name: chat.name,
description: chat.description,
invite: getChannelInviteLink(metadata.inviteCode),
preview: null,
picture: null,
verified: metadata.verified,
role: role,
};
}
protected ChannelMetadataToChannel(metadata: any): Channel {
let role = metadata.membershipType.toUpperCase();
if (role === 'VIEWER') {
role = ChannelRole.GUEST;
}
return {
id: metadata.id,
name: metadata.titleMetadata.title,
description: metadata.descriptionMetadata.description,
invite: metadata.inviteLink,
preview: metadata.pictureUrl,
picture: metadata.pictureUrl,
verified: metadata.isVerified,
role: role,
};
}
public async channelsList(query: ListChannelsQuery): Promise<Channel[]> {
const data = await this.whatsapp.getChannels();
let channels = data.map(this.ChatToChannel);
if (query.role) {
// @ts-ignore
channels = channels.filter((channel) => channel.role === query.role);
}
const promises = channels.map(async (channel) =>
this.whatsapp.getProfilePicUrl(channel.id),
);
const pictures = await Promise.all(promises);
channels = channels.map((channel, index) => {
channel.picture = pictures[index] || null;
channel.preview = channel.picture;
return channel;
});
return channels;
}
public channelsCreateChannel(
request: CreateChannelRequest,
): Promise<Channel> {
throw new NotImplementedByEngineError();
}
public async channelsGetChannel(id: string): Promise<Channel> {
return await this.channelsGetChannelByInviteCode(id);
}
public async channelsGetChannelByInviteCode(
inviteCode: string,
): Promise<Channel> {
const metadata = await this.whatsapp.getChannelByInviteCode(inviteCode);
const channel = this.ChannelMetadataToChannel(metadata);
channel.preview =
(await this.whatsapp.getProfilePicUrl(channel.id)) || null;
channel.picture = channel.preview;
return channel;
}
public channelsDeleteChannel(id: string): Promise<void> {
throw new NotImplementedByEngineError();
}
public channelsFollowChannel(id: string): Promise<void> {
throw new NotImplementedByEngineError();
}
public channelsUnfollowChannel(id: string): Promise<void> {
throw new NotImplementedByEngineError();
}
public channelsMuteChannel(id: string): Promise<void> {
throw new NotImplementedByEngineError();
}
public channelsUnmuteChannel(id: string): Promise<void> {
throw new NotImplementedByEngineError();
}
/**
* Presences methods
*/
public async setPresence(presence: WAHAPresenceStatus, chatId?: string) {
let chat: Chat;
switch (presence) {
@@ -633,6 +871,28 @@ export class WhatsappSessionWebJSCore extends WhatsappSession {
case WAHAEvents.GROUP_LEAVE:
this.whatsapp.on(Events.GROUP_LEAVE, handler);
return true;
case WAHAEvents.CHAT_ARCHIVE:
this.whatsapp.on('chat_archived', (chat, archived, _) => {
const body: ChatArchiveEvent = {
id: chat.id._serialized,
archived: archived,
timestamp: chat.timestamp,
};
handler(body);
});
return true;
case WAHAEvents.CALL_RECEIVED:
this.whatsapp.on('call', (call: Call) => {
const body: CallData = {
id: call.id,
from: call.from,
timestamp: call.timestamp,
isVideo: call.isVideo,
isGroup: call.isGroup,
};
handler(body);
});
return true;
default:
return false;
}
@@ -643,8 +903,8 @@ export class WhatsappSessionWebJSCore extends WhatsappSession {
try {
message = await this.downloadMedia(message);
} catch (e) {
this.log.error('Failed when tried to download media for a message');
this.log.error(e, e.stack);
this.logger.error('Failed when tried to download media for a message');
this.logger.error(e, e.stack);
}
}
return await this.toWAMessage(message);
@@ -666,6 +926,7 @@ export class WhatsappSessionWebJSCore extends WhatsappSession {
}
protected toWAMessage(message: Message): Promise<WAMessage> {
const replyTo = this.extractReplyTo(message);
// @ts-ignore
return Promise.resolve({
id: message.id._serialized,
@@ -686,11 +947,26 @@ export class WhatsappSessionWebJSCore extends WhatsappSession {
ackName: WAMessageAck[message.ack] || ACK_UNKNOWN,
location: message.location,
vCards: message.vCards,
replyTo: replyTo,
_data: message.rawData,
});
}
protected extractReplyTo(message: Message): ReplyToMessage | null {
// @ts-ignore
const quotedMsg = message.rawData.quotedMsg;
if (!quotedMsg) {
return;
}
return {
id: quotedMsg.id?.id,
participant: quotedMsg.author || quotedMsg.from,
body: quotedMsg.caption || quotedMsg.body,
};
}
public async getEngineInfo() {
// Add 1 seconds timeout
return {
WWebVersion: await this.whatsapp?.getWWebVersion(),
state: await this.whatsapp?.getState(),
@@ -705,11 +981,23 @@ export class WhatsappSessionWebJSCore extends WhatsappSession {
protected downloadMedia(message: Message) {
const processor = new EngineMediaProcessor();
return this.mediaManager.processMedia(processor, message);
return this.mediaManager.processMedia(processor, message, this.name);
}
protected getMessageOptions(request: any): any {
let mentions = request.mentions;
mentions = mentions ? mentions.map(this.ensureSuffix) : undefined;
const quotedMessageId = request.reply_to;
return {
mentions: mentions,
quotedMessageId: quotedMessageId,
};
}
}
export class EngineMediaProcessor implements IEngineMediaProcessor<Message> {
export class EngineMediaProcessor implements IMediaEngineProcessor<Message> {
hasMedia(message: Message): boolean {
if (!message.hasMedia) {
return false;
+131 -83
View File
@@ -1,21 +1,26 @@
import {
ConsoleLogger,
Injectable,
LogLevel,
NotFoundException,
UnprocessableEntityException,
} from '@nestjs/common';
import { MediaNoopStorage } from '@waha/core/media/MediaNoopStorage';
import { getPinoLogLevel, LoggerBuilder } from '@waha/utils/logging';
import { promiseTimeout, sleep } from '@waha/utils/promiseTimeout';
import { EventEmitter } from 'events';
import { PinoLogger } from 'nestjs-pino';
import { WhatsappConfigService } from '../config.service';
import { getLogLevels } from '../helpers';
import { WAHAEngine, WAHASessionStatus } from '../structures/enums.dto';
import {
WAHAEngine,
WAHAEvents,
WAHASessionStatus,
} from '../structures/enums.dto';
import {
ProxyConfig,
SessionConfig,
SessionDTO,
SessionInfo,
SessionLogoutRequest,
SessionStartRequest,
SessionStopRequest,
} from '../structures/sessions.dto';
import { WebhookConfig } from '../structures/webhooks.config.dto';
import { SessionManager } from './abc/manager.abc';
@@ -26,9 +31,8 @@ import { WhatsappSessionVenomCore } from './engines/venom/session.venom.core';
import { WhatsappSessionWebJSCore } from './engines/webjs/session.webjs.core';
import { DOCS_URL } from './exceptions';
import { getProxyConfig } from './helpers.proxy';
import { CoreMediaManager, MediaStorageCore } from './media.core';
import { MediaManagerCore } from './media/MediaManagerCore';
import { LocalSessionAuthRepository } from './storage/LocalSessionAuthRepository';
import { LocalSessionConfigRepository } from './storage/LocalSessionConfigRepository';
import { LocalStoreCore } from './storage/LocalStoreCore';
import { WebhookConductorCore } from './webhooks.core';
@@ -40,13 +44,15 @@ export class OnlyDefaultSessionIsAllowed extends UnprocessableEntityException {
}
}
export function buildLogger(name: string, levels: LogLevel[]) {
return new ConsoleLogger(name, { logLevels: levels });
}
@Injectable()
export class SessionManagerCore extends SessionManager {
private session?: WhatsappSession;
SESSION_STOP_TIMEOUT = 3000;
// session - exists and running (or failed or smth)
// null - stopped
// undefined - removed
private session: WhatsappSession | undefined | null;
private sessionConfig?: SessionConfig;
DEFAULT = 'default';
// @ts-ignore
@@ -55,25 +61,25 @@ export class SessionManagerCore extends SessionManager {
constructor(
private config: WhatsappConfigService,
private log: ConsoleLogger,
private engineConfigService: EngineConfigService,
private log: PinoLogger,
) {
super();
this.log.setContext('SessionManager');
this.session = undefined;
const engineName = this.config.getDefaultEngineName();
this.events = new EventEmitter();
this.session = null;
this.sessionConfig = null;
this.log.setContext(SessionManagerCore.name);
const engineName = this.engineConfigService.getDefaultEngineName();
this.EngineClass = this.getEngine(engineName);
this.store = new LocalStoreCore(engineName.toLowerCase());
this.sessionAuthRepository = new LocalSessionAuthRepository(this.store);
this.sessionConfigRepository = new LocalSessionConfigRepository(this.store);
this.startPredefinedSessions();
}
protected startPredefinedSessions() {
const startSessions = this.config.startSessions;
startSessions.forEach((sessionName) => {
this.start({ name: sessionName });
this.start(sessionName);
});
}
@@ -95,43 +101,60 @@ export class SessionManagerCore extends SessionManager {
}
}
async onApplicationShutdown(signal?: string) {
async beforeApplicationShutdown(signal?: string) {
if (!this.session) {
return;
}
await this.stop({ name: this.DEFAULT, logout: false });
await this.stop(this.DEFAULT, true);
}
//
// API Methods
//
async start(request: SessionStartRequest): Promise<SessionDTO> {
this.onlyDefault(request.name);
async exists(name: string): Promise<boolean> {
this.onlyDefault(name);
return this.session !== undefined;
}
isRunning(name: string): boolean {
this.onlyDefault(name);
return !!this.session;
}
async upsert(name: string, config?: SessionConfig): Promise<void> {
this.onlyDefault(name);
this.sessionConfig = config;
}
async start(name: string): Promise<SessionDTO> {
this.onlyDefault(name);
if (this.session) {
throw new UnprocessableEntityException(
`Session '${this.DEFAULT}' is already started.`,
);
}
const name = request.name;
this.log.log(`'${name}' - starting session...`);
const levels = getLogLevels(request.config?.debug);
const log = buildLogger(`WhatsappSession - ${name}`, levels);
const mediaManager = new CoreMediaManager(
new MediaStorageCore(),
this.log.info(`'${name}' - starting session...`);
const mediaManager = new MediaManagerCore(
new MediaNoopStorage(),
this.config.mimetypes,
);
const webhookLog = buildLogger(`Webhook - ${name}`, levels);
const webhook = new this.WebhookConductorClass(webhookLog);
const proxyConfig = this.getProxyConfig(request);
const logger = this.log.logger.child({
session: name,
sessionRunTimestamp: Date.now(),
});
logger.level = getPinoLogLevel(this.sessionConfig?.debug);
const loggerBuilder: LoggerBuilder = logger;
const webhook = new this.WebhookConductorClass(loggerBuilder);
const proxyConfig = this.getProxyConfig();
const sessionConfig: SessionParams = {
name,
mediaManager,
log,
loggerBuilder,
printQR: this.engineConfigService.shouldPrintQR,
sessionStore: this.store,
proxyConfig: proxyConfig,
sessionConfig: request.config,
sessionConfig: this.sessionConfig,
};
await this.sessionAuthRepository.init(name);
// @ts-ignore
@@ -139,11 +162,18 @@ export class SessionManagerCore extends SessionManager {
this.session = session;
// configure webhooks
const webhooks = this.getWebhooks(request);
const webhooks = this.getWebhooks();
webhook.configure(session, webhooks);
// configure events
session.events.on(
WAHAEvents.SESSION_STATUS,
this.handleSessionEvent(WAHAEvents.SESSION_STATUS, session),
);
// start session
await session.start();
logger.info('Session has been started.');
return {
name: session.name,
status: session.status,
@@ -151,13 +181,46 @@ export class SessionManagerCore extends SessionManager {
};
}
async stop(name: string, silent: boolean): Promise<void> {
this.onlyDefault(name);
if (!this.isRunning(name)) {
this.log.debug(`Session is not running.`, { session: name });
return;
}
this.log.info(`Stopping session...`, { session: name });
try {
const session = this.getSession(name);
await session.stop();
} catch (err) {
this.log.warn(`Error while stopping session '${name}'`);
if (!silent) {
throw err;
}
}
this.log.info(`Session has been stopped.`, { session: name });
this.session = null;
await sleep(this.SESSION_STOP_TIMEOUT);
}
async logout(name: string): Promise<void> {
this.onlyDefault(name);
await this.sessionAuthRepository.clean(name);
}
async delete(name: string): Promise<void> {
this.onlyDefault(name);
this.session = undefined;
this.sessionConfig = undefined;
}
/**
* Combine per session and global webhooks
*/
private getWebhooks(request: SessionStartRequest) {
private getWebhooks() {
let webhooks: WebhookConfig[] = [];
if (request.config?.webhooks) {
webhooks = webhooks.concat(request.config.webhooks);
if (this.sessionConfig?.webhooks) {
webhooks = webhooks.concat(this.sessionConfig.webhooks);
}
const globalWebhookConfig = this.config.getWebhookConfig();
if (globalWebhookConfig) {
@@ -169,75 +232,60 @@ export class SessionManagerCore extends SessionManager {
/**
* Get either session's or global proxy if defined
*/
protected getProxyConfig(
request: SessionStartRequest,
): ProxyConfig | undefined {
if (request.config?.proxy) {
return request.config.proxy;
protected getProxyConfig(): ProxyConfig | undefined {
if (this.sessionConfig?.proxy) {
return this.sessionConfig.proxy;
}
if (!this.session) {
return undefined;
}
const sessions = { [request.name]: this.session };
return getProxyConfig(this.config, sessions, request.name);
}
async stop(request: SessionStopRequest): Promise<void> {
this.onlyDefault(request.name);
const name = request.name;
this.log.log(`Stopping ${name} session...`);
const session = this.getSession(name);
await session.stop();
this.log.log(`"${name}" has been stopped.`);
this.session = undefined;
}
async logout(request: SessionLogoutRequest) {
const name = request.name;
this.onlyDefault(request.name);
this.stop({ name: name, logout: false })
.then(() => {
this.log.log(`Session '${name}' has been stopped.`);
})
.catch((err) => {
this.log.error(
`Error while stopping session '${name}' while logging out`,
err,
);
});
await this.sessionAuthRepository.clean(request.name);
const sessions = { [this.DEFAULT]: this.session };
return getProxyConfig(this.config, sessions, this.DEFAULT);
}
getSession(name: string): WhatsappSession {
this.onlyDefault(name);
const session = this.session;
if (!session) {
if (session === undefined) {
throw new NotFoundException(
`We didn't find a session with name '${name}'. Please start it first by using POST /sessions/start request`,
`We didn't find a session with name '${name}'.
Please start it first by using POST /sessions/${name}/start request`,
);
}
return session;
}
async getSessions(all: boolean): Promise<SessionInfo[]> {
if (!this.session) {
if (!all) {
return [];
}
if (this.session === null && all) {
return [
{
name: this.DEFAULT,
status: WAHASessionStatus.STOPPED,
config: undefined,
config: this.sessionConfig,
me: null,
},
];
}
const me = await this.session.getSessionMeInfo().catch((err) => null);
if (this.session === undefined && all) {
return [];
}
const me = this.session.getSessionMeInfo();
// Get engine info
let engineInfo = {};
if (this.session) {
try {
engineInfo = await promiseTimeout(10, this.session.getEngineInfo());
} catch (e) {
this.log.warn(
{ session: this.session },
'Error while getting engine info',
);
}
}
const engine = {
engine: this.session?.engine,
...(await this.session?.getEngineInfo().catch((err) => ({}))),
...engineInfo,
};
return [
{
-47
View File
@@ -1,47 +0,0 @@
import { WAMedia } from '../structures/responses.dto';
import {
IEngineMediaProcessor,
MediaManager,
MediaStorage,
} from './abc/media.abc';
import { DOCS_URL } from './exceptions';
export class MediaStorageCore implements MediaStorage {
async save(
messageId: string,
mimetype: string,
buffer: Buffer,
): Promise<string> {
return Promise.resolve(
`Media attachment's available only in WAHA Plus version. ${DOCS_URL}`,
);
}
}
export class CoreMediaManager implements MediaManager {
constructor(
private storage: MediaStorage,
private mimetypes: string[],
) {}
async processMedia<Message>(
processor: IEngineMediaProcessor<Message>,
message: Message,
): Promise<Message> {
if (!processor.hasMedia(message)) {
return message;
}
const mimetype = '';
const filename = processor.getFilename(message);
const url = await this.storage.save('', mimetype, Buffer.from(''));
const media: WAMedia = {
mimetype: mimetype,
filename: filename,
url: url,
};
// @ts-ignore
message.media = media;
return message;
}
}
+17
View File
@@ -0,0 +1,17 @@
/**
* Engine specific media processor
* Knows how to extract necessary attributes and fetch the data from Message
*/
interface IMediaEngineProcessor<Message> {
hasMedia(message: Message): boolean;
getFilename(message: Message): string | null;
getMimetype(message: Message): string;
getMessageId(message: Message): string;
getMediaBuffer(message: Message): Promise<Buffer | null>;
}
export { IMediaEngineProcessor };
+15
View File
@@ -0,0 +1,15 @@
import { IMediaEngineProcessor } from '@waha/core/media/IMediaEngineProcessor';
/**
* General interface for MediaManager - one that handles the logic
* and manipulates MediaStorage and MediaEngineProcessor
*/
interface IMediaManager {
processMedia<Message>(
processor: IMediaEngineProcessor<Message>,
message: Message,
session: string,
): Promise<Message>;
}
export { IMediaManager };
+42
View File
@@ -0,0 +1,42 @@
import { S3MediaData } from '@waha/structures/media.s3.dto';
/**
* Handles saving data to the physical storage
*/
interface Message {
// false_111111111@c.us_AAAAAAAAAAAAAA
id: string;
}
interface File {
extension: string;
filename?: string;
}
export interface MediaData {
session: string;
message: Message;
file: File;
}
/**
* For stored media data
*/
export interface MediaStorageData {
url: string;
s3?: S3MediaData;
}
abstract class IMediaStorage {
abstract init(): Promise<void>;
abstract save(buffer: Buffer, data: MediaData): Promise<boolean>;
abstract exists(data: MediaData): Promise<boolean>;
abstract getStorageData(data: MediaData): Promise<MediaStorageData>;
abstract purge(): Promise<void>;
}
export { IMediaStorage };
+44
View File
@@ -0,0 +1,44 @@
import { IMediaEngineProcessor } from '@waha/core/media/IMediaEngineProcessor';
import { IMediaStorage, MediaData } from '@waha/core/media/IMediaStorage';
import { WAMedia } from '@waha/structures/media.dto';
import { IMediaManager } from './IMediaManager';
export class MediaManagerCore implements IMediaManager {
constructor(
private storage: IMediaStorage,
private mimetypes: string[],
) {}
async processMedia<Message>(
processor: IMediaEngineProcessor<Message>,
message: Message,
session: string,
): Promise<Message> {
if (!processor.hasMedia(message)) {
return message;
}
const mimetype = '';
const mediaData: MediaData = {
session: session,
message: {
id: '',
},
file: {
extension: '',
},
};
const { url } = await this.storage.getStorageData(mediaData);
const filename = processor.getFilename(message);
const media: WAMedia = {
mimetype: mimetype,
filename: filename,
url: url,
};
// @ts-ignore
message.media = media;
return message;
}
}
+25
View File
@@ -0,0 +1,25 @@
import { DOCS_URL } from '@waha/core/exceptions';
import { IMediaStorage, MediaData } from '@waha/core/media/IMediaStorage';
export class MediaNoopStorage implements IMediaStorage {
async init() {
return;
}
async save(buffer: Buffer, data: MediaData): Promise<boolean> {
return Promise.resolve(true);
}
async exists(data: MediaData): Promise<boolean> {
return false;
}
async getStorageData(data: MediaData) {
const url = `Media attachment's available only in WAHA Plus version. ${DOCS_URL}`;
return { url };
}
async purge() {
return;
}
}
@@ -0,0 +1,21 @@
import { Injectable } from '@nestjs/common';
import { WhatsappConfigService } from '@waha/config.service';
@Injectable()
export class MediaLocalStorageConfig {
public filesUri = '/api/files';
constructor(private config: WhatsappConfigService) {}
get filesURL(): string {
return `${this.config.schema}://${this.config.hostname}:${this.config.port}${this.filesUri}/`;
}
get filesFolder(): string {
return this.config.get('WHATSAPP_FILES_FOLDER', '/tmp/whatsapp-files');
}
get filesLifetime(): number {
return parseInt(this.config.get('WHATSAPP_FILES_LIFETIME', '180'));
}
}
@@ -2,6 +2,4 @@ export abstract class ISessionAuthRepository {
abstract init(sessionName?: string): Promise<void>;
abstract clean(sessionName: string): Promise<void>;
abstract getAll(): Promise<string[]>;
}
@@ -8,5 +8,9 @@ export abstract class ISessionConfigRepository {
abstract get(sessionName: string): Promise<SessionConfig | null>;
abstract exists(sessionName: string): Promise<boolean>;
abstract delete(sessionName: string): Promise<void>;
abstract getAll(): Promise<string[]>;
}
+16 -13
View File
@@ -1,7 +1,10 @@
import * as fs from 'fs/promises';
// eslint-disable-next-line @typescript-eslint/no-var-requires
const fs = require('fs-extra');
import { ISessionAuthRepository } from './ISessionAuthRepository';
import { LocalStore } from './LocalStore';
// Keep all waha related files, ".waha.session.*"
const KEEP_FILES = /^\.waha\.session\..*$/;
export class LocalSessionAuthRepository extends ISessionAuthRepository {
private store: LocalStore;
@@ -16,17 +19,17 @@ export class LocalSessionAuthRepository extends ISessionAuthRepository {
}
async clean(sessionName: string) {
const folder = this.store.getSessionDirectory(sessionName);
await fs.rm(folder, { recursive: true });
}
async getAll(): Promise<string[]> {
await this.init();
const content = await fs.readdir(this.store.getEngineDirectory(), {
withFileTypes: true,
});
return content
.filter((dirent) => dirent.isDirectory())
.map((dirent) => dirent.name);
// Remove all files and directories recursively, but keep waha files
const sessionDirectory = this.store.getSessionDirectory(sessionName);
// Check it exists and it's directory
const exists = await fs.pathExists(sessionDirectory);
if (!exists) {
return;
}
const files = await fs.readdir(sessionDirectory);
const filesToRemove = files.filter((file) => !file.match(KEEP_FILES));
for (const file of filesToRemove) {
await fs.remove(`${sessionDirectory}/${file}`);
}
}
}
@@ -1,5 +1,7 @@
import * as fs from 'fs/promises';
import * as path from 'path';
// eslint-disable-next-line @typescript-eslint/no-var-requires
const fs = require('fs-extra');
import { fileExists } from '@waha/utils/files';
import { SessionConfig } from '../../structures/sessions.dto';
import { ISessionConfigRepository } from './ISessionConfigRepository';
@@ -14,19 +16,15 @@ export class LocalSessionConfigRepository extends ISessionConfigRepository {
this.store = store;
}
private async fileExists(filepath: string) {
try {
await fs.access(filepath, fs.constants.F_OK);
} catch (error) {
return false;
}
return true;
async exists(sessionName: string): Promise<boolean> {
const filepath = this.getFilePath(sessionName);
return await fileExists(filepath);
}
async get(sessionName: string): Promise<SessionConfig | null> {
const filepath = this.getFilePath(sessionName);
// Check file exists
if (!(await this.fileExists(filepath))) {
if (!(await fileExists(filepath))) {
return null;
}
@@ -52,15 +50,21 @@ export class LocalSessionConfigRepository extends ISessionConfigRepository {
}
private getFilePath(sessionName): string {
const folder = this.store.getSessionDirectory(sessionName);
return path.join(folder, this.FILENAME);
return this.store.getFilePath(sessionName, this.FILENAME);
}
async delete(sessionName: string): Promise<void> {
const filepath = this.getFilePath(sessionName);
if (!(await this.fileExists(filepath))) {
return;
}
await fs.unlink(filepath);
const sessionDirectory = this.store.getSessionDirectory(sessionName);
await fs.remove(sessionDirectory);
}
async getAll(): Promise<string[]> {
await this.store.init();
const content = await fs.readdir(this.store.getEngineDirectory(), {
withFileTypes: true,
});
return content
.filter((dirent) => dirent.isDirectory())
.map((dirent) => dirent.name);
}
}
+5
View File
@@ -17,4 +17,9 @@ export abstract class LocalStore extends DataStore {
* Get the directory where the session data is stored
*/
abstract getSessionDirectory(name: string): string;
/**
* Get the file path for a session
*/
abstract getFilePath(session: string, file: string): string;
}
+4
View File
@@ -45,6 +45,10 @@ export class LocalStoreCore extends LocalStore {
return this.getDirectoryPath(name);
}
getFilePath(session: string, file: string): string {
return path.join(this.getSessionDirectory(session), file);
}
protected getDirectoryPath(name: string): string {
const suffix = crypto.createHash('md5').update(name).digest('hex');
return path.join(this.getEngineDirectory(), `${name}-${suffix}`);
+21 -16
View File
@@ -1,5 +1,3 @@
import { ConsoleLogger } from '@nestjs/common';
import { WAHAEvents } from '../structures/enums.dto';
import { WebhookConfig } from '../structures/webhooks.config.dto';
import { WAHAWebhook } from '../structures/webhooks.dto';
@@ -7,30 +5,36 @@ import { VERSION } from '../version';
import { WAHAInternalEvent, WhatsappSession } from './abc/session.abc';
import { WebhookConductor, WebhookSender } from './abc/webhooks.abc';
import request = require('request');
import { LoggerBuilder } from '@waha/utils/logging';
import { Logger } from 'pino';
export class WebhookSenderCore extends WebhookSender {
send(json) {
this.log.log(`Sending POST to ${this.url}...`);
this.log.verbose(`POST DATA: ${JSON.stringify(json)}`);
this.logger.info({ url: this.url }, `Sending POST ...`);
this.logger.debug({ data: json }, `POST DATA`);
request.post(this.url, { json: json }, (error, res, body) => {
if (error) {
this.log.error(error);
this.logger.error(error);
return;
}
this.log.log(`POST request was sent with status code: ${res.statusCode}`);
this.log.debug(`Response: ${body}`);
this.logger.info(
`POST request was sent with status code: ${res.statusCode}`,
);
this.logger.debug({ body: body }, `Response`);
});
}
}
export class WebhookConductorCore implements WebhookConductor {
constructor(protected log: ConsoleLogger) {
this.log = log;
private logger: Logger;
constructor(protected loggerBuilder: LoggerBuilder) {
this.logger = loggerBuilder.child({ name: WebhookConductor.name });
}
protected buildSender(webhookConfig: WebhookConfig): WebhookSender {
return new WebhookSenderCore(this.log, webhookConfig);
return new WebhookSenderCore(this.loggerBuilder, webhookConfig);
}
private getSuitableEvents(events: WAHAEvents[] | string[]): WAHAEvents[] {
@@ -47,7 +51,7 @@ export class WebhookConductorCore implements WebhookConductor {
for (const event of events) {
// @ts-ignore
if (!allEvents.includes(event)) {
this.log.error(`Unknown event for webhook: '${event}'`);
this.logger.error(`Unknown event for webhook: '${event}'`);
continue;
}
rightEvents.push(event);
@@ -70,7 +74,7 @@ export class WebhookConductorCore implements WebhookConductor {
}
const url = webhook.url;
this.log.log(`Configuring webhooks for ${url}...`);
this.logger.info(`Configuring webhooks for ${url}...`);
const events = this.getSuitableEvents(webhook.events);
const sender = this.buildSender(webhook);
for (const event of events) {
@@ -93,14 +97,14 @@ export class WebhookConductorCore implements WebhookConductor {
url,
);
if (!found) {
this.log.error(
this.logger.error(
`Engine does not support webhook event: '${event}' for url '${url}'`,
);
}
});
}
}
this.log.log(`Webhooks were configured for ${url}.`);
this.logger.info(`Webhooks were configured for ${url}.`);
}
private configureSingleEvent(
@@ -117,7 +121,7 @@ export class WebhookConductorCore implements WebhookConductor {
if (!found) {
return false;
}
this.log.log(`Event '${event}' is enabled for url: ${url}`);
this.logger.info(`Event '${event}' is enabled for url: ${url}`);
return true;
}
@@ -127,10 +131,11 @@ export class WebhookConductorCore implements WebhookConductor {
data: any,
sender: WebhookSender,
) {
const me = await session.getSessionMeInfo().catch((err) => null);
const me = session.getSessionMeInfo();
const json: WAHAWebhook = {
event: event,
session: session.name,
metadata: session.sessionConfig?.metadata,
me: me,
payload: data,
engine: session.engine,
View File
Whitespace-only changes.
Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

-289
View File
@@ -1,289 +0,0 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Dashboard | WAHA</title>
<link rel="icon" href="./favicon.ico" />
<style>
:root {
--surface-card: #1f2937;
--surface-border: #374151;
--card-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25);
}
body,
html {
height: 100%;
padding: 0;
margin: 0;
}
* {
box-sizing: border-box;
}
.bg-image {
/* The image used */
background-image: url('./waha-dashboard.png');
/* Add the blur effect */
filter: blur(2px);
-webkit-filter: blur(2px);
/* Full height */
height: 100%;
/* Center and scale the image nicely */
background-position: center;
background-repeat: no-repeat;
background-size: cover;
}
body {
background-color: #111827;
color: rgba(255, 255, 255, 0.87);
}
svg {
width: 100px;
height: 100px;
}
.message {
font-size: 20px;
margin: 20px;
}
a {
color: rgb(158, 158, 255);
text-decoration-color: rgb(158, 158, 255);
}
.upgrade-link {
text-decoration: none;
font-size: 1.5rem;
}
.link-button {
background-color: #4ade80;
padding: 10px 20px;
border-radius: 5px;
color: #052e16;
text-decoration: none;
margin-top: 50px;
}
.card {
min-width: 25rem;
background: var(--surface-card);
border: 1px solid var(--surface-border);
box-shadow: var(--card-shadow);
padding: 2rem;
border-radius: 12px;
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
z-index: 2;
text-align: center;
}
</style>
</head>
<body>
<div class="bg-image"></div>
<div class="card">
<svg
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
version="1.1"
width="1080"
height="1080"
viewBox="0 0 1080 1080"
xml:space="preserve"
>
<desc>Created with Fabric.js 5.2.4</desc>
<defs></defs>
<g
transform="matrix(1 0 0 1 540 540)"
id="c3cb5d1b-8350-492a-8567-8c1ee2307b9a"
></g>
<g
transform="matrix(1 0 0 1 540 540)"
id="054ae9ae-db12-4435-8648-8bfb02267690"
>
<rect
style="
stroke: none;
stroke-width: 1;
stroke-dasharray: none;
stroke-linecap: butt;
stroke-dashoffset: 0;
stroke-linejoin: miter;
stroke-miterlimit: 4;
fill: rgb(255, 255, 255);
fill-rule: nonzero;
opacity: 1;
visibility: hidden;
"
vector-effect="non-scaling-stroke"
x="-540"
y="-540"
rx="0"
ry="0"
width="1080"
height="1080"
/>
</g>
<g transform="matrix(1 0 0 1 540 540)">
<rect
style="
stroke: none;
stroke-width: 1;
stroke-dasharray: none;
stroke-linecap: butt;
stroke-dashoffset: 0;
stroke-linejoin: miter;
stroke-miterlimit: 4;
fill: rgb(255, 255, 255);
fill-rule: nonzero;
opacity: 1;
visibility: hidden;
"
vector-effect="non-scaling-stroke"
x="-540"
y="-540"
rx="0"
ry="0"
width="1080"
height="1080"
/>
</g>
<g transform="matrix(2.02 0 0 2.02 540 540)">
<rect
style="
stroke: none;
stroke-width: 1;
stroke-dasharray: none;
stroke-linecap: butt;
stroke-dashoffset: 0;
stroke-linejoin: miter;
stroke-miterlimit: 4;
fill: rgb(255, 255, 255);
fill-rule: nonzero;
opacity: 1;
visibility: hidden;
"
vector-effect="non-scaling-stroke"
x="-540"
y="-540"
rx="0"
ry="0"
width="1080"
height="1080"
/>
</g>
<g transform="matrix(3.68 0 0 3.68 540.38 540.48)">
<path
style="
stroke: none;
stroke-width: 1;
stroke-dasharray: none;
stroke-linecap: butt;
stroke-dashoffset: 0;
stroke-linejoin: miter;
stroke-miterlimit: 4;
fill: rgb(74, 173, 32);
fill-rule: nonzero;
opacity: 1;
"
vector-effect="non-scaling-stroke"
transform=" translate(-392.21, -570.16)"
d="M 392.26 705.96 C 376.77 705.96 361.28999999999996 706.0400000000001 345.8 705.95 C 318.16 705.7900000000001 297.6 685.2700000000001 297.22 657.57 C 297.15000000000003 652.24 297.07000000000005 646.9100000000001 297.25 641.59 C 297.36 638.23 296.3 636.6 292.72 636.89 C 290.24 637.09 287.73 636.98 285.23 636.9 C 277.1 636.63 271.70000000000005 631.65 271.38 623.49 C 271.06 615.1800000000001 271.06 606.83 271.37 598.52 C 271.69 590.05 277.04 585.23 285.58 585.01 C 297.21 584.72 297.19 584.72 297.21999999999997 573.01 C 297.22999999999996 567.36 297.14 561.72 297.97999999999996 556.0699999999999 C 301.25999999999993 533.93 320.72999999999996 516.4599999999999 343.09 516.04 C 355.07 515.81 367.07 515.86 379.04999999999995 516.02 C 382.72999999999996 516.0699999999999 384.02 514.87 383.96 511.15 C 383.77 500.65999999999997 383.76 490.16999999999996 383.96999999999997 479.67999999999995 C 384.04999999999995 476.00999999999993 382.96 473.65999999999997 379.92999999999995 471.37999999999994 C 372.23999999999995 465.5799999999999 369.78 456.25999999999993 373.06999999999994 447.7199999999999 C 376.18999999999994 439.5999999999999 385.13999999999993 433.7399999999999 393.4599999999999 434.3599999999999 C 402.63999999999993 435.0399999999999 410.43999999999994 441.3499999999999 412.4599999999999 450.2199999999999 C 414.4799999999999 459.0299999999999 411.7099999999999 466.4299999999999 404.4699999999999 471.7899999999999 C 401.8699999999999 473.7199999999999 400.9199999999999 475.69999999999993 400.9699999999999 478.8299999999999 C 401.1199999999999 489.1499999999999 401.2499999999999 499.4799999999999 400.9299999999999 509.79999999999995 C 400.7899999999999 514.5 402.0299999999999 516.24 406.9999999999999 516.0799999999999 C 417.8099999999999 515.7299999999999 428.6399999999999 515.91 439.4699999999999 515.9899999999999 C 466.5199999999999 516.2099999999999 486.7199999999999 536.2299999999999 487.2399999999999 563.3 C 487.3399999999999 568.29 487.2599999999999 573.29 487.2799999999999 578.29 C 487.2999999999999 585 487.3099999999999 585 493.7899999999999 585.02 C 495.94999999999993 585.03 498.1199999999999 584.96 500.2799999999999 585.06 C 507.94999999999993 585.43 512.9999999999999 590.42 513.1499999999999 598.1199999999999 C 513.3199999999998 606.6099999999999 513.3099999999998 615.1099999999999 513.1599999999999 623.5999999999999 C 513.0299999999999 631.3599999999999 508.1999999999999 636.31 500.47999999999985 636.8699999999999 C 498.15999999999985 637.0399999999998 495.77999999999986 637.2199999999999 493.49999999999983 636.8899999999999 C 488.1999999999998 636.1299999999999 486.86999999999983 638.5399999999998 487.1999999999998 643.3999999999999 C 487.54999999999984 648.5399999999998 487.3499999999998 653.7199999999999 487.23999999999984 658.8799999999999 C 486.67999999999984 685.6599999999999 466.4599999999998 705.7399999999999 439.71999999999986 705.9599999999999 C 423.9 706.06 408.08 705.95 392.26 705.96 z"
stroke-linecap="round"
/>
</g>
<g transform="matrix(-3.68 0 0 -3.68 546.38 845.01)">
<path
style="
stroke: none;
stroke-width: 1;
stroke-dasharray: none;
stroke-linecap: butt;
stroke-dashoffset: 0;
stroke-linejoin: miter;
stroke-miterlimit: 4;
fill: rgb(56, 134, 22);
fill-rule: nonzero;
opacity: 1;
"
vector-effect="non-scaling-stroke"
transform=" translate(-394.07, -654.42)"
d="M 394.26 632.93 C 404.75 632.93 415.23 632.9 425.71999999999997 632.9399999999999 C 431.92999999999995 632.9599999999999 433.52 634.7399999999999 432.90999999999997 640.7099999999999 C 430.99999999999994 659.66 414.40999999999997 675.2199999999999 395.38 675.91 C 376.87 676.5799999999999 359.57 662.65 356 644.1899999999999 C 355.72 642.7199999999999 355.33 641.26 355.19 639.78 C 354.71 634.9 356.32 633.01 361.3 632.97 C 372.29 632.87 383.28 632.93 394.26 632.93 z"
stroke-linecap="round"
/>
</g>
<g transform="matrix(3.68 0 0 3.68 673.99 594.08)">
<path
style="
stroke: none;
stroke-width: 1;
stroke-dasharray: none;
stroke-linecap: butt;
stroke-dashoffset: 0;
stroke-linejoin: miter;
stroke-miterlimit: 4;
fill: rgb(56, 135, 22);
fill-rule: nonzero;
opacity: 1;
"
vector-effect="non-scaling-stroke"
transform=" translate(-428.75, -584.99)"
d="M 428.83 602 C 419.12 601.96 411.59999999999997 594.6 411.53 585.07 C 411.46999999999997 575.4100000000001 419.21 567.9100000000001 429.16999999999996 567.98 C 438.33 568.0500000000001 445.93999999999994 575.71 445.97999999999996 584.91 C 446.02 594.33 438.29 602.04 428.83 602 z"
stroke-linecap="round"
/>
</g>
<g transform="matrix(3.68 0 0 3.68 406.92 594.18)">
<path
style="
stroke: none;
stroke-width: 1;
stroke-dasharray: none;
stroke-linecap: butt;
stroke-dashoffset: 0;
stroke-linejoin: miter;
stroke-miterlimit: 4;
fill: rgb(56, 135, 22);
fill-rule: nonzero;
opacity: 1;
"
vector-effect="non-scaling-stroke"
transform=" translate(-356.06, -584.99)"
d="M 339.05 584.57 C 339.19 574.94 346.67 567.84 356.53000000000003 567.99 C 366.26000000000005 568.14 373.21000000000004 575.46 373.07000000000005 585.42 C 372.94000000000005 595.16 365.62000000000006 602.13 355.68000000000006 602 C 346.04 601.87 338.91 594.4 339.05 584.57 z"
stroke-linecap="round"
/>
</g>
</svg>
<div class="message">
Sorry,
<a
href="https://waha.devlike.pro/docs/how-to/waha-dashboard"
target="_blank"
>WAHA Dashboard</a
>
is not available in <strong>WAHA Core</strong> version.
<br />
Please consider upgrading to <strong>WAHA Plus</strong>.
</div>
<div style="margin-top: 2rem; margin-bottom: 1rem">
<a
class="upgrade-link link-button"
href="https://waha.devlike.pro/docs/how-to/plus-version/"
target="_blank"
>Upgrade to WAHA Plus</a
>
</div>
</div>
</body>
</html>
Binary file not shown.

Before

Width:  |  Height:  |  Size: 74 KiB

+1 -15
View File
@@ -1,5 +1,3 @@
import { LogLevel } from '@nestjs/common';
function parseBool(value: any): boolean {
if (typeof value === 'boolean') {
return value;
@@ -15,11 +13,6 @@ function parseBool(value: any): boolean {
return false;
}
console.error(
'parseBool got an unexpected value:',
value,
'(accepted values : "true", "false")',
);
throw new Error(
'Error: parseBool got unexpected value - use "true" or "false" values',
);
@@ -37,11 +30,4 @@ function splitAt(str: string, index) {
return [fst.join(''), snd.join('')];
}
function getLogLevels(debug: boolean): LogLevel[] {
const enableDebug = process.env.DEBUG != undefined || debug;
return enableDebug
? ['log', 'error', 'warn', 'debug', 'verbose']
: ['log', 'error', 'warn'];
}
export { flipObject, getLogLevels, parseBool, splitAt };
export { flipObject, parseBool, splitAt };
+48 -11
View File
@@ -1,26 +1,39 @@
import { NestFactory } from '@nestjs/core';
import { WsAdapter } from '@nestjs/platform-ws';
import {
getNestJSLogLevels,
getPinoLogLevel,
getPinoTransport,
} from '@waha/utils/logging';
import { json, urlencoded } from 'express';
import { Logger as NestJSPinoLogger } from 'nestjs-pino';
import { LoggerErrorInterceptor } from 'nestjs-pino';
import { Logger } from 'pino';
import pino from 'pino';
import { AllExceptionsFilter } from './api/exception.filter';
import { WhatsappConfigService } from './config.service';
import { AppModuleCore } from './core/app.module.core';
import { SwaggerConfiguratorCore } from './core/SwaggerConfiguratorCore';
import { getLogLevels } from './helpers';
import { AllExceptionsFilter } from './nestjs/AllExceptionsFilter';
import { WAHA_WEBHOOKS } from './structures/webhooks.dto';
import { getWAHAVersion, VERSION, WAHAVersion } from './version';
console.log('NODE - Catching unhandled exceptions enabled');
const logger: Logger = pino({
level: getPinoLogLevel(),
transport: getPinoTransport(),
}).child({ name: 'Bootstrap' });
logger.info('NODE - Catching unhandled rejection enabled');
process.on('unhandledRejection', (reason, promise) => {
console.error('Unhandled Rejection at:', promise, 'reason:', reason);
logger.error('Unhandled Rejection at:', promise, 'reason:', reason);
// @ts-ignore
console.error(reason.stack);
logger.error(reason.stack);
});
async function loadModules(): Promise<
[typeof AppModuleCore, typeof SwaggerConfiguratorCore]
> {
const version = getWAHAVersion();
console.log(`WAHA (WhatsApp HTTP API) - Running ${version} version...`);
if (version === WAHAVersion.CORE) {
const { AppModuleCore } = await import('./core/app.module.core');
@@ -38,28 +51,52 @@ async function loadModules(): Promise<
return [AppModulePlus, SwaggerConfiguratorPlus];
}
let app = undefined;
export function getApp() {
return app;
}
async function bootstrap() {
const version = getWAHAVersion();
logger.info(`WAHA (WhatsApp HTTP API) - Running ${version} version...`);
const [AppModule, SwaggerModule] = await loadModules();
const app = await NestFactory.create(AppModule, {
logger: getLogLevels(false),
const httpsOptions = AppModule.getHttpsOptions(logger);
app = await NestFactory.create(AppModule, {
logger: getNestJSLogLevels(),
httpsOptions: httpsOptions,
bufferLogs: true,
});
app.useLogger(app.get(NestJSPinoLogger));
// Print 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());
app.enableShutdownHooks();
app.useGlobalFilters(new AllExceptionsFilter());
app.enableCors();
// Ideally, we should apply it globally.
// 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
app.use(json({ limit: '50mb' }));
app.use(urlencoded({ limit: '50mb', extended: false }));
app.useWebSocketAdapter(new WsAdapter(app));
// Configure swagger
const swaggerConfigurator = new SwaggerModule(app);
swaggerConfigurator.configure(WAHA_WEBHOOKS);
AppModule.appReady(app, logger);
const config = app.get(WhatsappConfigService);
await app.listen(config.port);
console.log(`WhatsApp HTTP API is running on: ${await app.getUrl()}`);
console.log(`Environment:`, VERSION);
logger.info(`WhatsApp HTTP API is running on: ${await app.getUrl()}`);
logger.info(VERSION, 'Environment');
}
bootstrap();
bootstrap().catch((error) => {
logger.error(error, `Failed to start WAHA: ${error}`);
process.exit(1);
});
@@ -31,7 +31,7 @@ export class AllExceptionsFilter implements ExceptionFilter {
/**
* If file not found - we get weird 500 error
* So we convert that exception manually to 404 and send appropriate JSON response
* @issue https://github.com/devlikeapro/whatsapp-http-api/issues/134
* @issue https://github.com/devlikeapro/waha/issues/134
* @solution https://github.com/nestjs/serve-static/issues/139#issuecomment-612429557
*/
if (exception.code === 'ENOENT') {
+37
View File
@@ -0,0 +1,37 @@
import { applyDecorators } from '@nestjs/common';
import { ApiExtraModels, ApiResponse, getSchemaPath } from '@nestjs/swagger';
import { Base64File } from '@waha/structures/files.dto';
function getRefSchemaPaths(models) {
return models.map((model) => {
return { $ref: getSchemaPath(model) };
});
}
/**
* Decorator to add a file accept header to the swagger documentation
*/
export function ApiFileAcceptHeader(...models) {
models = models.length ? models : [Base64File];
return applyDecorators(
// Add extra models, otherwise it'll give a error
// $ref not found
ApiExtraModels(...models),
ApiResponse({
status: 200,
content: {
'image/png': {
schema: {
type: 'string',
format: 'binary',
},
},
'application/json': {
schema: {
oneOf: getRefSchemaPaths(models),
},
},
},
}),
);
}
@@ -0,0 +1,34 @@
import { BufferJSON } from '@adiwajshing/baileys/lib/Utils';
import {
CallHandler,
ExecutionContext,
Injectable,
NestInterceptor,
StreamableFile,
} from '@nestjs/common';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';
@Injectable()
export class BufferJsonReplacerInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
return next.handle().pipe(
map((data) => {
if (typeof data !== 'object' || data === null) {
return data;
}
// Buffer
if (Buffer.isBuffer(data) || data?.data || data?.url) {
return data;
}
// StreamableFile
if (data instanceof StreamableFile) {
return data;
}
return JSON.parse(JSON.stringify(data, BufferJSON.replacer));
}),
);
}
}
File renamed without changes.
+17
View File
@@ -0,0 +1,17 @@
import { ApiParam } from '@nestjs/swagger';
export const ChatIdApiParam = ApiParam({
name: 'chatId',
required: true,
type: 'string',
description: 'Chat ID',
example: '123456789@c.us',
});
export const GroupIdApiParam = ApiParam({
name: 'id',
required: true,
type: 'string',
description: 'Group ID',
example: '123123123@g.us',
});
Loaded 100 of 141 files, more files were not shown because too many files have changed in this diff. Show more