Compare commits

...
149 Commits
Author SHA1 Message Date
devlikepro d85e2498fd [core] Up 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-09-11 13:14:22 +07:00
devlikepro 507382b6d2 [core] 2024.9.5 2024-09-11 09:13:28 +07:00
devlikepro f343cf8549 [core] Show better error for screenshot
fix #538
2024-09-11 09:12:54 +07:00
devlikepro 832d5baf35 [core] WEBJS optimizations 2024-09-11 09:02:36 +07:00
devlikepro 0253f32574 [core] node 22.8 2024-09-11 09:02:36 +07:00
devlikepro 6173180272 [core] remove VENOM 2024-09-11 09:02:31 +07:00
devlikepro 2d21931fb1 [core] log stack 2024-09-11 09:02:30 +07:00
devlikepro bdaa7187fd [core] accept phone as chatId in typing and presense 2024-09-10 16:12:03 +07:00
devlikepro e7ad6e06ec [core] use node dist/main 2024-09-10 10:33:43 +07:00
devlikepro 7c14483852 [core] YARN_CHECKSUM_BEHAVIOR=update 2024-09-10 10:10:58 +07:00
devlikepro 8b4b1f934e [core] Add corepack 2024-09-10 09:49:03 +07:00
devlikepro 713eb3f169 [core] Up nodejs 20.17.0 2024-09-09 16:33:34 +07:00
devlikepro 48297c80d1 [core] remove listeners 2024-09-09 15:58:52 +07:00
devlikepro 315ffdac92 [core] set timeout null at finally 2024-09-09 15:47:56 +07:00
devlikepro 7ddc17e6e5 [core] Add /api/server/debug/heapsnapshot 2024-09-09 15:33:24 +07:00
devlikepro e3b8073c82 [core] force null timeout and interval 2024-09-09 14:23:07 +07:00
devlikepro 80517e416c [core] WAHA_LOG_FORMAT=JSON in examples 2024-09-09 10:58:04 +07:00
devlikepro 4b71aade9d [core] gitignore heapsnapshot 2024-09-09 10:44:42 +07:00
devlikepro 1b52698c54 [core] remove session run time 2024-09-09 10:43:52 +07:00
devlikepro b8f372d83f [core] n8n docker-compose 2024-09-08 12:24:01 +07:00
devlikepro 7bc5f55fb2 [core] n8n docker-compose 2024-09-08 11:11:46 +07:00
devlikepro 7eeafd03b3 [core] fix mongodb
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-06 09:32:18 +07:00
devlikepro f88480d271 [core] 2024.9.4 2024-09-06 09:24:10 +07:00
devlikepro 9b8cce85bd [core] Fix operations for empty config sessions - local files 2024-09-06 09:23:45 +07:00
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
95 changed files with 3501 additions and 3046 deletions

No files matched your search

