Compare commits

...
101 Commits
Author SHA1 Message Date
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
83 changed files with 3145 additions and 694 deletions

No files matched your search

+3
View File
@@ -2,6 +2,8 @@
example.ts
.*sessions
.secrets
.minio
.media
tokens
files
venv
@@ -11,6 +13,7 @@ src/test.js
test.js
.wwebjs_cache
src/dashboard
.eslintcache
# compiled output
/dist
+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())
+3 -2
View File
@@ -24,7 +24,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 c1a91f879a35f0ae32d46ddb2e061ac1819a3d4f
RUN \
wget https://github.com/devlikeapro/dashboard/archive/${WAHA_DASHBOARD_SHA}.zip \
&& unzip ${WAHA_DASHBOARD_SHA}.zip -d /tmp/dashboard \
@@ -86,6 +86,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 +97,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:
+54 -14
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
@@ -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
yarn start:prod
+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)
+8 -2
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",
@@ -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",
+7 -3
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,12 +27,10 @@ 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) {}
+5 -2
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) {}
+7 -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) {}
+22 -18
View File
@@ -26,7 +26,7 @@ import { WAMessage } from '../structures/responses.dto';
@ApiSecurity('api_key')
@Controller('api')
@ApiTags('chatting')
@ApiTags('📤 Chatting')
export class ChattingController {
constructor(private manager: SessionManager) {}
@@ -45,16 +45,6 @@ export class ChattingController {
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> {
@@ -123,13 +113,6 @@ export class ChattingController {
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);
@@ -172,4 +155,25 @@ export class ChattingController {
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('/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);
}
}
+1 -1
View File
@@ -11,7 +11,7 @@ import { ContactQuery, ContactRequest } from '../structures/contacts.dto';
@ApiSecurity('api_key')
@Controller('api/contacts')
@ApiTags('contacts')
@ApiTags('👤 Contacts')
export class ContactsController {
constructor(private manager: SessionManager) {}
+5 -2
View File
@@ -8,6 +8,10 @@ import {
Put,
} 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,11 +22,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) {}
+3 -2
View File
@@ -1,11 +1,12 @@
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) {}
-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),
},
},
},
}),
);
}
+5 -2
View File
@@ -1,14 +1,17 @@
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 {
SessionApiParam,
SessionParam,
} from '@waha/nestjs/params/SessionApiParam';
import { Label, LabelID, 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) {}
+3 -2
View File
@@ -1,9 +1,10 @@
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.' })
+5 -2
View File
@@ -7,6 +7,10 @@ import {
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';
@@ -15,11 +19,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) {}
+3 -3
View File
@@ -7,16 +7,16 @@ import {
UseInterceptors,
} from '@nestjs/common';
import { ApiSecurity, ApiTags } from '@nestjs/swagger';
import { ApiFileAcceptHeader } from '@waha/nestjs/ApiFileAcceptHeader';
import { Response } from 'express';
import { SessionManager } from '../core/abc/manager.abc';
import { BufferResponseInterceptor } from '../nestjs/BufferResponseInterceptor';
import { SessionQuery } from '../structures/base.dto';
import { BufferResponseInterceptor } from './BufferResponseInterceptor';
import { ApiFileAcceptHeader } from './helpers';
@ApiSecurity('api_key')
@Controller('api')
@ApiTags('screenshot')
@ApiTags('🖼️ Screenshot')
export class ScreenshotController {
constructor(private manager: SessionManager) {}
+107
View File
@@ -0,0 +1,107 @@
import * as process from 'node:process';
import {
Body,
Controller,
Get,
Logger,
Post,
Query,
UsePipes,
} from '@nestjs/common';
import { ApiOperation, ApiSecurity, ApiTags } from '@nestjs/swagger';
import { getApp } from '@waha/main';
import { WAHAValidationPipe } from '@waha/nestjs/pipes/WAHAValidationPipe';
import { WAHAEnvironment } from '@waha/structures/environment.dto';
import {
EnvironmentQuery,
ServerStatusResponse,
StopRequest,
} from '@waha/structures/server.dto';
import { VERSION } from '@waha/version';
import * as lodash from 'lodash';
@ApiSecurity('api_key')
@Controller('api/server')
@ApiTags('🔍 Observability')
export class ServerController {
private logger: Logger;
constructor() {
this.logger = new Logger('ServerController');
}
@Get('version')
@ApiOperation({ summary: 'Get the version of the server' })
get(): WAHAEnvironment {
return VERSION;
}
@Get('environment')
@ApiOperation({ summary: 'Return 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: '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);
}
}
}
+255 -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,231 @@ class SessionsController {
}
return session;
}
}
@ApiSecurity('api_key')
@Controller('api/sessions/:session')
@ApiTags('sessions')
class SessionController {
constructor(private manager: SessionManager) {}
@Get('me')
@Get(':session/me')
@SessionApiParam
@ApiOperation({ summary: 'Get information about the authenticated account' })
getMe(@SessionParam session: WhatsappSession): MeInfo | null {
return session.getSessionMeInfo();
}
@Post('')
@ApiOperation({
summary: 'Create a session',
description:
'Create session a new session (and start it at the same time if required).',
})
@UsePipes(new WAHAValidationPipe())
async create(@Body() request: SessionCreateRequest): Promise<SessionDTO> {
const name = request.name || generatePrefixedId('session');
await this.withLock(name, async () => {
if (await this.manager.exists(name)) {
const msg = `Session '${name}' already exists. Use PUT to update it.`;
throw new UnprocessableEntityException(msg);
}
const config = request.config;
const start = request.start || false;
await this.manager.upsert(name, config);
if (start) {
await this.manager.start(name);
}
});
return await this.manager.getSessionInfo(name);
}
@Put(':session')
@ApiOperation({
summary: 'Update a session',
description: '',
})
@SessionApiParam
@UsePipes(new WAHAValidationPipe())
async update(
@Param('session') name: string,
@Body() request: SessionUpdateRequest,
): Promise<SessionDTO> {
await this.withLock(name, async () => {
if (!(await this.manager.exists(name))) {
throw new NotFoundException('Session not found');
}
const config = request.config;
const isRunning = this.manager.isRunning(name);
await this.manager.stop(name, true);
await this.manager.upsert(name, config);
if (isRunning) {
await this.manager.start(name);
}
});
return await this.manager.getSessionInfo(name);
}
@Delete(':session')
@SessionApiParam
@ApiOperation({
summary: 'Delete the session',
description:
'Delete the session with the given name. Stop and logout as well. Idempotent operation.',
})
@UsePipes(new WAHAValidationPipe())
async delete(@Param('session') name: string): Promise<void> {
await this.withLock(name, async () => {
await this.manager.stop(name, true);
await this.manager.logout(name);
await this.manager.delete(name);
});
}
@Post(':session/start')
@SessionApiParam
@ApiOperation({
summary: 'Start the session',
description:
'Start the session with the given name. The session must exist. Identity operation.',
})
@UsePipes(new WAHAValidationPipe())
async start(@Param('session') name: string): Promise<SessionDTO> {
await this.withLock(name, async () => {
const exists = await this.manager.exists(name);
if (!exists) {
throw new NotFoundException('Session not found');
}
const isRunning = this.manager.isRunning(name);
if (!isRunning) {
await this.manager.start(name);
}
});
return await this.manager.getSessionInfo(name);
}
@Post(':session/stop')
@SessionApiParam
@ApiOperation({
summary: 'Stop the session',
description: 'Stop the session with the given name. Idempotent operation.',
})
@UsePipes(new WAHAValidationPipe())
async stop(@Param('session') name: string): Promise<SessionDTO> {
await this.withLock(name, async () => {
const exists = await this.manager.exists(name);
if (!exists) {
throw new NotFoundException('Session not found');
}
await this.manager.stop(name, false);
});
return await this.manager.getSessionInfo(name);
}
@Post(':session/logout')
@SessionApiParam
@ApiOperation({
summary: 'Logout from the session',
description: 'Logout the session, restart a session if it was not STOPPED',
})
@UsePipes(new WAHAValidationPipe())
async logout(@Param('session') name: string): Promise<SessionDTO> {
await this.withLock(name, async () => {
const exists = await this.manager.exists(name);
if (!exists) {
throw new NotFoundException('Session not found');
}
const isRunning = this.manager.isRunning(name);
await this.manager.stop(name, true);
await this.manager.logout(name);
if (isRunning) {
await this.manager.start(name);
}
});
return await this.manager.getSessionInfo(name);
}
@Post(':session/restart')
@SessionApiParam
@ApiOperation({
summary: 'Restart the session',
description: 'Restart the session with the given name.',
})
@UsePipes(new WAHAValidationPipe())
async restart(@Param('session') name: string): Promise<SessionDTO> {
await this.withLock(name, async () => {
const exists = await this.manager.exists(name);
if (!exists) {
throw new NotFoundException('Session not found');
}
await this.manager.stop(name, true);
await this.manager.start(name);
});
return await this.manager.getSessionInfo(name);
}
@Post('/start/')
@ApiOperation({
summary: 'Upsert and Start session',
description:
'Create session (if not exists) or update a config (if exists) and start it.',
deprecated: true,
})
async DEPRACATED_start(
@Body() request: SessionStartDeprecatedRequest,
): Promise<SessionDTO> {
const name = request.name;
if (this.manager.isRunning(name)) {
const msg = `Session '${name}' is already started.`;
throw new UnprocessableEntityException(msg);
}
return await this.withLock(name, async () => {
const config = request.config;
if (config) {
await this.manager.upsert(name, config);
}
return await this.manager.start(name);
});
}
@Post('/stop/')
@ApiOperation({
summary: 'Stop (and Logout if asked) session',
description: 'Stop session and Logout by default.',
deprecated: true,
})
async DEPRECATED_stop(
@Body() request: SessionStopDeprecatedRequest,
): Promise<void> {
const name = request.name;
if (request.logout) {
// Old API did remove the session complete
await this.withLock(name, async () => {
await this.manager.stop(name, true);
await this.manager.logout(name);
await this.manager.delete(name);
});
} else {
await this.withLock(name, async () => {
await this.manager.stop(name, false);
});
}
return;
}
@Post('/logout/')
@ApiOperation({
summary: 'Logout and Delete session.',
description: 'Stop, Logout and Delete session.',
deprecated: true,
})
async DEPRECATED_logout(
@Body() request: SessionLogoutDeprecatedRequest,
): Promise<void> {
const name = request.name;
await this.withLock(name, async () => {
await this.manager.stop(name, true);
await this.manager.logout(name);
await this.manager.delete(name);
});
return;
}
}
export { SessionController, SessionsController };
export { SessionsController };
+5 -2
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 { SessionManager } from '../core/abc/manager.abc';
import { WhatsappSession } from '../core/abc/session.abc';
@@ -18,11 +22,10 @@ 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) {}
+2 -2
View File
@@ -11,10 +11,10 @@ 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 version of the server', deprecated: true })
get(): WAHAEnvironment {
return VERSION;
}
+9 -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(',') : [];
}
+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 };
+33 -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,43 @@ export abstract class WhatsappSession {
// Run optimized version of Chrome
// References:
// https://github.com/pedroslopez/whatsapp-web.js/issues/1420
// https://github.com/wppconnect-team/wppconnect/issues/1326
// https://www.bannerbear.com/blog/ways-to-speed-up-puppeteer-screenshots/
return [
'--no-sandbox',
'--aggressive-cache-discard',
'--disable-accelerated-2d-canvas',
'--disable-application-cache',
'--disable-background-networking',
'--disable-cache',
'--disable-client-side-phishing-detection',
'--disable-setuid-sandbox',
'--disable-component-update',
'--disable-default-apps',
'--disable-popup-blocking',
'--disable-offer-store-unmasked-wallet-cards',
'--disable-speech-api',
'--hide-scrollbars',
'--mute-audio',
'--disable-extensions',
'--disable-dev-shm-usage',
'--disable-accelerated-2d-canvas',
'--no-first-run',
'--no-default-browser-check',
'--no-pings',
'--password-store=basic',
'--use-mock-keychain',
'--no-zygote',
'--single-process',
'--disable-extensions',
'--disable-gpu',
'--disable-offer-store-unmasked-wallet-cards',
'--disable-offline-load-stale-cache',
'--disable-popup-blocking',
'--disable-setuid-sandbox',
'--disable-speech-api',
'--disable-sync',
'--disable-translate',
'--disable-web-security',
'--disk-cache-size=0',
'--hide-scrollbars',
'--ignore-certificate-errors',
'--ignore-ssl-errors',
'--metrics-recording-only',
'--mute-audio',
'--no-default-browser-check',
'--no-first-run',
'--no-pings',
'--no-sandbox',
'--no-zygote',
'--password-store=basic',
'--safebrowsing-disable-auto-update',
'--single-process',
'--use-mock-keychain',
];
}
+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,
+32 -29
View File
@@ -1,17 +1,20 @@
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 } 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 +29,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 +51,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 +74,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 +100,6 @@ export const IMPORTS = [
export const CONTROLLERS = [
AuthController,
SessionsController,
SessionController,
ChattingController,
ChatsController,
ChannelsController,
@@ -111,9 +109,10 @@ export const CONTROLLERS = [
GroupsController,
PresenceController,
ScreenshotController,
VersionController,
HealthController,
PingController,
HealthController,
ServerController,
VersionController,
];
const PROVIDERS = [
{
@@ -132,8 +131,8 @@ const PROVIDERS = [
SwaggerConfigServiceCore,
WhatsappConfigService,
EngineConfigService,
ConsoleLogger,
WebsocketGatewayCore,
MediaLocalStorageConfig,
];
@Module({
@@ -142,7 +141,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;
+93 -61
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 {
@@ -443,8 +446,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 +538,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 +560,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 +590,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,11 +632,14 @@ 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]);
}
@@ -668,19 +666,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 +688,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: {
@@ -930,13 +927,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 +1007,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 +1257,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 +1278,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: contextInfo.participant,
body: body,
};
}
protected toWAContact(contact: Contact) {
contact.id = toCusFormat(contact.id);
// @ts-ignore
@@ -1464,11 +1480,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 +1583,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';
+3 -3
View File
@@ -1,4 +1,5 @@
import { UnprocessableEntityException } from '@nestjs/common/exceptions/unprocessable-entity.exception';
import { IMediaEngineProcessor } from '@waha/core/media/IMediaEngineProcessor';
import * as Buffer from 'buffer';
import { create, CreateConfig, Message, Whatsapp } from 'venom-bot';
@@ -21,7 +22,6 @@ import {
WAHASessionStatus,
} from '../../../structures/enums.dto';
import { WAMessage } from '../../../structures/responses.dto';
import { IEngineMediaProcessor } from '../../abc/media.abc';
import { WAHAInternalEvent, WhatsappSession } from '../../abc/session.abc';
import { NotImplementedByEngineError } from '../../exceptions';
import { QR } from '../../QR';
@@ -248,7 +248,7 @@ export class WhatsappSessionVenomCore extends WhatsappSession {
protected downloadMedia(message: Message) {
const processor = new EngineMediaProcessor(this);
return this.mediaManager.processMedia(processor, message);
return this.mediaManager.processMedia(processor, message, this.name);
}
private processIncomingMessage(message: Message) {
@@ -280,7 +280,7 @@ export class WhatsappSessionVenomCore extends WhatsappSession {
}
}
export class EngineMediaProcessor implements IEngineMediaProcessor<Message> {
export class EngineMediaProcessor implements IMediaEngineProcessor<Message> {
constructor(public session: WhatsappSessionVenomCore) {}
hasMedia(message: any): boolean {
+53 -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
@@ -234,8 +236,18 @@ export class WhatsappSessionWebJSCore extends WhatsappSession {
try {
this.whatsapp?.removeAllListeners();
this.startDelayedJob.cancel();
// It's possible that browser yet starting
await waitUntil(
async () => {
const result = !!this.whatsapp.pupBrowser;
this.logger.debug(`Browser is ready to be closed: ${result}`);
return result;
},
1_000,
10_000,
);
this.whatsapp?.destroy().catch((error) => {
this.logger.debug('Failed to destroy the client', error);
this.logger.warn(error, 'Failed to destroy the client');
});
} catch (error) {
this.logger.error(error);
@@ -341,10 +353,7 @@ export class WhatsappSessionWebJSCore extends WhatsappSession {
}
sendText(request: MessageTextRequest) {
const options = {
// It's fine to sent just ids instead of Contact object
mentions: request.mentions as unknown as string[],
};
const options = this.getMessageOptions(request);
return this.whatsapp.sendMessage(
this.ensureSuffix(request.chatId),
request.text,
@@ -371,11 +380,7 @@ export class WhatsappSessionWebJSCore extends WhatsappSession {
}
reply(request: MessageReplyRequest) {
const options = {
quotedMessageId: request.reply_to,
// It's fine to sent just ids instead of Contact object
mentions: request.mentions as unknown as string[],
};
const options = this.getMessageOptions(request);
return this.whatsapp.sendMessage(request.chatId, request.text, options);
}
@@ -395,7 +400,8 @@ export class WhatsappSessionWebJSCore extends WhatsappSession {
const location = new Location(request.latitude, request.longitude, {
name: request.title,
});
return this.whatsapp.sendMessage(request.chatId, location);
const options = this.getMessageOptions(request);
return this.whatsapp.sendMessage(request.chatId, location, options);
}
async sendSeen(request: SendSeenRequest) {
@@ -457,12 +463,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 +926,7 @@ export class WhatsappSessionWebJSCore extends WhatsappSession {
}
protected toWAMessage(message: Message): Promise<WAMessage> {
const replyTo = this.extractReplyTo(message);
// @ts-ignore
return Promise.resolve({
id: message.id._serialized,
@@ -940,10 +947,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 +981,23 @@ export class WhatsappSessionWebJSCore extends WhatsappSession {
protected downloadMedia(message: Message) {
const processor = new EngineMediaProcessor();
return this.mediaManager.processMedia(processor, message);
return this.mediaManager.processMedia(processor, message, this.name);
}
protected getMessageOptions(request: any): any {
let mentions = request.mentions;
mentions = mentions ? mentions.map(this.ensureSuffix) : undefined;
const quotedMessageId = request.reply_to;
return {
mentions: mentions,
quotedMessageId: quotedMessageId,
};
}
}
export class EngineMediaProcessor implements IEngineMediaProcessor<Message> {
export class EngineMediaProcessor implements IMediaEngineProcessor<Message> {
hasMedia(message: Message): boolean {
if (!message.hasMedia) {
return false;
+93 -67
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,11 +18,9 @@ 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';
@@ -33,9 +31,8 @@ import { WhatsappSessionVenomCore } from './engines/venom/session.venom.core';
import { WhatsappSessionWebJSCore } from './engines/webjs/session.webjs.core';
import { DOCS_URL } from './exceptions';
import { getProxyConfig } from './helpers.proxy';
import { CoreMediaManager, MediaStorageCore } from './media.core';
import { MediaManagerCore } from './media/MediaManagerCore';
import { LocalSessionAuthRepository } from './storage/LocalSessionAuthRepository';
import { LocalSessionConfigRepository } from './storage/LocalSessionConfigRepository';
import { LocalStoreCore } from './storage/LocalStoreCore';
import { WebhookConductorCore } from './webhooks.core';
@@ -49,7 +46,13 @@ export class OnlyDefaultSessionIsAllowed extends UnprocessableEntityException {
@Injectable()
export class SessionManagerCore extends SessionManager {
private session?: WhatsappSession;
SESSION_STOP_TIMEOUT = 3000;
// session - exists and running (or failed or smth)
// null - stopped
// undefined - removed
private session: WhatsappSession | undefined | null;
private sessionConfig?: SessionConfig;
DEFAULT = 'default';
// @ts-ignore
@@ -63,20 +66,20 @@ export class SessionManagerCore extends SessionManager {
) {
super();
this.events = new EventEmitter();
this.session = null;
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);
});
}
@@ -102,32 +105,48 @@ 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 !== undefined;
}
isRunning(name: string): boolean {
this.onlyDefault(name);
return !!this.session;
}
async upsert(name: string, config?: SessionConfig): Promise<void> {
this.onlyDefault(name);
this.sessionConfig = config;
}
async start(name: string): Promise<SessionDTO> {
this.onlyDefault(name);
if (this.session) {
throw new UnprocessableEntityException(
`Session '${this.DEFAULT}' is already started.`,
);
}
const name = request.name;
this.log.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);
const logger = this.log.logger.child({
session: name,
sessionRunTimestamp: Date.now(),
});
logger.level = getPinoLogLevel(this.sessionConfig?.debug);
const loggerBuilder: LoggerBuilder = logger;
const webhook = new this.WebhookConductorClass(loggerBuilder);
const proxyConfig = this.getProxyConfig(request);
const proxyConfig = this.getProxyConfig();
const sessionConfig: SessionParams = {
name,
mediaManager,
@@ -135,7 +154,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 +162,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 +173,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 +181,46 @@ export class SessionManagerCore extends SessionManager {
};
}
async stop(name: string, silent: boolean): Promise<void> {
this.onlyDefault(name);
if (!this.isRunning(name)) {
this.log.debug(`Session is not running.`, { session: name });
return;
}
this.log.info(`Stopping session...`, { session: name });
try {
const session = this.getSession(name);
await session.stop();
} catch (err) {
this.log.warn(`Error while stopping session '${name}'`);
if (!silent) {
throw err;
}
}
this.log.info(`Session has been stopped.`, { session: name });
this.session = null;
await sleep(this.SESSION_STOP_TIMEOUT);
}
async logout(name: string): Promise<void> {
this.onlyDefault(name);
await this.sessionAuthRepository.clean(name);
}
async delete(name: string): Promise<void> {
this.onlyDefault(name);
this.session = undefined;
this.sessionConfig = undefined;
}
/**
* Combine per session and global webhooks
*/
private getWebhooks(request: SessionStartRequest) {
private getWebhooks() {
let webhooks: WebhookConfig[] = [];
if (request.config?.webhooks) {
webhooks = webhooks.concat(request.config.webhooks);
if (this.sessionConfig?.webhooks) {
webhooks = webhooks.concat(this.sessionConfig.webhooks);
}
const globalWebhookConfig = this.config.getWebhookConfig();
if (globalWebhookConfig) {
@@ -179,71 +232,44 @@ 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 };
return getProxyConfig(this.config, sessions, this.DEFAULT);
}
getSession(name: string): WhatsappSession {
this.onlyDefault(name);
const session = this.session;
if (!session) {
if (session === undefined) {
throw new NotFoundException(
`We didn't find a session with name '${name}'. Please start it first by using POST /sessions/start request`,
`We didn't find a session with name '${name}'.
Please start it first by using POST /sessions/${name}/start request`,
);
}
return session;
}
async getSessions(all: boolean): Promise<SessionInfo[]> {
if (!this.session) {
if (!all) {
return [];
}
if (this.session === null && all) {
return [
{
name: this.DEFAULT,
status: WAHASessionStatus.STOPPED,
config: undefined,
config: this.sessionConfig,
me: null,
},
];
}
if (this.session === undefined && all) {
return [];
}
const me = this.session.getSessionMeInfo();
// Get engine info
let engineInfo = {};
-47
View File
@@ -1,47 +0,0 @@
import { WAMedia } from '../structures/responses.dto';
import {
IEngineMediaProcessor,
MediaManager,
MediaStorage,
} from './abc/media.abc';
import { DOCS_URL } from './exceptions';
export class MediaStorageCore implements MediaStorage {
async save(
messageId: string,
mimetype: string,
buffer: Buffer,
): Promise<string> {
return Promise.resolve(
`Media attachment's available only in WAHA Plus version. ${DOCS_URL}`,
);
}
}
export class CoreMediaManager implements MediaManager {
constructor(
private storage: MediaStorage,
private mimetypes: string[],
) {}
async processMedia<Message>(
processor: IEngineMediaProcessor<Message>,
message: Message,
): Promise<Message> {
if (!processor.hasMedia(message)) {
return message;
}
const mimetype = '';
const filename = processor.getFilename(message);
const url = await this.storage.save('', mimetype, Buffer.from(''));
const media: WAMedia = {
mimetype: mimetype,
filename: filename,
url: url,
};
// @ts-ignore
message.media = media;
return message;
}
}
+17
View File
@@ -0,0 +1,17 @@
/**
* Engine specific media processor
* Knows how to extract necessary attributes and fetch the data from Message
*/
interface IMediaEngineProcessor<Message> {
hasMedia(message: Message): boolean;
getFilename(message: Message): string | null;
getMimetype(message: Message): string;
getMessageId(message: Message): string;
getMediaBuffer(message: Message): Promise<Buffer | null>;
}
export { IMediaEngineProcessor };
+15
View File
@@ -0,0 +1,15 @@
import { IMediaEngineProcessor } from '@waha/core/media/IMediaEngineProcessor';
/**
* General interface for MediaManager - one that handles the logic
* and manipulates MediaStorage and MediaEngineProcessor
*/
interface IMediaManager {
processMedia<Message>(
processor: IMediaEngineProcessor<Message>,
message: Message,
session: string,
): Promise<Message>;
}
export { IMediaManager };
+42
View File
@@ -0,0 +1,42 @@
import { S3MediaData } from '@waha/structures/media.s3.dto';
/**
* Handles saving data to the physical storage
*/
interface Message {
// false_111111111@c.us_AAAAAAAAAAAAAA
id: string;
}
interface File {
extension: string;
filename?: string;
}
export interface MediaData {
session: string;
message: Message;
file: File;
}
/**
* For stored media data
*/
export interface MediaStorageData {
url: string;
s3?: S3MediaData;
}
abstract class IMediaStorage {
abstract init(): Promise<void>;
abstract save(buffer: Buffer, data: MediaData): Promise<boolean>;
abstract exists(data: MediaData): Promise<boolean>;
abstract getStorageData(data: MediaData): Promise<MediaStorageData>;
abstract purge(): Promise<void>;
}
export { IMediaStorage };
+44
View File
@@ -0,0 +1,44 @@
import { IMediaEngineProcessor } from '@waha/core/media/IMediaEngineProcessor';
import { IMediaStorage, MediaData } from '@waha/core/media/IMediaStorage';
import { WAMedia } from '@waha/structures/media.dto';
import { IMediaManager } from './IMediaManager';
export class MediaManagerCore implements IMediaManager {
constructor(
private storage: IMediaStorage,
private mimetypes: string[],
) {}
async processMedia<Message>(
processor: IMediaEngineProcessor<Message>,
message: Message,
session: string,
): Promise<Message> {
if (!processor.hasMedia(message)) {
return message;
}
const mimetype = '';
const mediaData: MediaData = {
session: session,
message: {
id: '',
},
file: {
extension: '',
},
};
const { url } = await this.storage.getStorageData(mediaData);
const filename = processor.getFilename(message);
const media: WAMedia = {
mimetype: mimetype,
filename: filename,
url: url,
};
// @ts-ignore
message.media = media;
return message;
}
}
+25
View File
@@ -0,0 +1,25 @@
import { DOCS_URL } from '@waha/core/exceptions';
import { IMediaStorage, MediaData } from '@waha/core/media/IMediaStorage';
export class MediaNoopStorage implements IMediaStorage {
async init() {
return;
}
async save(buffer: Buffer, data: MediaData): Promise<boolean> {
return Promise.resolve(true);
}
async exists(data: MediaData): Promise<boolean> {
return false;
}
async getStorageData(data: MediaData) {
const url = `Media attachment's available only in WAHA Plus version. ${DOCS_URL}`;
return { url };
}
async purge() {
return;
}
}
@@ -0,0 +1,21 @@
import { Injectable } from '@nestjs/common';
import { WhatsappConfigService } from '@waha/config.service';
@Injectable()
export class MediaLocalStorageConfig {
public filesUri = '/api/files';
constructor(private config: WhatsappConfigService) {}
get filesURL(): string {
return `${this.config.schema}://${this.config.hostname}:${this.config.port}${this.filesUri}/`;
}
get filesFolder(): string {
return this.config.get('WHATSAPP_FILES_FOLDER', '/tmp/whatsapp-files');
}
get filesLifetime(): number {
return parseInt(this.config.get('WHATSAPP_FILES_LIFETIME', '180'));
}
}
@@ -2,6 +2,4 @@ export abstract class ISessionAuthRepository {
abstract init(sessionName?: string): Promise<void>;
abstract clean(sessionName: string): Promise<void>;
abstract getAll(): Promise<string[]>;
}
@@ -8,5 +8,9 @@ export abstract class ISessionConfigRepository {
abstract get(sessionName: string): Promise<SessionConfig | null>;
abstract exists(sessionName: string): Promise<boolean>;
abstract delete(sessionName: string): Promise<void>;
abstract getAll(): Promise<string[]>;
}
+16 -13
View File
@@ -1,7 +1,10 @@
import * as fs from 'fs/promises';
// eslint-disable-next-line @typescript-eslint/no-var-requires
const fs = require('fs-extra');
import { ISessionAuthRepository } from './ISessionAuthRepository';
import { LocalStore } from './LocalStore';
// Keep all waha related files, ".waha.session.*"
const KEEP_FILES = /^\.waha\.session\..*$/;
export class LocalSessionAuthRepository extends ISessionAuthRepository {
private store: LocalStore;
@@ -16,17 +19,17 @@ export class LocalSessionAuthRepository extends ISessionAuthRepository {
}
async clean(sessionName: string) {
const folder = this.store.getSessionDirectory(sessionName);
await fs.rm(folder, { recursive: true });
}
async getAll(): Promise<string[]> {
await this.init();
const content = await fs.readdir(this.store.getEngineDirectory(), {
withFileTypes: true,
});
return content
.filter((dirent) => dirent.isDirectory())
.map((dirent) => dirent.name);
// Remove all files and directories recursively, but keep waha files
const sessionDirectory = this.store.getSessionDirectory(sessionName);
// Check it exists and it's directory
const exists = await fs.pathExists(sessionDirectory);
if (!exists) {
return;
}
const files = await fs.readdir(sessionDirectory);
const filesToRemove = files.filter((file) => !file.match(KEEP_FILES));
for (const file of filesToRemove) {
await fs.remove(`${sessionDirectory}/${file}`);
}
}
}
@@ -1,5 +1,7 @@
import * as fs from 'fs/promises';
import * as path from 'path';
// eslint-disable-next-line @typescript-eslint/no-var-requires
const fs = require('fs-extra');
import { fileExists } from '@waha/utils/files';
import { SessionConfig } from '../../structures/sessions.dto';
import { ISessionConfigRepository } from './ISessionConfigRepository';
@@ -14,19 +16,15 @@ export class LocalSessionConfigRepository extends ISessionConfigRepository {
this.store = store;
}
private async fileExists(filepath: string) {
try {
await fs.access(filepath, fs.constants.F_OK);
} catch (error) {
return false;
}
return true;
async exists(sessionName: string): Promise<boolean> {
const filepath = this.getFilePath(sessionName);
return await fileExists(filepath);
}
async get(sessionName: string): Promise<SessionConfig | null> {
const filepath = this.getFilePath(sessionName);
// Check file exists
if (!(await this.fileExists(filepath))) {
if (!(await fileExists(filepath))) {
return null;
}
@@ -56,10 +54,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',
);
+15 -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,7 @@ async function bootstrap() {
logger.info(VERSION, 'Environment');
}
bootstrap();
bootstrap().catch((error) => {
logger.error(error, `Failed to start WAHA: ${error}`);
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.
+9
View File
@@ -0,0 +1,9 @@
import { ApiParam } from '@nestjs/swagger';
export const ChatIdApiParam = ApiParam({
name: 'chatId',
required: true,
type: 'string',
description: 'Chat ID',
example: '123456789@c.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 <code>name</code>',
});
/**
* 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.
+42 -5
View File
@@ -15,7 +15,7 @@ import {
VoiceBinaryFile,
VoiceRemoteFile,
} from './files.dto';
import { ChatIdProperty } from './properties.dto';
import { ChatIdProperty, ReplyToProperty } from './properties.dto';
/**
* Queries
@@ -72,6 +72,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)',
})
@@ -144,6 +145,9 @@ export class MessageTextRequest extends ChatRequest {
text = 'Hi there!';
@ApiHideProperty()
mentions?: string[];
@ReplyToProperty()
reply_to?: string;
}
export class EditMessageRequest {
@@ -155,16 +159,26 @@ export class EditMessageRequest {
export class MessageReplyRequest extends MessageTextRequest {
text = 'Reply text';
@ApiProperty({
example: 'false_11111111111@c.us_AAAAAAAAAAAAAAAAAAAA',
})
reply_to: string;
}
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)
@@ -180,10 +194,16 @@ class FileRequest extends ChatRequest {
export class MessageImageRequest extends FileRequest {
caption: string;
@ReplyToProperty()
reply_to?: string;
}
export class MessageFileRequest extends FileRequest {
caption: string;
@ReplyToProperty()
reply_to?: string;
}
@ApiExtraModels(VoiceBinaryFile, VoiceRemoteFile)
@@ -195,6 +215,9 @@ export class MessageVoiceRequest extends ChatRequest {
],
})
file: VoiceBinaryFile | VoiceRemoteFile;
@ReplyToProperty()
reply_to?: string;
}
@ApiExtraModels(VideoRemoteFile, VideoBinaryFile)
@@ -208,6 +231,13 @@ export class MessageVideoRequest extends ChatRequest {
file: VideoRemoteFile | VideoBinaryFile;
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 +286,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 {
+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;
}
+98 -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;
}
@@ -42,6 +61,7 @@ export class NowebStoreConfig {
description:
'Enable or disable the store for contacts, chats, and messages.',
})
@IsBoolean()
enabled: boolean = false;
@ApiProperty({
@@ -50,22 +70,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 +125,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 +154,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;
}
+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,
})
+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.2',
engine: getEngineName(),
tier: getWAHAVersion(),
browser: getBrowserExecutablePath(),
+1408 -55
View File
File diff suppressed because it is too large. Load diff