+4
View File
@@ -1,7 +1,10 @@
*.heapsnapshot
.yarn
example.ts
.*sessions
.secrets
.minio
.media
tokens
files
venv
@@ -11,6 +14,7 @@ src/test.js
test.js
.wwebjs_cache
src/dashboard
.eslintcache
# compiled output
/dist
+1 -1
View File
@@ -1 +1 @@
v20.5.0
v22.8
+15 -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'
@@ -50,11 +62,13 @@ repos:
- 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())
+6 -3
View File
@@ -1,7 +1,7 @@
#
# Build
#
ARG NODE_VERSION=20.12.2-bullseye
ARG NODE_VERSION=22.8-bullseye
FROM node:${NODE_VERSION} as build
ENV PUPPETEER_SKIP_DOWNLOAD=True
@@ -9,6 +9,8 @@ ENV PUPPETEER_SKIP_DOWNLOAD=True
WORKDIR /src
COPY package.json .
COPY yarn.lock .
ENV YARN_CHECKSUM_BEHAVIOR=update
RUN npm install -g corepack && corepack enable
RUN yarn set version 3.6.3
RUN yarn install
@@ -24,7 +26,7 @@ RUN yarn build && find ./dist -name "*.d.ts" -delete
FROM node:${NODE_VERSION} as dashboard
# Download WAHA Dashboard
ENV WAHA_DASHBOARD_SHA 63fd6e9e23e90f90e8aa0de04feb3f1909f63021
ENV WAHA_DASHBOARD_SHA 9afbd7b68618ea67be01bdefa978cf98a6336611
RUN \
wget https://github.com/devlikeapro/dashboard/archive/${WAHA_DASHBOARD_SHA}.zip \
&& unzip ${WAHA_DASHBOARD_SHA}.zip -d /tmp/dashboard \
@@ -86,6 +88,7 @@ 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
@@ -96,4 +99,4 @@ ENV WAHA_ZIPPER=ZIPUNZIP
# Run command, etc
EXPOSE 3000
CMD yarn start:prod
CMD ["/entrypoint.sh"]
+1 -1
View File
@@ -32,7 +32,7 @@ for-swagger:
WHATSAPP_SWAGGER_CONFIG_ADVANCED=true . ${NVM_DIR}/nvm.sh && nvm exec yarn start
up-noweb:
. ${NVM_DIR}/nvm.sh && nvm exec yarn up @adiwajshing/baileys@github:devlikeapro/Baileys#fork-master-channels
. ${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
+1 -1
View File
@@ -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:
+35
View File
@@ -0,0 +1,35 @@
services:
waha:
restart: always
container_name: waha
# image: devlikeapro/waha:latest
image: devlikeapro/waha-plus:latest
ports:
- '3000:3000/tcp'
volumes:
- './.sessions:/app/.sessions'
- './.media:/app/.media'
environment:
- WHATSAPP_API_KEY=321
- WAHA_DASHBOARD_USERNAME=waha
- WAHA_DASHBOARD_PASSWORD=waha
- WHATSAPP_DEFAULT_ENGINE=WEBJS
- WHATSAPP_RESTART_ALL_SESSIONS=True
- WAHA_PRINT_QR=False
- WAHA_MEDIA_STORAGE=LOCAL
- WHATSAPP_FILES_LIFETIME=0
- WHATSAPP_FILES_FOLDER=/app/.media
n8n:
image: docker.n8n.io/n8nio/n8n
container_name: n8n
restart: always
environment:
- WEBHOOK_URL=http://n8n:5678
ports:
- '5678:5678'
volumes:
- n8n_data:/home/node/.n8n
volumes:
n8n_data: {}
+56 -16
View File
@@ -57,6 +57,7 @@ services:
# 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
@@ -84,10 +85,10 @@ services:
# 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_FORMAT=JSON
- WAHA_LOG_LEVEL=info
# - WAHA_LOG_FORMAT=PRETTY
# If you want to see more logs, you can set the log level to debug
#- WAHA_LOG_LEVEL=debug
# DEBUG=1 # shortcut for setting log level to debug
@@ -147,11 +148,13 @@ services:
# 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
#- WHATSAPP_API_HOSTNAME=waha.example.pro
#- 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
#
#
@@ -165,15 +168,30 @@ services:
# 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
#
@@ -188,16 +206,38 @@ services:
# https://waha.devlike.pro/docs/how-to/storages/#sessions---mongodb
#- WHATSAPP_SESSIONS_MONGO_URL=mongodb://mongodb:27017
#
# NOTE: Uncomment this block to run WAHA+ with MongoDB
# mongodb:
# image: mongo
# container_name: mongodb
# ports:
# - '27017:27017/tcp'
# volumes:
# - mongodb_data:/data/db
# restart: always
# NOTE: Uncomment this block to run WAHA+ with MongoDB
#volumes:
# mongodb_data: {}
# 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
ports:
- '27017:27017/tcp'
volumes:
- 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
node dist/main
+2 -2
View File
@@ -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)
+9 -4
View File
@@ -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,11 +23,13 @@
"test:e2e": "jest --config ./test/jest-e2e.json"
},
"dependencies": {
"@adiwajshing/baileys": "github:devlikeapro/Baileys#fork-master-channels",
"@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",
@@ -40,7 +43,7 @@
"@types/ws": "^8.5.4",
"adm-zip": "0.5.10",
"async-lock": "^1.4.1",
"better-sqlite3": "9.6.0",
"better-sqlite3": "11.3.0",
"check-disk-space": "^3.4.0",
"chokidar": "^3.6.0",
"class-transformer": "^0.5.1",
@@ -48,7 +51,9 @@
"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",
@@ -61,6 +66,7 @@
"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",
@@ -71,7 +77,6 @@
"rxjs": "^7.1.0",
"sharp": "^0.33.4",
"swagger-ui-express": "^4.1.4",
"venom-bot": "5.0.1",
"whatsapp-web.js": "github:devlikeapro/whatsapp-web.js#fork-webpack-exodus-channels"
},
"devDependencies": {
+1 -1
View File
@@ -11,7 +11,7 @@ docker run -d \
--env WHATSAPP_API_KEY=321 \
--env WAHA_DASHBOARD_USERNAME=waha \
--env WAHA_DASHBOARD_PASSWORD=waha \
--env WAHA_LOG_FORMAT=PRETTY \
--env WAHA_LOG_FORMAT=JSON \
--env WAHA_LOG_LEVEL=info \
--env WHATSAPP_DEFAULT_ENGINE=WEBJS \
--env WHATSAPP_RESTART_ALL_SESSIONS=True \
+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)
+9 -3
View File
@@ -8,6 +8,10 @@ import {
Query,
} from '@nestjs/common';
import { ApiOperation, ApiSecurity, ApiTags } from '@nestjs/swagger';
import {
SessionApiParam,
SessionParam,
} from '@waha/nestjs/params/SessionApiParam';
import {
Channel,
CreateChannelRequest,
@@ -18,11 +22,10 @@ import {
import { SessionManager } from '../core/abc/manager.abc';
import { isNewsletter, WhatsappSession } from '../core/abc/session.abc';
import { SessionApiParam, SessionParam } from './helpers';
@ApiSecurity('api_key')
@Controller('api/:session/channels')
@ApiTags('channels')
@ApiTags('📢 Channels')
export class ChannelsController {
constructor(private manager: SessionManager) {}
@@ -58,7 +61,10 @@ export class ChannelsController {
@SessionApiParam
@NewsletterIdOrInviteCodeApiParam
@ApiOperation({
summary: 'Get the channel info - either by id @newsletter OR invite code.',
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,
+10 -7
View File
@@ -11,22 +11,22 @@ import {
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, 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) {}
@@ -44,6 +44,7 @@ class ChatsController {
@Delete(':chatId')
@SessionApiParam
@ApiOperation({ summary: 'Deletes the chat' })
@ChatIdApiParam
deleteChat(
@SessionParam session: WhatsappSession,
@Param('chatId') chatId: string,
@@ -54,6 +55,7 @@ class ChatsController {
@Get(':chatId/messages')
@SessionApiParam
@ApiOperation({ summary: 'Gets messages in the chat' })
@ChatIdApiParam
getChatMessages(
@Query() query: GetChatMessagesQuery,
@SessionParam session: WhatsappSession,
@@ -66,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,
+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', 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('/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,
+6 -3
View File
@@ -1,18 +1,21 @@
import { Controller, Get } from '@nestjs/common';
import { ApiOperation, 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, performing all health checks.',
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),
},
},
},
}),
);
}
+9 -3
View File
@@ -1,14 +1,18 @@
import { Body, Controller, Get, Param, Put } from '@nestjs/common';
import { ApiOperation, ApiSecurity, ApiTags } from '@nestjs/swagger';
import { SessionApiParam, SessionParam } from '@waha/api/helpers';
import { WhatsappSession } from '@waha/core/abc/session.abc';
import { Label, LabelID, SetLabelsRequest } from '@waha/structures/labels.dto';
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')
@ApiTags('🏷️ Labels')
export class LabelsController {
constructor(private manager: SessionManager) {}
@@ -21,6 +25,7 @@ export class LabelsController {
@Get('/chats/:chatId')
@SessionApiParam
@ChatIdApiParam
@ApiOperation({ summary: 'Get labels for the chat' })
getChatLabels(
@SessionParam session: WhatsappSession,
@@ -31,6 +36,7 @@ export class LabelsController {
@Put('/chats/:chatId')
@SessionApiParam
@ChatIdApiParam
@ApiOperation({ summary: 'Save labels for the chat' })
putChatLabels(
@SessionParam session: WhatsappSession,
+7 -3
View File
@@ -1,12 +1,16 @@
import { Controller, Get } from '@nestjs/common';
import { ApiOperation, ApiTags } from '@nestjs/swagger';
import { ApiOperation, ApiSecurity, ApiTags } from '@nestjs/swagger';
import { PingResponse } from '@waha/structures/ping.dto';
@ApiSecurity('api_key')
@Controller('ping')
@ApiTags('other')
@ApiTags('🔍 Observability')
export class PingController {
@Get()
@ApiOperation({ summary: 'Ping the server, perform no operations.' })
@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) {}
+144
View File
@@ -0,0 +1,144 @@
import * as process from 'node:process';
import * as v8 from 'node:v8';
import {
Body,
Controller,
Get,
Logger,
NotFoundException,
Post,
Query,
Res,
StreamableFile,
UseInterceptors,
UsePipes,
} from '@nestjs/common';
import { ApiOperation, ApiSecurity, ApiTags } from '@nestjs/swagger';
import { WhatsappConfigService } from '@waha/config.service';
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);
}
}
}
@ApiSecurity('api_key')
@Controller('api/server/debug')
@ApiTags('🔍 Observability')
export class ServerDebugController {
private logger: Logger;
private readonly enabled: boolean;
constructor(protected config: WhatsappConfigService) {
this.logger = new Logger('ServerDebugController');
this.enabled = this.config.debugModeEnabled;
}
@Get('heapsnapshot')
@ApiOperation({
summary: 'Return a heapsnapshot',
description: "Return a heapsnapshot of the server's memory",
})
async heapsnapshot() {
if (!this.enabled) {
throw new NotFoundException('WAHA_DEBUG_MODE is disabled');
}
this.logger.log('Creating a heap snapshot...');
const heap = v8.getHeapSnapshot();
const fileName = `${Date.now()}.heapsnapshot`;
return new StreamableFile(heap, {
type: 'application/octet-stream',
disposition: `attachment; filename=${fileName}`,
});
}
}
+253 -47
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('/')
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,229 @@ 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): 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;
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 };
+11 -16
View File
@@ -1,13 +1,9 @@
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';
@@ -18,17 +14,16 @@ import {
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,
@@ -38,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,
@@ -48,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,
@@ -58,7 +53,7 @@ class StatusController {
@Post('video')
@SessionApiParam
@ApiOperation({ summary: 'Send video status.' })
@ApiOperation({ summary: 'Send video status' })
sendVideoStatus(
@SessionParam session: WhatsappSession,
@Body() status: VideoStatus,
@@ -68,7 +63,7 @@ class StatusController {
@Post('delete')
@SessionApiParam
@ApiOperation({ summary: 'DELETE sent status.' })
@ApiOperation({ summary: 'DELETE sent status' })
deleteStatus(
@SessionParam session: WhatsappSession,
@Body() status: DeleteStatusRequest,
+6 -2
View File
@@ -11,10 +11,14 @@ import { VERSION } from '../version';
@ApiSecurity('api_key')
@Controller('api/version')
@ApiTags('other')
@ApiTags('🔍 Observability')
export class VersionController {
@Get('')
@ApiOperation({ summary: 'Get the version of the server' })
@ApiOperation({
summary: 'Get the server version ',
deprecated: true,
description: "Use 'GET /api/server/version' instead ",
})
get(): WAHAEnvironment {
return VERSION;
}
+14 -19
View File
@@ -1,18 +1,16 @@
import { Injectable } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { WAHAEvents } from '@waha/structures/enums.dto';
import { parseBool } from './helpers';
import { WebhookConfig } from './structures/webhooks.config.dto';
@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 {
@@ -23,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'];
@@ -64,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,
);
@@ -101,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(',') : [];
}
@@ -146,4 +136,9 @@ export class WhatsappConfigService {
3000,
);
}
get debugModeEnabled(): boolean {
const value = this.configService.get('WAHA_DEBUG_MODE', 'false');
return parseBool(value);
}
}
+13 -12
View File
@@ -56,29 +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('channels', 'Channels (newsletters) 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('👥 Groups', `Groups methods.<br>`)
.addTag('✅ Presence', `Presence information`)
.addTag(
'labels',
'🏷️ Labels',
'Labels - available only for WhatsApp Business accounts',
)
.addTag('other', 'Other methods')
.addTag('🔍 Observability', 'Other methods')
.addTag('🗄️ Storage', 'Storage methods')
.addApiKey({
type: 'apiKey',
description: 'Your secret api key',
+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>;
}
+17 -11
View File
@@ -1,19 +1,13 @@
import {
BeforeApplicationShutdown,
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, WAHAEvents } from '../../structures/enums.dto';
import {
MeInfo,
SessionConfig,
SessionDTO,
SessionInfo,
SessionLogoutRequest,
SessionStartRequest,
SessionStopRequest,
} from '../../structures/sessions.dto';
import { ISessionAuthRepository } from '../storage/ISessionAuthRepository';
import { ISessionConfigRepository } from '../storage/ISessionConfigRepository';
@@ -37,11 +31,22 @@ export abstract class SessionManager implements BeforeApplicationShutdown {
//
// 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;
@@ -55,6 +60,7 @@ export abstract class SessionManager implements BeforeApplicationShutdown {
const data: WAHAWebhook = {
event: event,
session: session.name,
metadata: session.sessionConfig?.metadata,
me: me,
payload: payload,
engine: session.engine,
-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 };
+36 -19
View File
@@ -56,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');
@@ -88,7 +88,7 @@ export enum WAHAInternalEvent {
export interface SessionParams {
name: string;
printQR: boolean;
mediaManager: MediaManager;
mediaManager: IMediaManager;
loggerBuilder: LoggerBuilder;
sessionStore: DataStore;
proxyConfig?: ProxyConfig;
@@ -101,7 +101,7 @@ export abstract class WhatsappSession {
public engine: WAHAEngine;
public name: string;
protected mediaManager: MediaManager;
protected mediaManager: IMediaManager;
public loggerBuilder: LoggerBuilder;
protected logger: Logger;
protected sessionStore: DataStore;
@@ -152,29 +152,46 @@ 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-site-isolation-trials', // https://superuser.com/questions/654565/how-to-run-google-chrome-in-a-single-process
'--disable-speech-api',
'--disable-sync',
'--disable-translate',
'--disable-web-security',
'--disk-cache-size=0',
'--hide-scrollbars',
'--ignore-certificate-errors',
'--ignore-ssl-errors',
'--in-process-gpu', // https://superuser.com/questions/654565/how-to-run-google-chrome-in-a-single-process
'--metrics-recording-only',
'--mute-audio',
'--no-default-browser-check',
'--no-first-run',
'--no-pings',
'--no-sandbox',
'--no-zygote',
'--password-store=basic',
'--renderer-process-limit=2', // https://superuser.com/questions/654565/how-to-run-google-chrome-in-a-single-process
'--safebrowsing-disable-auto-update',
'--single-process',
'--use-mock-keychain',
];
}
+8 -6
View File
@@ -1,6 +1,5 @@
import {
BeforeApplicationShutdown,
ConsoleLogger,
Logger,
LoggerService,
} from '@nestjs/common';
@@ -13,9 +12,10 @@ import {
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 { WebsocketHeartbeatJob } from '@waha/utils/WebsocketHeartbeatJob';
import { WebSocket } from '@waha/utils/ws';
import { generatePrefixedId } from '@waha/utils/ids';
import { IncomingMessage } from 'http';
import * as lodash from 'lodash';
import * as url from 'url';
@@ -52,8 +52,8 @@ export class WebsocketGatewayCore
}
handleConnection(socket: WebSocket, request: IncomingMessage, ...args): any {
const random = Math.random().toString(36).substring(7);
const id = `wsc_${random}`;
// 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);
@@ -105,7 +105,9 @@ export class WebsocketGatewayCore
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) => {
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,
+36 -29
View File
@@ -1,17 +1,23 @@
import { ConsoleLogger, INestApplication, 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 { BufferJsonReplacerInterceptor } from '@waha/api/BufferJsonReplacerInterceptor';
import { ChannelsController } from '@waha/api/channels.controller';
import {
ServerController,
ServerDebugController,
} 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';
@@ -26,10 +32,7 @@ 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';
@@ -51,7 +54,11 @@ export const IMPORTS = [
transport: getPinoTransport(),
autoLogging: {
ignore: (req) => {
return req.url.startsWith('/dashboard/');
return (
req.url.startsWith('/dashboard/') ||
req.url.startsWith('/api/files/') ||
req.url.startsWith('/api/s3/')
);
},
},
serializers: {
@@ -70,29 +77,24 @@ export const IMPORTS = [
}),
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,
@@ -101,7 +103,6 @@ export const IMPORTS = [
export const CONTROLLERS = [
AuthController,
SessionsController,
SessionController,
ChattingController,
ChatsController,
ChannelsController,
@@ -111,9 +112,11 @@ export const CONTROLLERS = [
GroupsController,
PresenceController,
ScreenshotController,
VersionController,
HealthController,
PingController,
HealthController,
ServerController,
ServerDebugController,
VersionController,
];
const PROVIDERS = [
{
@@ -132,8 +135,8 @@ const PROVIDERS = [
SwaggerConfigServiceCore,
WhatsappConfigService,
EngineConfigService,
ConsoleLogger,
WebsocketGatewayCore,
MediaLocalStorageConfig,
];
@Module({
@@ -142,7 +145,11 @@ 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;
+102 -63
View File
@@ -4,6 +4,7 @@ import makeWASocket, {
DisconnectReason,
extractMessageContent,
getAggregateVotesInPollMessage,
getContentType,
getKeyAuthor,
getUrlFromDirectPath,
isJidGroup,
@@ -25,6 +26,7 @@ import { isLidUser } from '@adiwajshing/baileys/lib/WABinary/jid-utils';
import { Logger as BaileysLogger } from '@adiwajshing/baileys/node_modules/pino';
import { UnprocessableEntityException } from '@nestjs/common';
import { NowebInMemoryStore } from '@waha/core/engines/noweb/store/NowebInMemoryStore';
import { IMediaEngineProcessor } from '@waha/core/media/IMediaEngineProcessor';
import { flipObject, parseBool, splitAt } from '@waha/helpers';
import { PairingCodeResponse } from '@waha/structures/auth.dto';
import { CallData } from '@waha/structures/calls.dto';
@@ -41,11 +43,13 @@ import {
LabelChatAssociation,
LabelID,
} from '@waha/structures/labels.dto';
import { ReplyToMessage } from '@waha/structures/message.dto';
import {
PollVote,
PollVotePayload,
WAMessageAckBody,
} from '@waha/structures/webhooks.dto';
import { waitUntil } from '@waha/utils/promiseTimeout';
import { SingleDelayedJobRunner } from '@waha/utils/SingleDelayedJobRunner';
import { SinglePeriodicJobRunner } from '@waha/utils/SinglePeriodicJobRunner';
import * as Buffer from 'buffer';
@@ -101,7 +105,6 @@ import {
DeleteStatusRequest,
TextStatus,
} from '../../../structures/status.dto';
import { IEngineMediaProcessor } from '../../abc/media.abc';
import {
ensureSuffix,
getChannelInviteLink,
@@ -195,9 +198,9 @@ export class WhatsappSessionNoWebCore extends WhatsappSession {
);
}
start() {
async start() {
this.status = WAHASessionStatus.STARTING;
this.buildClient();
await this.buildClient();
}
getSocketConfig(agent, state): any {
@@ -281,6 +284,8 @@ export class WhatsappSessionNoWebCore extends WhatsappSession {
}
async buildClient() {
// @ts-ignore
this.sock?.ev?.removeAllListeners();
this.sock = await this.makeSocket();
this.issueMessageUpdateOnEdits();
this.issuePresenceUpdateOnMessageUpsert();
@@ -443,8 +448,11 @@ export class WhatsappSessionNoWebCore extends WhatsappSession {
// @ts-ignore
this.sock?.ev?.removeAllListeners();
this.sock?.ws?.removeAllListeners();
await this.store?.close();
// wait until connection is not connecting to avoid error:
// "WebSocket was closed before the connection was established"
await waitUntil(async () => !this.sock?.ws?.isConnecting, 1_000, 10_000);
this.sock?.end(undefined);
this.store?.close();
}
getSessionMeInfo(): MeInfo | null {
@@ -532,26 +540,20 @@ export class WhatsappSessionNoWebCore extends WhatsappSession {
};
}
sendText(request: MessageTextRequest) {
async sendText(request: MessageTextRequest) {
const chatId = toJID(this.ensureSuffix(request.chatId));
const message = {
text: request.text,
mentions: request.mentions?.map(toJID),
};
return this.sock.sendMessage(chatId, message);
const options = await this.getMessageOptions(request);
return this.sock.sendMessage(chatId, message, options);
}
public deleteMessage(chatId: string, messageId: string) {
const jid = toJID(this.ensureSuffix(chatId));
const key = parseMessageId(messageId);
const options = {
statusJidList: [
'79521562380@s.whatsapp.net',
'79069571990@s.whatsapp.net',
'79069471990@s.whatsapp.net',
],
};
return this.sock.sendMessage(jid, { delete: key }, options);
const key = parseMessageIdSerialized(messageId);
return this.sock.sendMessage(jid, { delete: key });
}
public editMessage(
@@ -560,7 +562,7 @@ export class WhatsappSessionNoWebCore extends WhatsappSession {
request: EditMessageRequest,
) {
const jid = toJID(this.ensureSuffix(chatId));
const key = parseMessageId(messageId);
const key = parseMessageIdSerialized(messageId);
const message = {
text: request.text,
mentions: request.mentions?.map(toJID),
@@ -590,23 +592,18 @@ export class WhatsappSessionNoWebCore extends WhatsappSession {
};
const message = { poll: poll };
const remoteJid = toJID(request.chatId);
const result = await this.sock.sendMessage(remoteJid, message);
const options = await this.getMessageOptions(request);
const result = await this.sock.sendMessage(remoteJid, message, options);
return this.toWAMessage(result);
}
async reply(request: MessageReplyRequest) {
const { id } = parseMessageId(request.reply_to);
const quotedMessage = await this.store.loadMessage(
toJID(request.chatId),
id,
);
const options = await this.getMessageOptions(request);
const message = {
text: request.text,
mentions: request.mentions?.map(toJID),
};
return await this.sock.sendMessage(request.chatId, message, {
quoted: quotedMessage,
});
return await this.sock.sendMessage(request.chatId, message, options);
}
sendImage(request: MessageImageRequest) {
@@ -637,21 +634,26 @@ export class WhatsappSessionNoWebCore extends WhatsappSession {
}
async sendSeen(request: SendSeenRequest) {
const key = parseMessageId(request.messageId);
const key = parseMessageIdSerialized(request.messageId);
const participant = request.participant
? toJID(this.ensureSuffix(request.participant))
: undefined;
const data = {
remoteJid: key.remoteJid,
id: key.id,
participant: request.participant,
participant: participant,
};
return this.sock.readMessages([data]);
}
async startTyping(request: ChatRequest) {
return this.sock.sendPresenceUpdate('composing', request.chatId);
const chatId = toJID(this.ensureSuffix(request.chatId));
return this.sock.sendPresenceUpdate('composing', chatId);
}
async stopTyping(request: ChatRequest) {
return this.sock.sendPresenceUpdate('paused', request.chatId);
const chatId = toJID(this.ensureSuffix(request.chatId));
return this.sock.sendPresenceUpdate('paused', chatId);
}
async getMessages(query: GetMessageQuery) {
@@ -668,19 +670,18 @@ export class WhatsappSessionNoWebCore extends WhatsappSession {
toJID(chatId),
toNumber(limit),
);
const result = [];
const promises = [];
for (const msg of messages) {
const wamsg = await this.processIncomingMessage(msg, downloadMedia);
if (!wamsg) {
continue;
}
result.push(wamsg);
promises.push(this.processIncomingMessage(msg, downloadMedia));
}
let result = await Promise.all(promises);
result = result.filter(Boolean);
return result;
}
async setReaction(request: MessageReactionRequest) {
const key = parseMessageId(request.messageId);
const key = parseMessageIdSerialized(request.messageId);
const reactionMessage = {
react: {
text: request.reaction,
@@ -691,7 +692,7 @@ export class WhatsappSessionNoWebCore extends WhatsappSession {
}
async setStar(request: MessageStarRequest) {
const key = parseMessageId(request.messageId);
const key = parseMessageIdSerialized(request.messageId);
await this.sock.chatModify(
{
star: {
@@ -890,6 +891,9 @@ export class WhatsappSessionNoWebCore extends WhatsappSession {
`NOWEB engine doesn't support '${presence}' presence.`,
);
}
if (chatId) {
chatId = toJID(this.ensureSuffix(chatId));
}
await this.sock.sendPresenceUpdate(enginePresence, chatId);
}
@@ -930,13 +934,7 @@ export class WhatsappSessionNoWebCore extends WhatsappSession {
public deleteStatus(request: DeleteStatusRequest) {
const messageId = request.id;
let key: any;
if (messageId.includes('_')) {
key = parseMessageId(messageId);
} else {
key = { id: messageId };
}
const key = parseMessageIdSerialized(messageId, true);
key.fromMe = true;
key.remoteJid = BROADCAST_ID;
const JIDs = request.contacts.map(toJID);
@@ -1016,19 +1014,19 @@ export class WhatsappSessionNoWebCore extends WhatsappSession {
}
public async channelsFollowChannel(id: string): Promise<void> {
return await this.sock.newsletterFollow(id);
return await this.sock.newsletterAction(id, 'follow');
}
public async channelsUnfollowChannel(id: string): Promise<void> {
return await this.sock.newsletterUnfollow(id);
return await this.sock.newsletterAction(id, 'unfollow');
}
public async channelsMuteChannel(id: string): Promise<void> {
return await this.sock.newsletterMute(id);
return await this.sock.newsletterAction(id, 'mute');
}
public async channelsUnmuteChannel(id: string): Promise<void> {
return await this.sock.newsletterUnmute(id);
return await this.sock.newsletterAction(id, 'unmute');
}
/**
@@ -1266,18 +1264,8 @@ export class WhatsappSessionNoWebCore extends WhatsappSession {
protected toWAMessage(message): Promise<WAMessage> {
const fromToParticipant = getFromToParticipant(message);
const id = buildMessageId(message.key);
let body = message.message.conversation;
if (!body) {
// Some of the messages have no conversation, but instead have text in extendedTextMessage
// https://github.com/devlikeapro/waha/issues/90
body = message.message.extendedTextMessage?.text;
}
if (!body) {
// Populate from caption
const mediaContent = extractMediaContent(message.message);
// @ts-ignore - AudioMessage doesn't have caption field
body = mediaContent?.caption;
}
const body = this.extractBody(message.message);
const replyTo = this.extractReplyTo(message.message);
const ack = message.ack || message.status - 1;
return Promise.resolve({
id: id,
@@ -1297,10 +1285,45 @@ export class WhatsappSessionNoWebCore extends WhatsappSession {
ackName: WAMessageAck[ack] || ACK_UNKNOWN,
location: message.location,
vCards: message.vCards,
replyTo: replyTo,
_data: message,
});
}
protected extractBody(message): string | null {
if (!message) {
return null;
}
let body = message.conversation;
if (!body) {
// Some of the messages have no conversation, but instead have text in extendedTextMessage
// https://github.com/devlikeapro/waha/issues/90
body = message.extendedTextMessage?.text;
}
if (!body) {
// Populate from caption
const mediaContent = extractMediaContent(message);
// @ts-ignore - AudioMessage doesn't have caption field
body = mediaContent?.caption;
}
return body;
}
protected extractReplyTo(message): ReplyToMessage | null {
const msgType = getContentType(message);
const contextInfo = message[msgType]?.contextInfo;
if (!contextInfo) {
return null;
}
const quotedMessage = contextInfo.quotedMessage;
const body = this.extractBody(quotedMessage);
return {
id: contextInfo.stanzaId,
participant: toCusFormat(contextInfo.participant),
body: body,
};
}
protected toWAContact(contact: Contact) {
contact.id = toCusFormat(contact.id);
// @ts-ignore
@@ -1464,11 +1487,23 @@ export class WhatsappSessionNoWebCore extends WhatsappSession {
protected downloadMedia(message) {
const processor = new EngineMediaProcessor(this);
return this.mediaManager.processMedia(processor, message);
return this.mediaManager.processMedia(processor, message, this.name);
}
protected async getMessageOptions(request: any): Promise<any> {
let quoted;
if (request.reply_to) {
const key = parseMessageIdSerialized(request.reply_to, true);
quoted = await this.store.loadMessage(toJID(request.chatId), key.id);
}
return {
quoted: quoted,
};
}
}
export class EngineMediaProcessor implements IEngineMediaProcessor<any> {
export class EngineMediaProcessor implements IMediaEngineProcessor<any> {
constructor(public session: WhatsappSessionNoWebCore) {}
hasMedia(message: any): boolean {
@@ -1555,7 +1590,11 @@ function buildMessageId({ id, remoteJid, fromMe }) {
* false_11111111111@c.us_AAA
* {id: "AAA", remoteJid: "11111111111@s.whatsapp.net", "fromMe": false}
*/
function parseMessageId(messageId) {
function parseMessageIdSerialized(messageId: string, soft: boolean = false) {
if (!messageId.includes('_') && soft) {
return { id: messageId };
}
const parts = messageId.split('_');
if (parts.length != 3) {
throw new Error(
@@ -1,6 +1,6 @@
import { Chat, Contact, makeInMemoryStore, proto } from '@adiwajshing/baileys';
import { Label } from '@adiwajshing/baileys/lib/Types/Label';
import { BadRequestException, ConsoleLogger } from '@nestjs/common';
import { BadRequestException } from '@nestjs/common';
import { INowebStore } from './INowebStore';
@@ -1,307 +0,0 @@
import { UnprocessableEntityException } from '@nestjs/common/exceptions/unprocessable-entity.exception';
import * as Buffer from 'buffer';
import { create, CreateConfig, Message, Whatsapp } from 'venom-bot';
import {
ChatRequest,
CheckNumberStatusQuery,
GetMessageQuery,
MessageFileRequest,
MessageImageRequest,
MessageLinkPreviewRequest,
MessageLocationRequest,
MessageReactionRequest,
MessageReplyRequest,
MessageTextRequest,
WANumberExistResult,
} from '../../../structures/chatting.dto';
import {
WAHAEngine,
WAHAEvents,
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';
export class WhatsappSessionVenomCore extends WhatsappSession {
engine = WAHAEngine.VENOM;
whatsapp: Whatsapp;
private qr: QR;
public constructor(config) {
super(config);
this.qr = new QR();
}
protected buildClient() {
const venomOptions: CreateConfig = {
headless: true,
devtools: false,
debug: false,
logQR: true,
browserArgs: this.getBrowserArgsForPuppeteer(),
autoClose: 60000,
puppeteerOptions: {},
};
this.addProxyConfig(venomOptions);
return create(this.name, this.getCatchQR(), undefined, venomOptions);
}
protected addProxyConfig(venomOptions: CreateConfig) {
if (this.proxyConfig?.server !== undefined) {
venomOptions.addProxy = [this.proxyConfig?.server];
}
if (
this.proxyConfig?.username !== undefined &&
this.proxyConfig?.password !== undefined
) {
venomOptions.userProxy = this.proxyConfig?.username;
venomOptions.userPass = this.proxyConfig?.password;
}
}
protected getCatchQR() {
return (base64Qrimg, asciiQR, attempts, urlCode) => {
this.qr.save(base64Qrimg, urlCode);
this.printQR(this.qr);
this.status = WAHASessionStatus.SCAN_QR_CODE;
this.logger.debug('Number of attempts to read the qrcode: ', attempts);
};
}
async start() {
this.status = WAHASessionStatus.STARTING;
try {
this.whatsapp = await this.buildClient();
} catch (error) {
this.status = WAHASessionStatus.FAILED;
this.logger.error(error);
this.qr.save('');
return;
}
this.status = WAHASessionStatus.WORKING;
this.events.emit(WAHAInternalEvent.ENGINE_START);
return this;
}
async stop() {
this.status = WAHASessionStatus.STOPPED;
this.events.removeAllListeners();
await this.whatsapp.close();
}
subscribeEngineEvent(event: WAHAEvents | string, handler: (message) => void) {
switch (event) {
case WAHAEvents.MESSAGE:
this.whatsapp.onMessage((message: Message) =>
this.processIncomingMessage(message).then(handler),
);
return true;
case WAHAEvents.MESSAGE_ANY:
this.whatsapp.onAnyMessage((message: Message) =>
this.processIncomingMessage(message).then(handler),
);
return true;
case WAHAEvents.STATE_CHANGE:
this.whatsapp.onStateChange(handler);
return true;
case WAHAEvents.MESSAGE_ACK:
this.whatsapp.onAck(handler);
return true;
case WAHAEvents.GROUP_JOIN:
this.whatsapp.onAddedToGroup(handler);
return true;
default:
return false;
}
}
/**
* START - Methods for API
*/
/**
* Auth methods
*/
public getQR(): QR {
return this.qr;
}
getScreenshot(): Promise<Buffer> {
if (this.status === WAHASessionStatus.STARTING) {
throw new UnprocessableEntityException(
`The session is starting, please try again after few seconds`,
);
} else if (this.status === WAHASessionStatus.SCAN_QR_CODE) {
return Promise.resolve(this.qr.get());
} else if (this.status === WAHASessionStatus.WORKING) {
return this.whatsapp.page.screenshot();
} else {
throw new UnprocessableEntityException(`Unknown status - ${this.status}`);
}
}
async checkNumberStatus(
request: CheckNumberStatusQuery,
): Promise<WANumberExistResult> {
try {
return await this.whatsapp.checkNumberStatus(
this.ensureSuffix(request.phone),
);
} catch (error) {
// Catch number doesn't exist error and return it as is
if (error.status === 404 && !error.numberExists) {
return error;
}
throw error;
}
}
sendText(request: MessageTextRequest) {
return this.whatsapp.sendText(
this.ensureSuffix(request.chatId),
request.text,
);
}
reply(request: MessageReplyRequest) {
return this.whatsapp.reply(request.chatId, request.text, request.reply_to);
}
sendFile(request: MessageFileRequest) {
throw new NotImplementedByEngineError();
}
sendImage(request: MessageImageRequest) {
throw new NotImplementedByEngineError();
}
async sendVoice(request) {
throw new NotImplementedByEngineError();
}
sendLinkPreview(request: MessageLinkPreviewRequest) {
return this.whatsapp.sendLinkPreview(
request.chatId,
request.url,
request.title,
);
}
sendLocation(request: MessageLocationRequest) {
return this.whatsapp.sendLocation(
request.chatId,
String(request.latitude),
String(request.longitude),
request.title,
);
}
sendSeen(chat: ChatRequest) {
return this.whatsapp.markMarkSeenMessage(chat.chatId);
}
startTyping(chat: ChatRequest) {
return this.whatsapp.startTyping(chat.chatId, false);
}
stopTyping(chat: ChatRequest) {
return;
// The method is not available yet in new venom-bot
// return this.whatsapp.stopTyping(chat.chatId);
}
async getChatMessages(chatId: string, limit: number) {
const messages = await this.whatsapp.getAllMessagesInChat(
chatId,
true,
false,
);
// Go over messages, download media, and convert to right format.
const result = [];
for (const [count, message] of messages.entries()) {
if (count > limit) {
// Have enough in the list, stop processing
break;
}
result.push(await this.processIncomingMessage(message));
}
return result;
}
async getMessages(query: GetMessageQuery) {
return this.getChatMessages(query.chatId, query.limit);
}
setReaction(request: MessageReactionRequest) {
throw new NotImplementedByEngineError();
}
/**
* END - Methods for API
*/
protected downloadMedia(message: Message) {
const processor = new EngineMediaProcessor(this);
return this.mediaManager.processMedia(processor, message);
}
private processIncomingMessage(message: Message) {
return this.downloadMedia(message).then(this.toWAMessage);
}
protected toWAMessage(message: Message): Promise<WAMessage> {
// @ts-ignore
return Promise.resolve({
id: message.id,
timestamp: message.timestamp,
from: message.from,
fromMe: message.fromMe,
to: message.to,
body: message.body,
// Media
// @ts-ignore
hasMedia: Boolean(message.media),
// @ts-ignore
media: message.media,
// @ts-ignore
mediaUrl: message.media?.url,
// @ts-ignore
ack: message.ack,
location: undefined,
vCards: undefined,
_data: message,
});
}
}
export class EngineMediaProcessor implements IEngineMediaProcessor<Message> {
constructor(public session: WhatsappSessionVenomCore) {}
hasMedia(message: any): boolean {
if (!message.isMMS || !message.isMedia) {
return message;
}
}
getMessageId(message: any): string {
return '';
}
getMimetype(message: any): string {
return '';
}
getMediaBuffer(message: any): Promise<Buffer | null> {
return Promise.resolve(undefined);
}
getFilename(message: Message): string | null {
return null;
}
}
+55 -20
View File
@@ -1,5 +1,4 @@
import { UnprocessableEntityException } from '@nestjs/common/exceptions/unprocessable-entity.exception';
import { IEngineMediaProcessor } from '@waha/core/abc/media.abc';
import {
getChannelInviteLink,
WAHAInternalEvent,
@@ -10,6 +9,7 @@ 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';
@@ -52,9 +52,11 @@ import {
SettingsSecurityChangeInfo,
} 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 {
Call,
@@ -152,6 +154,7 @@ export class WhatsappSessionWebJSCore extends WhatsappSession {
);
return;
}
await this.end();
await this.start();
});
}
@@ -174,7 +177,6 @@ export class WhatsappSessionWebJSCore extends WhatsappSession {
}
protected async init() {
await this.end();
this.shouldRestart = true;
this.whatsapp = await this.buildClient();
this.whatsapp
@@ -233,9 +235,21 @@ export class WhatsappSessionWebJSCore extends WhatsappSession {
private async end() {
try {
this.whatsapp?.removeAllListeners();
this.whatsapp?.pupBrowser?.removeAllListeners();
this.whatsapp?.pupPage?.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.debug('Failed to destroy the client', error);
this.logger.warn(error, 'Failed to destroy the client');
});
} catch (error) {
this.logger.error(error);
@@ -341,10 +355,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,
@@ -371,11 +382,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);
}
@@ -395,7 +402,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) {
@@ -457,12 +465,12 @@ export class WhatsappSessionWebJSCore extends WhatsappSession {
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;
}
@@ -920,6 +928,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,
@@ -940,10 +949,24 @@ 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 {
@@ -960,11 +983,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;
+101 -76
View File
@@ -1,12 +1,12 @@
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 } from '@waha/utils/promiseTimeout';
import { promiseTimeout, sleep } from '@waha/utils/promiseTimeout';
import { EventEmitter } from 'events';
import { PinoLogger } from 'nestjs-pino';
@@ -18,24 +18,20 @@ import {
} 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';
import { SessionParams, WhatsappSession } from './abc/session.abc';
import { EngineConfigService } from './config/EngineConfigService';
import { WhatsappSessionNoWebCore } from './engines/noweb/session.noweb.core';
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';
@@ -46,10 +42,20 @@ export class OnlyDefaultSessionIsAllowed extends UnprocessableEntityException {
);
}
}
enum DefaultSessionStatus {
REMOVED = undefined,
STOPPED = null,
}
@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 | DefaultSessionStatus;
private sessionConfig?: SessionConfig;
DEFAULT = 'default';
// @ts-ignore
@@ -63,28 +69,26 @@ export class SessionManagerCore extends SessionManager {
) {
super();
this.events = new EventEmitter();
this.session = DefaultSessionStatus.STOPPED;
this.sessionConfig = null;
this.log.setContext(SessionManagerCore.name);
this.session = undefined;
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);
});
}
protected getEngine(engine: WAHAEngine): typeof WhatsappSession {
if (engine === WAHAEngine.WEBJS) {
return WhatsappSessionWebJSCore;
} else if (engine === WAHAEngine.VENOM) {
return WhatsappSessionVenomCore;
} else if (engine === WAHAEngine.NOWEB) {
return WhatsappSessionNoWebCore;
} else {
@@ -102,32 +106,45 @@ export class SessionManagerCore extends SessionManager {
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 !== DefaultSessionStatus.REMOVED;
}
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.info(`'${name}' - starting session...`);
const mediaManager = new CoreMediaManager(
new MediaStorageCore(),
const mediaManager = new MediaManagerCore(
new MediaNoopStorage(),
this.config.mimetypes,
);
const logger = this.log.logger.child({ session: name });
logger.level = getPinoLogLevel(request.config?.debug);
logger.level = getPinoLogLevel(this.sessionConfig?.debug);
const loggerBuilder: LoggerBuilder = logger;
const webhook = new this.WebhookConductorClass(loggerBuilder);
const proxyConfig = this.getProxyConfig(request);
const proxyConfig = this.getProxyConfig();
const sessionConfig: SessionParams = {
name,
mediaManager,
@@ -135,7 +152,7 @@ export class SessionManagerCore extends SessionManager {
printQR: this.engineConfigService.shouldPrintQR,
sessionStore: this.store,
proxyConfig: proxyConfig,
sessionConfig: request.config,
sessionConfig: this.sessionConfig,
};
await this.sessionAuthRepository.init(name);
// @ts-ignore
@@ -143,7 +160,7 @@ 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
@@ -154,6 +171,7 @@ export class SessionManagerCore extends SessionManager {
// start session
await session.start();
logger.info('Session has been started.');
return {
name: session.name,
status: session.status,
@@ -161,13 +179,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 = DefaultSessionStatus.STOPPED;
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 = DefaultSessionStatus.REMOVED;
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) {
@@ -179,44 +230,15 @@ 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.info(`Stopping ${name} session...`);
const session = this.getSession(name);
await session.stop();
this.log.info(`"${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.info(`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 as WhatsappSession };
return getProxyConfig(this.config, sessions, this.DEFAULT);
}
getSession(name: string): WhatsappSession {
@@ -224,32 +246,35 @@ export class SessionManagerCore extends SessionManager {
const session = this.session;
if (!session) {
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}'.\n` +
`Please start it first by using POST /sessions/${name}/start request`,
);
}
return session;
return session as WhatsappSession;
}
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 = this.session.getSessionMeInfo();
if (this.session === undefined && all) {
return [];
}
const session = this.session as WhatsappSession;
const me = session.getSessionMeInfo();
// Get engine info
let engineInfo = {};
if (this.session) {
if (session) {
try {
engineInfo = await promiseTimeout(10, this.session.getEngineInfo());
engineInfo = await promiseTimeout(10, session.getEngineInfo());
} catch (e) {
this.log.warn(
{ session: this.session },
@@ -258,14 +283,14 @@ export class SessionManagerCore extends SessionManager {
}
}
const engine = {
engine: this.session?.engine,
engine: session?.engine,
...engineInfo,
};
return [
{
name: this.session.name,
status: this.session.status,
config: this.session.sessionConfig,
name: session.name,
status: session.status,
config: session.sessionConfig,
me: me,
engine: engine,
},
-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,11 +16,13 @@ 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;
async exists(sessionName: string): Promise<boolean> {
const filepath = this.getFilePath(sessionName);
const exists = await fileExists(filepath);
if (!exists) {
// check directory exist for empty config sessions
const folder = this.store.getSessionDirectory(sessionName);
return await fileExists(folder);
}
return true;
}
@@ -26,7 +30,7 @@ export class LocalSessionConfigRepository extends ISessionConfigRepository {
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;
}
@@ -56,10 +60,17 @@ export class LocalSessionConfigRepository extends ISessionConfigRepository {
}
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);
}
}
+1 -2
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';
@@ -137,6 +135,7 @@ export class WebhookConductorCore implements WebhookConductor {
const json: WAHAWebhook = {
event: event,
session: session.name,
metadata: session.sessionConfig?.metadata,
me: me,
payload: data,
engine: session.engine,
-5
View File
@@ -13,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',
);
+17 -5
View File
@@ -11,10 +11,10 @@ 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 { AllExceptionsFilter } from './nestjs/AllExceptionsFilter';
import { WAHA_WEBHOOKS } from './structures/webhooks.dto';
import { getWAHAVersion, VERSION, WAHAVersion } from './version';
@@ -23,7 +23,7 @@ const logger: Logger = pino({
transport: getPinoTransport(),
}).child({ name: 'Bootstrap' });
logger.info('NODE - Catching unhandled exceptions enabled');
logger.info('NODE - Catching unhandled rejection enabled');
process.on('unhandledRejection', (reason, promise) => {
logger.error('Unhandled Rejection at:', promise, 'reason:', reason);
// @ts-ignore
@@ -34,7 +34,6 @@ async function loadModules(): Promise<
[typeof AppModuleCore, typeof SwaggerConfiguratorCore]
> {
const version = getWAHAVersion();
logger.info(`WAHA (WhatsApp HTTP API) - Running ${version} version...`);
if (version === WAHAVersion.CORE) {
const { AppModuleCore } = await import('./core/app.module.core');
@@ -52,10 +51,18 @@ 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 httpsOptions = AppModule.getHttpsOptions(logger);
const app = await NestFactory.create(AppModule, {
app = await NestFactory.create(AppModule, {
logger: getNestJSLogLevels(),
httpsOptions: httpsOptions,
bufferLogs: true,
@@ -89,4 +96,9 @@ async function bootstrap() {
logger.info(VERSION, 'Environment');
}
bootstrap();
bootstrap().catch((error) => {
logger.error(error, `Failed to start WAHA: ${error}`);
// @ts-ignore
logger.error(error.stack);
process.exit(1);
});
File renamed without changes.
+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),
},
},
},
}),
);
}
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',
});
+9
View File
@@ -0,0 +1,9 @@
import { ApiParam } from '@nestjs/swagger';
export const MessageIdApiParam = ApiParam({
name: 'messageId',
required: true,
type: 'string',
description: 'Message ID',
example: 'true_123456789@c.us_BAE6A33293978B16',
});
+20
View File
@@ -0,0 +1,20 @@
import { Param } from '@nestjs/common';
import { ApiParam } from '@nestjs/swagger';
import { SessionPipe } from '@waha/nestjs/pipes/SessionPipe';
/**
* Decorator for a method that uses SessionPipe above
*/
export const SessionApiParam = ApiParam({
name: 'session',
required: true,
type: 'string',
schema: {
default: 'default',
},
description: 'Session name',
});
/**
* Session param
*/
export const SessionParam = Param('session', SessionPipe);
+17
View File
@@ -0,0 +1,17 @@
import { Injectable, PipeTransform } from '@nestjs/common';
import { SessionManager } from '@waha/core/abc/manager.abc';
import { WhatsappSession } from '@waha/core/abc/session.abc';
/**
* 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);
}
}
+15
View File
@@ -0,0 +1,15 @@
import { ValidationPipe, ValidationPipeOptions } from '@nestjs/common';
import { parseBool } from '@waha/helpers';
// So we can change it to True during development and testing
const WAHA_HTTP_STRICT_MODE = parseBool(process.env.WAHA_HTTP_STRICT_MODE);
export class WAHAValidationPipe extends ValidationPipe {
constructor(options?: ValidationPipeOptions) {
options = options || {};
options.transform = true;
options.whitelist = true;
options.forbidNonWhitelisted = WAHA_HTTP_STRICT_MODE;
super(options);
}
}
+23
View File
@@ -0,0 +1,23 @@
/**
* Convert string to boolean
* @param value
* @constructor
*/
export function BooleanString({ value }: { value: any }) {
switch (value) {
case 'true':
return true;
case 'True':
return true;
case '1':
return true;
case 'false':
return false;
case 'False':
return false;
case '0':
return false;
default:
return value;
}
}
+35
View File
@@ -0,0 +1,35 @@
import {
registerDecorator,
ValidationArguments,
ValidationOptions,
} from 'class-validator';
export function IsDynamicObject(validationOptions?: ValidationOptions) {
return function (object: Record<string, any>, propertyName: string) {
registerDecorator({
name: 'IsDynamicObject',
target: object.constructor,
propertyName: propertyName,
constraints: [],
options: validationOptions,
validator: {
validate(value: any, args: ValidationArguments) {
if (!value || typeof value !== 'object') {
return false;
}
for (const key in value) {
if (typeof key !== 'string' || typeof value[key] !== 'string') {
return false;
}
}
return true;
},
defaultMessage(validationArguments?: ValidationArguments): string {
return `${validationArguments.property} accepts string key-value pairs only`;
},
},
});
};
}
@@ -1,5 +1,5 @@
import { LoggerService } from '@nestjs/common';
import { WebSocket } from '@waha/utils/ws';
import { WebSocket } from '@waha/nestjs/ws/ws';
import { WebSocketServer } from 'ws';
export class WebsocketHeartbeatJob {
File renamed without changes.
+1
View File
@@ -4,6 +4,7 @@ export enum QRCodeFormat {
IMAGE = 'image',
RAW = 'raw',
}
export class QRCodeQuery {
format: QRCodeFormat = QRCodeFormat.IMAGE;
}
+1 -1
View File
@@ -15,5 +15,5 @@ export class SessionQuery {
*/
export class SessionBaseRequest {
session = WHATSAPP_DEFAULT_SESSION_NAME;
session: string = WHATSAPP_DEFAULT_SESSION_NAME;
}
+4
View File
@@ -45,6 +45,10 @@ export class Channel {
})
id: string;
@ApiProperty({
description: 'Channel name',
example: 'Channel Name',
})
name: string;
description?: string;
+53 -11
View File
@@ -15,12 +15,16 @@ import {
VoiceBinaryFile,
VoiceRemoteFile,
} from './files.dto';
import { ChatIdProperty } from './properties.dto';
import { ChatIdProperty, ReplyToProperty } from './properties.dto';
/**
* Queries
*/
export class CheckNumberStatusQuery extends SessionQuery {
@ApiProperty({
description: 'The phone number to check',
example: '1213213213',
})
@IsString()
phone: string;
}
@@ -72,6 +76,7 @@ export class SendSeenRequest extends ChatRequest {
@ApiProperty({
example: '11111111111@c.us',
required: false,
default: null,
description:
'NOWEB engine only - the ID of the user that sent the message (undefined for individual chats)',
})
@@ -141,30 +146,44 @@ export class MessageContactVcardRequest extends ChatRequest {
}
export class MessageTextRequest extends ChatRequest {
text = 'Hi there!';
text: string = 'Hi there!';
@ApiHideProperty()
mentions?: string[];
@ReplyToProperty()
reply_to?: string;
}
export class EditMessageRequest {
text = 'Hello, world!';
text: string = 'Hello, world!';
@ApiHideProperty()
mentions?: string[];
}
export class MessageReplyRequest extends MessageTextRequest {
text = 'Reply text';
@ApiProperty({
example: 'false_11111111111@c.us_AAAAAAAAAAAAAAAAAAAA',
})
reply_to: string;
text: string = 'Reply text';
}
export class MessageLocationRequest extends ChatRequest {
@ApiProperty({
example: 38.8937255,
})
latitude: number;
@ApiProperty({
example: -77.0969763,
})
longitude: number;
@ApiProperty({
example: 'Our office',
})
title: string;
@ReplyToProperty()
reply_to?: string;
}
@ApiExtraModels(BinaryFile, RemoteFile)
@@ -179,11 +198,17 @@ class FileRequest extends ChatRequest {
}
export class MessageImageRequest extends FileRequest {
caption: string;
caption?: string;
@ReplyToProperty()
reply_to?: string;
}
export class MessageFileRequest extends FileRequest {
caption: string;
caption?: string;
@ReplyToProperty()
reply_to?: string;
}
@ApiExtraModels(VoiceBinaryFile, VoiceRemoteFile)
@@ -195,6 +220,9 @@ export class MessageVoiceRequest extends ChatRequest {
],
})
file: VoiceBinaryFile | VoiceRemoteFile;
@ReplyToProperty()
reply_to?: string;
}
@ApiExtraModels(VideoRemoteFile, VideoBinaryFile)
@@ -207,7 +235,14 @@ export class MessageVideoRequest extends ChatRequest {
})
file: VideoRemoteFile | VideoBinaryFile;
caption: string = 'Just watch at this!';
caption?: string = 'Just watch at this!';
@ApiProperty({
description:
'The ID of the message to reply to - false_11111111111@c.us_AAAAAAAAAAAAAAAAAAAA',
example: null,
})
reply_to?: string;
}
export class MessageLinkPreviewRequest extends ChatRequest {
@@ -256,6 +291,13 @@ export class MessagePoll {
export class MessagePollRequest extends ChatRequest {
poll: MessagePoll;
@ApiProperty({
description:
'The ID of the message to reply to - false_11111111111@c.us_AAAAAAAAAAAAAAAAAAAA',
example: null,
})
reply_to?: string;
}
export class MessageDestination {
+3
View File
@@ -1,3 +1,4 @@
import { ChatIdProperty } from '@waha/structures/properties.dto';
import { IsString } from 'class-validator';
import { SessionBaseRequest, SessionQuery } from './base.dto';
@@ -7,6 +8,7 @@ import { SessionBaseRequest, SessionQuery } from './base.dto';
*/
export class ContactQuery extends SessionQuery {
@ChatIdProperty()
@IsString()
contactId: string;
}
@@ -16,6 +18,7 @@ export class ContactQuery extends SessionQuery {
*/
export class ContactRequest extends SessionBaseRequest {
@ChatIdProperty()
@IsString()
contactId: string;
}
-1
View File
@@ -32,7 +32,6 @@ export enum WAHASessionStatus {
}
export enum WAHAEngine {
VENOM = 'VENOM',
WEBJS = 'WEBJS',
NOWEB = 'NOWEB',
}
+5
View File
@@ -1,3 +1,4 @@
import { ApiProperty } from '@nestjs/swagger';
import { IsArray, IsString } from 'class-validator';
/**
@@ -5,6 +6,9 @@ import { IsArray, IsString } from 'class-validator';
*/
export class Participant {
@IsString()
@ApiProperty({
example: '123456789@c.us',
})
id: string;
}
@@ -29,6 +33,7 @@ export class DescriptionRequest {
@IsString()
description: string;
}
export class SubjectRequest {
@IsString()
subject: string;
+37
View File
@@ -0,0 +1,37 @@
import { ApiProperty } from '@nestjs/swagger';
import { S3MediaData } from '@waha/structures/media.s3.dto';
export class WAMedia {
@ApiProperty({
description: 'The URL for the media in the message if any',
example:
'http://localhost:3000/api/files/false_11111111111@c.us_AAAAAAAAAAAAAAAAAAAA.oga',
})
url?: string;
@ApiProperty({
description: 'mimetype for the media in the message if any',
example: 'audio/jpeg',
})
mimetype?: string;
@ApiProperty({
description: 'The original filename in mediaUrl in the message if any',
example: 'example.pdf',
})
filename?: string;
@ApiProperty({
description:
'S3 attributes for the media in the message ' +
'if you are using S3 media storage',
})
s3?: S3MediaData;
@ApiProperty({
description: "Error message if there's an error downloading the media",
example: null,
})
// eslint-disable-next-line @typescript-eslint/ban-types
error?: object;
}
+15
View File
@@ -0,0 +1,15 @@
import { ApiProperty } from '@nestjs/swagger';
export class S3MediaData {
@ApiProperty({
description: 'The name of the S3 bucket',
example: 'my-bucket',
})
Bucket: string;
@ApiProperty({
description: 'The key of the object in the S3 bucket',
example: 'default/false_11111111111@c.us_AAAAAAAAAAAAAAAAAAAA.oga',
})
Key: string;
}
+18
View File
@@ -0,0 +1,18 @@
import { ApiProperty } from '@nestjs/swagger';
import {
ChatIdProperty,
MessageIdOnlyProperty,
} from '@waha/structures/properties.dto';
export class ReplyToMessage {
@MessageIdOnlyProperty()
id: string;
@ChatIdProperty()
participant?: string;
@ApiProperty({
example: 'Hello!',
})
body?: string;
}
+21 -1
View File
@@ -15,4 +15,24 @@ function MessageIdProperty() {
});
}
export { ChatIdProperty, MessageIdProperty };
function MessageIdOnlyProperty() {
return ApiProperty({
description: 'Message ID',
example: 'AAAAAAAAAAAAAAAAAAAA',
});
}
function ReplyToProperty() {
return ApiProperty({
description:
'The ID of the message to reply to - false_11111111111@c.us_AAAAAAAAAAAAAAAAAAAA',
example: null,
});
}
export {
ChatIdProperty,
MessageIdOnlyProperty,
MessageIdProperty,
ReplyToProperty,
};
+8 -22
View File
@@ -1,4 +1,6 @@
import { ApiProperty } from '@nestjs/swagger';
import { WAMedia } from '@waha/structures/media.dto';
import { ReplyToMessage } from '@waha/structures/message.dto';
import { WAMessageAck } from './enums.dto';
import { ChatIdProperty, MessageIdProperty } from './properties.dto';
@@ -9,27 +11,6 @@ export class WALocation {
longitude: string;
}
export class WAMedia {
@ApiProperty({
description: 'The URL for the media in the message if any',
example:
'http://localhost:3000/api/files/false_11111111111@c.us_AAAAAAAAAAAAAAAAAAAA.oga',
})
url: string;
@ApiProperty({
description: 'mimetype for the media in the message if any',
example: 'audio/jpeg',
})
mimetype?: string;
@ApiProperty({
description: 'The original filename in mediaUrl in the message if any',
example: 'example.pdf',
})
filename?: string;
}
class WAMessageBase {
@MessageIdProperty()
id: string;
@@ -78,7 +59,10 @@ export class WAMessage extends WAMessageBase {
})
hasMedia: boolean;
media?: WAMedia = null;
@ApiProperty({
description: 'Media object for the message if any and downloaded',
})
media?: WAMedia;
@ApiProperty({
description:
@@ -116,6 +100,8 @@ export class WAMessage extends WAMessageBase {
})
vCards?: string[];
replyTo?: ReplyToMessage;
/** Returns message in a raw format */
@ApiProperty({
description:
+43
View File
@@ -0,0 +1,43 @@
import { ApiProperty } from '@nestjs/swagger';
import { BooleanString } from '@waha/nestjs/validation/BooleanString';
import { Transform } from 'class-transformer';
import { IsBoolean, IsOptional } from 'class-validator';
export class EnvironmentQuery {
@ApiProperty({
example: false,
required: false,
description: 'Include all environment variables',
})
@Transform(BooleanString)
@IsBoolean()
@IsOptional()
all: boolean = false;
}
export class StopRequest {
@ApiProperty({
example: false,
required: false,
description:
'By default, it gracefully stops the server, ' +
'but you can force it to terminate immediately.',
})
@IsBoolean()
@IsOptional()
force: boolean = false;
}
export class ServerStatusResponse {
@ApiProperty({
example: 1723788847247,
description: 'The timestamp when the server started (milliseconds).',
})
startTimestamp: number;
@ApiProperty({
example: 3600000,
description: 'The uptime of the server in milliseconds.',
})
uptime: number;
}
+50
View File
@@ -0,0 +1,50 @@
import { ApiProperty } from '@nestjs/swagger';
import { SessionConfig } from '@waha/structures/sessions.dto';
import { Type } from 'class-transformer';
import {
IsBoolean,
IsOptional,
IsString,
ValidateNested,
} from 'class-validator';
export class SessionStartDeprecatedRequest {
@ApiProperty({
example: 'default',
description: 'Session name (id)',
})
@IsString()
name: string;
@ValidateNested()
@Type(() => SessionConfig)
@IsOptional()
config?: SessionConfig;
}
export class SessionStopDeprecatedRequest {
@ApiProperty({
example: 'default',
description: 'Session name (id)',
})
@IsString()
name: string;
@ApiProperty({
example: false,
required: false,
description: 'Stop and logout from the session.',
})
@IsBoolean()
@IsOptional()
logout: boolean | undefined = false;
}
export class SessionLogoutDeprecatedRequest {
@ApiProperty({
example: 'default',
description: 'Session name (id)',
})
@IsString()
name: string;
}
+99 -22
View File
@@ -1,4 +1,15 @@
import { ApiProperty } from '@nestjs/swagger';
import { BooleanString } from '@waha/nestjs/validation/BooleanString';
import { IsDynamicObject } from '@waha/nestjs/validation/IsDynamicObject';
import { Transform, Type } from 'class-transformer';
import {
IsArray,
IsBoolean,
IsOptional,
IsString,
MaxLength,
ValidateNested,
} from 'class-validator';
import { WAHASessionStatus } from './enums.dto';
import { ChatIdProperty } from './properties.dto';
@@ -14,7 +25,10 @@ export class ListSessionsQuery {
description:
'Return all sessions, including those that are in the STOPPED state.',
})
all: boolean;
@Transform(BooleanString)
@IsBoolean()
@IsOptional()
all?: boolean;
}
/**
@@ -24,16 +38,21 @@ export class ProxyConfig {
@ApiProperty({
example: 'localhost:3128',
})
@IsString()
server: string;
@ApiProperty({
example: null,
})
@IsString()
@IsOptional()
username?: string;
@ApiProperty({
example: null,
})
@IsString()
@IsOptional()
password?: string;
}
@@ -41,7 +60,9 @@ export class NowebStoreConfig {
@ApiProperty({
description:
'Enable or disable the store for contacts, chats, and messages.',
example: true,
})
@IsBoolean()
enabled: boolean = false;
@ApiProperty({
@@ -50,22 +71,52 @@ export class NowebStoreConfig {
'Full sync will download all contacts, chats, and messages from the phone.\n' +
'If disabled, only messages early than 90 days will be downloaded and some contacts may be missing.',
})
@IsBoolean()
fullSync: boolean = false;
}
export class NowebConfig {
@ValidateNested()
@Type(() => NowebStoreConfig)
@IsOptional()
store?: NowebStoreConfig;
}
export class SessionConfig {
@ValidateNested({ each: true })
@Type(() => WebhookConfig)
@IsArray()
@IsOptional()
webhooks?: WebhookConfig[];
@ApiProperty({
example: {
'user.id': '123',
'user.email': 'email@example.com',
},
description:
"Metadata for the session. You'll get 'metadata' in all webhooks.",
required: false,
})
@IsDynamicObject()
@IsOptional()
metadata?: Map<string, string>;
@ApiProperty({
example: null,
})
@ValidateNested()
@Type(() => ProxyConfig)
@IsOptional()
proxy?: ProxyConfig;
debug: boolean = false;
@ApiProperty({
required: false,
default: false,
})
@IsBoolean()
@IsOptional()
debug: boolean;
@ApiProperty({
example: {
@@ -75,30 +126,20 @@ export class SessionConfig {
},
},
})
@ValidateNested()
@Type(() => NowebConfig)
@IsOptional()
noweb?: NowebConfig;
}
export class SessionStartRequest {
name = 'default';
config?: SessionConfig;
}
export class SessionStopRequest {
name = 'default';
@ApiProperty({
example: false,
required: false,
description: 'Stop and logout from the session.',
})
logout = false;
}
export class SessionLogoutRequest {
name = 'default';
}
export class SessionDTO {
name = 'default';
@ApiProperty({
example: 'default',
description: 'Session name (id)',
})
@IsString()
name: string;
status: WAHASessionStatus;
config?: SessionConfig;
}
@@ -114,3 +155,39 @@ export class SessionInfo extends SessionDTO {
me?: MeInfo;
engine?: any;
}
const MONGO_DB_NAME_LIMIT = 64;
const MONGO_DB_NAME_PREFIX_LEN = 'waha_noweb'.length;
export class SessionCreateRequest {
@ApiProperty({
example: 'default',
description: 'Session name (id)',
required: false,
})
@IsString()
@IsOptional()
@MaxLength(MONGO_DB_NAME_LIMIT - MONGO_DB_NAME_PREFIX_LEN)
name: string | undefined;
@ValidateNested()
@Type(() => SessionConfig)
@IsOptional()
config?: SessionConfig;
@ApiProperty({
description: 'Start session after creation',
example: true,
default: true,
})
@IsBoolean()
@IsOptional()
start?: boolean;
}
export class SessionUpdateRequest {
@ValidateNested()
@Type(() => SessionConfig)
@IsOptional()
config?: SessionConfig;
}
+18 -16
View File
@@ -3,6 +3,8 @@ import { ApiExtraModels, ApiProperty, getSchemaPath } from '@nestjs/swagger';
import {
BinaryFile,
RemoteFile,
VideoBinaryFile,
VideoRemoteFile,
VoiceBinaryFile,
VoiceRemoteFile,
} from './files.dto';
@@ -19,48 +21,48 @@ class StatusRequest {
}
export class TextStatus extends StatusRequest {
text = 'Have a look! https://github.com/';
backgroundColor = '#38b42f';
font = 1;
text: string = 'Have a look! https://github.com/';
backgroundColor: string = '#38b42f';
font: number = 1;
}
@ApiExtraModels(BinaryFile, RemoteFile)
@ApiExtraModels(RemoteFile, BinaryFile)
export class ImageStatus extends StatusRequest {
@ApiProperty({
oneOf: [
{ $ref: getSchemaPath(BinaryFile) },
{ $ref: getSchemaPath(RemoteFile) },
{ $ref: getSchemaPath(BinaryFile) },
],
})
file: BinaryFile | RemoteFile;
file: RemoteFile | BinaryFile;
caption: string;
caption?: string;
}
@ApiExtraModels(VoiceBinaryFile, VoiceRemoteFile)
@ApiExtraModels(VoiceRemoteFile, VoiceBinaryFile)
export class VoiceStatus extends StatusRequest {
@ApiProperty({
oneOf: [
{ $ref: getSchemaPath(VoiceBinaryFile) },
{ $ref: getSchemaPath(VoiceRemoteFile) },
{ $ref: getSchemaPath(VoiceBinaryFile) },
],
})
file: VoiceBinaryFile | VoiceRemoteFile;
file: VoiceRemoteFile | VoiceBinaryFile;
backgroundColor = '#38b42f';
backgroundColor: string = '#38b42f';
}
@ApiExtraModels(BinaryFile, RemoteFile)
@ApiExtraModels(VideoRemoteFile, VideoBinaryFile)
export class VideoStatus extends StatusRequest {
@ApiProperty({
oneOf: [
{ $ref: getSchemaPath(BinaryFile) },
{ $ref: getSchemaPath(RemoteFile) },
{ $ref: getSchemaPath(VideoRemoteFile) },
{ $ref: getSchemaPath(VideoBinaryFile) },
],
})
file: BinaryFile | RemoteFile;
file: VideoRemoteFile | VideoBinaryFile;
caption: string;
caption?: string;
}
export class DeleteStatusRequest extends StatusRequest {
+37 -4
View File
@@ -1,25 +1,43 @@
import { ApiProperty } from '@nestjs/swagger';
import { WAHAEvents } from '@waha/structures/enums.dto';
import { Type } from 'class-transformer';
import {
IsArray,
IsEnum,
IsNumber,
IsOptional,
IsString,
IsUrl,
ValidateNested,
} from 'class-validator';
export class RetriesConfiguration {
@ApiProperty({
example: 2,
})
delaySeconds: number;
@IsNumber()
@IsOptional()
delaySeconds?: number;
@ApiProperty({
example: 15,
})
attempts: number;
@IsNumber()
@IsOptional()
attempts?: number;
}
export class CustomHeader {
@ApiProperty({
example: 'X-My-Custom-Header',
})
@IsString()
name: string;
@ApiProperty({
example: 'Value',
})
@IsString()
value: string;
}
@@ -27,7 +45,9 @@ export class HmacConfiguration {
@ApiProperty({
example: 'your-secret-key',
})
key: string;
@IsString()
@IsOptional()
key?: string;
}
export class WebhookConfig {
@@ -37,26 +57,39 @@ export class WebhookConfig {
description:
'You can use https://docs.webhook.site/ to test webhooks and see the payload',
})
@IsUrl({ require_protocol: true, require_tld: false })
url: string;
@ApiProperty({
example: ['message', 'session.status'],
required: true,
})
events: string[];
@IsEnum(WAHAEvents, { each: true })
@IsArray()
events: WAHAEvents[];
@ApiProperty({
example: null,
})
@ValidateNested()
@Type(() => HmacConfiguration)
@IsOptional()
hmac?: HmacConfiguration;
@ApiProperty({
example: null,
})
@ValidateNested()
@Type(() => RetriesConfiguration)
@IsOptional()
retries?: RetriesConfiguration;
@ApiProperty({
example: null,
})
@ValidateNested()
@Type(() => CustomHeader)
@IsArray()
@IsOptional()
customHeaders?: CustomHeader[];
}
+9
View File
@@ -106,6 +106,15 @@ export class WAHAWebhook {
})
session: string;
@ApiProperty({
example: {
'user.id': '123',
'user.email': 'email@example.com',
},
description: 'Metadata for the session.',
})
metadata?: Map<string, string>;
@ApiProperty({
example: WAHAEngine.WEBJS,
})
+2 -1
View File
@@ -32,8 +32,8 @@ export class SingleDelayedJobRunner {
this.timeout = setTimeout(() => {
this.logger.debug(`Running job...`);
fn().finally(() => {
this.logger.debug(`Job finished`);
this.timeout = null;
this.logger.debug(`Job finished`);
});
}, this.timeoutMs);
this.logger.info(`Job scheduled with timeout ${this.timeoutMs} ms`);
@@ -42,6 +42,7 @@ export class SingleDelayedJobRunner {
cancel() {
clearTimeout(this.timeout);
this.timeout = null;
this.logger.info(`Job cancelled`);
}
+1
View File
@@ -51,6 +51,7 @@ export class SinglePeriodicJobRunner {
stop() {
clearInterval(this.interval);
this.interval = null;
this.logger.info(`Job stopped`);
}
+11
View File
@@ -0,0 +1,11 @@
// eslint-disable-next-line @typescript-eslint/no-var-requires
const fs = require('fs-extra');
export async function fileExists(filepath: string) {
try {
await fs.access(filepath, fs.constants.F_OK);
} catch (error) {
return false;
}
return true;
}
+10
View File
@@ -0,0 +1,10 @@
import { v4 as uuid4 } from 'uuid';
/**
* Generate prefix uuid (but remove -)
* @param prefix
*/
export function generatePrefixedId(prefix: string) {
const id = uuid4().replace(/-/g, '');
return `${prefix}_${id}`;
}
+5
View File
@@ -31,6 +31,11 @@ export function getPinoLogLevel(debug: boolean = false): Level {
return getDefaultPinoLogLevel();
}
export function isDebugEnabled() {
const level = getPinoLogLevel();
return level == 'debug' || level == 'trace';
}
export function getPinoHttpUseLevel(): LevelWithSilent {
const levels = ['fatal', 'error', 'warn', 'info', 'debug', 'trace'];
const level = (process.env.WAHA_HTTP_LOG_LEVEL || 'info').toLowerCase();
+14
View File
@@ -36,3 +36,17 @@ export const promiseTimeout = function (
export async function sleep(ms: number) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
export async function waitUntil(
condition: () => Promise<boolean>,
everyMs: number,
timeoutMs: number,
): Promise<boolean> {
const startTime = Date.now();
let result = await condition();
while (!result && Date.now() - startTime < timeoutMs) {
await sleep(everyMs);
result = await condition();
}
return result;
}
+1 -1
View File
@@ -33,7 +33,7 @@ export function getEngineName(): string {
}
export const VERSION: WAHAEnvironment = {
version: '2024.8.4',
version: '2024.9.5',
engine: getEngineName(),
tier: getWAHAVersion(),
browser: getBrowserExecutablePath(),
+1464 -1976
View File
File diff suppressed because it is too large. Load diff