Compare commits
112
Commits
2024.9.6
...
2024.10.11
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f1bb445593 | ||
|
|
7f777f338f | ||
|
|
4449dfdbd6 | ||
|
|
6226e65e97 | ||
|
|
be1783c029 | ||
|
|
83a002ae5b | ||
|
|
f0fcb2b329 | ||
|
|
5ff695a5b3 | ||
|
|
43fe1f076e | ||
|
|
1afd7e042c | ||
|
|
ba6474f97e | ||
|
|
d8eed114aa | ||
|
|
e61ad6f79c | ||
|
|
9d27616003 | ||
|
|
0f2641909f | ||
|
|
9ca96acda2 | ||
|
|
77db6c5e98 | ||
|
|
c4aa9804d2 | ||
|
|
eb7bfe3a62 | ||
|
|
01fd7a8d56 | ||
|
|
81fb93e5d6 | ||
|
|
64716ec6e6 | ||
|
|
e0f787be32 | ||
|
|
138da79599 | ||
|
|
b8b290c2a5 | ||
|
|
f35dcc0ec6 | ||
|
|
62704e261a | ||
|
|
f16ace6169 | ||
|
|
35628aa93b | ||
|
|
64bf42dc1c | ||
|
|
55fc6a4282 | ||
|
|
1a59300cd7 | ||
|
|
9c7d6d835e | ||
|
|
2a20242654 | ||
|
|
171f9de073 | ||
|
|
3b4d896bcd | ||
|
|
4b602333bc | ||
|
|
347cd193dd | ||
|
|
964d8dde9b | ||
|
|
bf794ce67f | ||
|
|
51dc9ea8c1 | ||
|
|
791a1fd475 | ||
|
|
244eb5f692 | ||
|
|
003bc33c25 | ||
|
|
b488b9f1df | ||
|
|
ff4f0a59af | ||
|
|
10e2c4bbd4 | ||
|
|
18536330ce | ||
|
|
e894069d4e | ||
|
|
5be2ca0deb | ||
|
|
83e2405151 | ||
|
|
220c0dfb01 | ||
|
|
5474f1ec0e | ||
|
|
dbd01abde8 | ||
|
|
83bbab0969 | ||
|
|
cc6808a6cd | ||
|
|
191fd89cfb | ||
|
|
42e5b46365 | ||
|
|
f444ce86f9 | ||
|
|
ce05b3d087 | ||
|
|
ab4dd28c51 | ||
|
|
9e23b287ff | ||
|
|
135c1ab10b | ||
|
|
de680c93f6 | ||
|
|
57ac57824f | ||
|
|
38df64c191 | ||
|
|
575969f938 | ||
|
|
42c388e5c9 | ||
|
|
6346433d45 | ||
|
|
44e63e00c3 | ||
|
|
8648b44583 | ||
|
|
086d540ee2 | ||
|
|
adf6836ee5 | ||
|
|
62f4406c51 | ||
|
|
e4c201e105 | ||
|
|
b6b0b1e7b3 | ||
|
|
9d857d015f | ||
|
|
2055c595bb | ||
|
|
00e33ceac7 | ||
|
|
77ef1ecebf | ||
|
|
57672a2a9c | ||
|
|
0ae594b198 | ||
|
|
14a7e1c72d | ||
|
|
e570cbef12 | ||
|
|
c6d732a723 | ||
|
|
9cb0131a37 | ||
|
|
d72b665cfe | ||
|
|
d6b87585f7 | ||
|
|
2e01692e92 | ||
|
|
78ad79c08f | ||
|
|
725446a022 | ||
|
|
3bd7594b7f | ||
|
|
2bd314732a | ||
|
|
f4d7776bca | ||
|
|
80e13beb2b | ||
|
|
5c37826710 | ||
|
|
693a288839 | ||
|
|
ecc0e1b60d | ||
|
|
1d60a4ca42 | ||
|
|
7bf343d071 | ||
|
|
0430b0ed53 | ||
|
|
9147dd9925 | ||
|
|
b6e7fac619 | ||
|
|
c1d15e570d | ||
|
|
72b067c8a7 | ||
|
|
94df9a681d | ||
|
|
1d30bb7e8e | ||
|
|
0779e326d7 | ||
|
|
3ad5b67fef | ||
|
|
d747bf72fd | ||
|
|
8474cb5789 | ||
|
|
83650c7dc9 |
No files matched your search
+176
@@ -0,0 +1,176 @@
|
||||
# WAHA Configuration
|
||||
# https://waha.devlike.pro/docs/how-to/config/
|
||||
#
|
||||
# Common
|
||||
#
|
||||
# If you know the right URL, you can use it here.
|
||||
# Use any port in WHATSAPP_API_PORT and domain in WHATSAPP_API_HOSTNAME
|
||||
# https://waha.devlike.pro/docs/how-to/config/#common
|
||||
#WHATSAPP_API_SCHEMA=http
|
||||
#WHATSAPP_API_PORT=3000
|
||||
#WHATSAPP_API_HOSTNAME=localhost
|
||||
#WAHA_BASE_URL=http://localhost:3000
|
||||
#
|
||||
# Set your timezone to see the right time on screenshots
|
||||
# Find your timezone name in the list
|
||||
# https://en.wikipedia.org/wiki/List_of_tz_database_time_zones
|
||||
#TZ=Europe/Dublin
|
||||
|
||||
#
|
||||
# Security
|
||||
#
|
||||
# https://waha.devlike.pro/docs/how-to/security/
|
||||
#
|
||||
WHATSAPP_API_KEY=321
|
||||
WAHA_DASHBOARD_USERNAME=waha
|
||||
WAHA_DASHBOARD_PASSWORD=waha
|
||||
#WHATSAPP_SWAGGER_USERNAME=admin
|
||||
#WHATSAPP_SWAGGER_PASSWORD=admin
|
||||
|
||||
#
|
||||
# Dashboard
|
||||
#
|
||||
# https://waha.devlike.pro/docs/how-to/waha-dashboard/#configuration
|
||||
# Enabled by default
|
||||
#WAHA_DASHBOARD_ENABLED=true
|
||||
#WAHA_DASHBOARD_USERNAME=waha
|
||||
#WAHA_DASHBOARD_PASSWORD=waha
|
||||
|
||||
#
|
||||
# Logging
|
||||
# https://waha.devlike.pro/docs/how-to/observability/#logging
|
||||
#
|
||||
# Set log format to JSON if you consume logs with log management systems
|
||||
WAHA_LOG_FORMAT=JSON
|
||||
WAHA_LOG_LEVEL=info
|
||||
# WAHA_LOG_FORMAT=PRETTY
|
||||
# If you want to see more logs, you can set the log level to debug
|
||||
#WAHA_LOG_LEVEL=debug
|
||||
# DEBUG=1 # shortcut for setting log level to debug
|
||||
|
||||
#
|
||||
# Engine
|
||||
# https://waha.devlike.pro/docs/how-to/engines/
|
||||
#
|
||||
# Choose the right engine for your needs (WEBJS by default)
|
||||
WHATSAPP_DEFAULT_ENGINE=WEBJS
|
||||
# Use NOWEB engine for the fastest performance
|
||||
#WHATSAPP_DEFAULT_ENGINE=NOWEB
|
||||
|
||||
# Worker
|
||||
# WAHA_WORKER_ID=waha1
|
||||
|
||||
#
|
||||
# Sessions
|
||||
# https://waha.devlike.pro/docs/how-to/config/#sessions
|
||||
#
|
||||
# Do not print QR codes in logs
|
||||
WAHA_PRINT_QR=False
|
||||
#
|
||||
# OR you can specify sessions by name, if you have handful amount of sessions
|
||||
#WHATSAPP_START_SESSION=session1,session2
|
||||
#
|
||||
# Restart all sessions when container starts
|
||||
# !!! Do not use it with multiple workers against one database !!!
|
||||
# WHATSAPP_RESTART_ALL_SESSIONS=True
|
||||
|
||||
#
|
||||
# Swagger
|
||||
# https://waha.devlike.pro/docs/how-to/swagger/
|
||||
#
|
||||
WHATSAPP_SWAGGER_USERNAME=admin
|
||||
WHATSAPP_SWAGGER_PASSWORD=admin
|
||||
# Disable Swagger
|
||||
#WHATSAPP_SWAGGER_ENABLED=false
|
||||
# Enable advanced Swagger configuration
|
||||
#WHATSAPP_SWAGGER_CONFIG_ADVANCED=true
|
||||
#
|
||||
# Swagger White Label customization
|
||||
# https://waha.devlike.pro/docs/how-to/swagger/#white-label
|
||||
#WHATSAPP_SWAGGER_TITLE=WAHA API
|
||||
#
|
||||
|
||||
#
|
||||
# Global Proxy Settings
|
||||
# https://waha.devlike.pro/docs/how-to/config/#global-proxy-configuration
|
||||
#
|
||||
# You can also set proxy settings for each session later when starting it
|
||||
# https://waha.devlike.pro/docs/how-to/sessions/#configure-proxy
|
||||
#WHATSAPP_PROXY_SERVER=proxy.example.com:3128
|
||||
#WHATSAPP_PROXY_SERVER_USERNAME=user
|
||||
#WHATSAPP_PROXY_SERVER_PASSWORD=pass
|
||||
#
|
||||
|
||||
#
|
||||
# HTTPS Configuration
|
||||
# https://waha.devlike.pro/docs/how-to/config/#https
|
||||
#
|
||||
# Consider using certbot for HTTPS
|
||||
# https://waha.devlike.pro/blog/setting-up-https-for-waha/#lets-encrypt-certbot--waha
|
||||
#
|
||||
#WAHA_HTTPS_ENABLED=true
|
||||
#WAHA_HTTPS_PATH_KEY=/etc/letsencrypt/live/waha.example.pro/privkey.pem
|
||||
#WAHA_HTTPS_PATH_CERT=/etc/letsencrypt/live/waha.example.pro/cert.pem \
|
||||
#WAHA_HTTPS_PATH_CA=/etc/letsencrypt/live/waha.example.pro/chain.pem
|
||||
#WHATSAPP_API_SCHEMA=https
|
||||
#WHATSAPP_API_PORT=3000
|
||||
#WHATSAPP_API_HOSTNAME=waha.example.pro
|
||||
#
|
||||
|
||||
#
|
||||
# Media Configuration
|
||||
#
|
||||
# https://waha.devlike.pro/docs/how-to/config/#files
|
||||
#
|
||||
# Disable media (images, videos, files) download for incoming messages
|
||||
#WHATSAPP_DOWNLOAD_MEDIA=false
|
||||
#
|
||||
# Download only specific media types
|
||||
#WHATSAPP_FILES_MIMETYPES=image/jpeg,image/png
|
||||
#
|
||||
|
||||
# Media - Local Storage
|
||||
# https://waha.devlike.pro/docs/how-to/storages/#media---local
|
||||
#
|
||||
# Set to 0 to keep media files forever
|
||||
# https://waha.devlike.pro/docs/how-to/storages/#save-media-files-between-the-container-restarts
|
||||
WAHA_MEDIA_STORAGE=LOCAL
|
||||
WHATSAPP_FILES_LIFETIME=0
|
||||
WHATSAPP_FILES_FOLDER=/app/.media
|
||||
# Keep media files for 180 seconds (3 minutes)
|
||||
#WHATSAPP_FILES_LIFETIME=180
|
||||
#
|
||||
|
||||
# Media - S3 Storage
|
||||
# https://waha.devlike.pro/docs/how-to/storages/#media---s3
|
||||
#WAHA_MEDIA_STORAGE=S3
|
||||
#WAHA_S3_REGION=eu-west-2
|
||||
#WAHA_S3_BUCKET=waha
|
||||
#WAHA_S3_ACCESS_KEY_ID=minioadmin
|
||||
#WAHA_S3_SECRET_ACCESS_KEY=minioadmin
|
||||
#WAHA_S3_ENDPOINT=http://minio:9000 # Not required if you're using AWS S3
|
||||
#WAHA_S3_FORCE_PATH_STYLE=True # Required for Minio
|
||||
#WAHA_S3_PROXY_FILES=True # Required for docker-compose setup
|
||||
|
||||
#
|
||||
# Global Webhooks
|
||||
#
|
||||
# https://waha.devlike.pro/docs/how-to/config/#webhooks
|
||||
# https://waha.devlike.pro/docs/how-to/webhooks/
|
||||
#WHATSAPP_HOOK_URL=https://webhook.site/11111111-1111-1111-1111-11111111
|
||||
#WHATSAPP_HOOK_EVENTS=session.status,message,message.reaction
|
||||
#
|
||||
|
||||
#
|
||||
# Local Configuration
|
||||
#
|
||||
# WAHA_LOCAL_STORE_BASE_DIR=/app/sessions
|
||||
# Remember to map the volume to the host machine to the right direction in "volumes" field in docker-compose.yml
|
||||
# volumes:
|
||||
# - './.sessions:/app/sessions'
|
||||
|
||||
#
|
||||
# MongoDB Configuration
|
||||
# https://waha.devlike.pro/docs/how-to/storages/#sessions---mongodb
|
||||
#WHATSAPP_SESSIONS_MONGO_URL=mongodb://mongodb:27017
|
||||
#
|
||||
@@ -6,25 +6,28 @@ labels: bug
|
||||
assignees: ''
|
||||
---
|
||||
|
||||
**Describe the bug**
|
||||
### Describe the bug
|
||||
|
||||
> A clear and concise description of what the bug is. Feel free to remove
|
||||
> sections that you don't feel to make the text shorter!
|
||||
A clear and concise description of what the bug is. Feel free to remove sections
|
||||
that you don't feel to make the text shorter!
|
||||
|
||||
**Version**
|
||||
### Version
|
||||
|
||||
Get the WAHA version by calling `GET /api/version`
|
||||
|
||||
```json
|
||||
{
|
||||
"version": "2023.11.1",
|
||||
"engine": "NOWEB",
|
||||
"tier": "PLUS",
|
||||
"browser": "/usr/bin/google-chrome-stable"
|
||||
"version": "YYYY.MM.DD",
|
||||
"engine": "ENGINE",
|
||||
"tier": "TIER"
|
||||
}
|
||||
```
|
||||
|
||||
> You can get the WAHA version by calling `GET /api/version` Try to update to
|
||||
> [the latest version](https://github.com/devlikeapro/waha/releases) before
|
||||
> creating an issue!
|
||||
Try to update to
|
||||
[the latest version](https://github.com/devlikeapro/waha/releases) before
|
||||
creating an issue!
|
||||
|
||||
## Steps
|
||||
|
||||
**To Reproduce** Steps to reproduce the behavior:
|
||||
|
||||
@@ -33,29 +36,23 @@ assignees: ''
|
||||
3. Scroll down to '....'
|
||||
4. See error
|
||||
|
||||
**Expected behavior**
|
||||
### Expected behavior
|
||||
|
||||
> A clear and concise description of what you expected to happen.
|
||||
A clear and concise description of what you expected to happen.
|
||||
|
||||
**Engine** I'm using **WEBJS** engine.
|
||||
### Requests - Responses
|
||||
|
||||
> If you set `WHATSAPP_DEFAULT_ENGINE` explicitly to
|
||||
> [one of the engine](https://waha.devlike.pro/docs/how-to/engines/) - please
|
||||
> provide that information here and in the issue title.
|
||||
Please attach all sent requests, responses that you sent.
|
||||
|
||||
**Requests - Responses**
|
||||
### Docker Logs
|
||||
|
||||
> Please attach all sent requests, responses that you sent.
|
||||
Collect and attach related docker logs if you have any
|
||||
https://waha.devlike.pro/docs/how-to/deploy/#viewing-the-logs
|
||||
|
||||
**Docker Logs**
|
||||
### Screenshots
|
||||
|
||||
> Collect and attach related docker logs if you have any
|
||||
> https://waha.devlike.pro/docs/how-to/deploy/#viewing-the-logs
|
||||
If applicable, add screenshots to help explain your problem.
|
||||
|
||||
**Screenshots**
|
||||
### Additional context
|
||||
|
||||
> If applicable, add screenshots to help explain your problem.
|
||||
|
||||
**Additional context**
|
||||
|
||||
> Add any other context about the problem here.
|
||||
Add any other context about the problem here.
|
||||
@@ -9,16 +9,16 @@ assignees: ''
|
||||
**Is your feature request related to a problem? Please describe.** I'm always
|
||||
frustrated when [...]
|
||||
|
||||
> A clear and concise description of what the problem is. Ex.
|
||||
A clear and concise description of what the problem is. Ex.
|
||||
|
||||
**Describe the solution you'd like**
|
||||
|
||||
> A clear and concise description of what you want to happen.
|
||||
A clear and concise description of what you want to happen.
|
||||
|
||||
**Describe alternatives you've considered**
|
||||
|
||||
> A clear and concise description of any alternative solutions or features
|
||||
> you've considered.
|
||||
A clear and concise description of any alternative solutions or features you've
|
||||
considered.
|
||||
|
||||
**Additional context** Add any other context or screenshots about the feature
|
||||
request here.
|
||||
@@ -0,0 +1,11 @@
|
||||
---
|
||||
name: Question
|
||||
about: I have a question about the project or feature
|
||||
title: '[Question] '
|
||||
labels: 'question'
|
||||
assignees: ''
|
||||
---
|
||||
|
||||
👉 Kindly use
|
||||
[GitHub Discussions](https://github.com/devlikeapro/waha/discussions) to ask
|
||||
questions!
|
||||
@@ -1,3 +1,4 @@
|
||||
.env
|
||||
*.heapsnapshot
|
||||
.yarn
|
||||
example.ts
|
||||
@@ -15,6 +16,7 @@ test.js
|
||||
.wwebjs_cache
|
||||
src/dashboard
|
||||
.eslintcache
|
||||
__pycache__
|
||||
|
||||
# compiled output
|
||||
/dist
|
||||
|
||||
@@ -72,4 +72,5 @@ repos:
|
||||
entrypoint.sh|
|
||||
.pre-commit-config.yaml|
|
||||
^src/core/engines/webjs/session.webjs.core.ts|
|
||||
^src/core/engines/webjs/_lodash.js|
|
||||
)$
|
||||
+8
-4
@@ -26,7 +26,7 @@ RUN yarn build && find ./dist -name "*.d.ts" -delete
|
||||
FROM node:${NODE_VERSION} AS dashboard
|
||||
|
||||
# Download WAHA Dashboard
|
||||
ENV WAHA_DASHBOARD_SHA 92dc0b84de69d8c126ecf6fc6bb469d8c7fa291f
|
||||
ENV WAHA_DASHBOARD_SHA e6a72c5de2effc97d83e997cb1f361851604bae8
|
||||
RUN \
|
||||
wget https://github.com/devlikeapro/dashboard/archive/${WAHA_DASHBOARD_SHA}.zip \
|
||||
&& unzip ${WAHA_DASHBOARD_SHA}.zip -d /tmp/dashboard \
|
||||
@@ -50,8 +50,12 @@ RUN echo "USE_BROWSER=$USE_BROWSER"
|
||||
# Install ffmpeg to generate previews for videos
|
||||
RUN apt-get update && apt-get install -y ffmpeg --no-install-recommends && rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Install zip and unzip
|
||||
RUN apt-get update && apt-get install -y zip unzip --no-install-recommends && rm -rf /var/lib/apt/lists/*
|
||||
# Install zip and unzip - either for chromium or chrome
|
||||
RUN if [ "$USE_BROWSER" = "chromium" ] || [ "$USE_BROWSER" = "chrome" ]; then \
|
||||
apt-get update \
|
||||
&& apt-get install -y zip unzip \
|
||||
&& rm -rf /var/lib/apt/lists/*; \
|
||||
fi
|
||||
|
||||
# Install fonts if using either chromium or chrome
|
||||
RUN if [ "$USE_BROWSER" = "chromium" ] || [ "$USE_BROWSER" = "chrome" ]; then \
|
||||
@@ -73,7 +77,7 @@ RUN if [ "$USE_BROWSER" = "chromium" ]; then \
|
||||
# Install Chrome
|
||||
# Available versions:
|
||||
# https://www.ubuntuupdates.org/package/google_chrome/stable/main/base/google-chrome-stable
|
||||
ARG CHROME_VERSION="126.0.6478.182-1"
|
||||
ARG CHROME_VERSION="130.0.6723.69-1"
|
||||
RUN if [ "$USE_BROWSER" = "chrome" ]; then \
|
||||
wget --no-verbose -O /tmp/chrome.deb https://dl.google.com/linux/chrome/deb/pool/main/g/google-chrome-stable/google-chrome-stable_${CHROME_VERSION}_amd64.deb \
|
||||
&& apt-get update \
|
||||
|
||||
@@ -32,10 +32,10 @@ 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-2
|
||||
. ${NVM_DIR}/nvm.sh && nvm exec yarn up @adiwajshing/baileys@github:devlikeapro/Baileys#fork-master-channels-678
|
||||
|
||||
up-webjs:
|
||||
. ${NVM_DIR}/nvm.sh && nvm exec yarn up whatsapp-web.js@github:devlikeapro/whatsapp-web.js#fork-webpack-exodus-channels
|
||||
. ${NVM_DIR}/nvm.sh && nvm exec yarn up whatsapp-web.js@github:devlikeapro/whatsapp-web.js#fork-main-channels
|
||||
|
||||
start-proxy:
|
||||
docker run --rm -d --name squid-container -e TZ=UTC -p 3128:3128 ubuntu/squid:5.2-22.04_beta
|
||||
+33
-223
@@ -1,246 +1,56 @@
|
||||
#
|
||||
# https://waha.devlike.pro/docs/how-to/install/
|
||||
# Here's we provide a simple example of how to install WAHA using Docker Compose.
|
||||
#
|
||||
# We're assuming you have Docker and Docker Compose installed.
|
||||
# If you don't have Docker Compose installed, you can follow the official guide:
|
||||
# https://docs.docker.com/compose/install/
|
||||
#
|
||||
|
||||
services:
|
||||
waha:
|
||||
# Always restart the container on failure
|
||||
restart: always
|
||||
|
||||
#
|
||||
# Image
|
||||
#
|
||||
image: devlikeapro/waha-plus:latest # or devlikeapro/waha:latest if you're using WAHA Core
|
||||
#
|
||||
# We strongly recommend using the pinned version of WAHA
|
||||
# https://waha.devlike.pro/docs/how-to/engines/#versions
|
||||
# image: devlikeapro/waha-plus:latest-2024.7.6 # 'latest' is not mistaken here
|
||||
#
|
||||
# Multiple images are available
|
||||
# https://waha.devlike.pro/docs/how-to/engines/#docker-images
|
||||
# image: devlikeapro/waha-plus:chrome
|
||||
#
|
||||
# https://portal.devlike.pro/docker-image
|
||||
image: devlikeapro/waha-plus:latest
|
||||
# WAHA Core
|
||||
# image: devlikeapro/waha:latest
|
||||
|
||||
# The first port can be any free port you want
|
||||
# The second port must be the same as WHATSAPP_API_PORT or 3000 by default
|
||||
# Consider using the same port for simplicity
|
||||
ports:
|
||||
- '3000:3000/tcp'
|
||||
# NOTE: if you're using HTTP, you can use the following port mapping
|
||||
#- '80:3000/tcp'
|
||||
# NOTE: if you're using HTTPS, you can use the following port mapping
|
||||
#- '443:3000/tcp'
|
||||
- '127.0.0.1:3000:3000/tcp'
|
||||
|
||||
volumes:
|
||||
# Store sessions in the .sessions folder (comment it if you're using MongoDB)
|
||||
# Doesn't work with WAHA Core
|
||||
- './.sessions:/app/.sessions'
|
||||
|
||||
# Save media files
|
||||
# https://waha.devlike.pro/docs/how-to/storages/#save-media-files-between-the-container-restarts
|
||||
- './.media:/app/.media'
|
||||
|
||||
# If you're using certbot for HTTPS, you can use the following volume mapping
|
||||
# https://waha.devlike.pro/blog/setting-up-https-for-waha/#lets-encrypt-certbot--waha
|
||||
# - '/etc/letsencrypt:/etc/letsencrypt'
|
||||
|
||||
# WAHA Configuration
|
||||
# https://waha.devlike.pro/docs/how-to/config/
|
||||
environment:
|
||||
#
|
||||
# Common
|
||||
#
|
||||
# If you know the right URL, you can use it here.
|
||||
# Use any port in WHATSAPP_API_PORT and domain in WHATSAPP_API_HOSTNAME
|
||||
# https://waha.devlike.pro/docs/how-to/config/#common
|
||||
#- WHATSAPP_API_SCHEMA=http
|
||||
#- WHATSAPP_API_PORT=3000
|
||||
#- WHATSAPP_API_HOSTNAME=localhost
|
||||
#
|
||||
# Set your timezone to see the right time on screenshots
|
||||
# Find your timezone name in the list
|
||||
# https://en.wikipedia.org/wiki/List_of_tz_database_time_zones
|
||||
#- TZ=Europe/Dublin
|
||||
|
||||
#
|
||||
# Security
|
||||
#
|
||||
# https://waha.devlike.pro/docs/how-to/security/
|
||||
#
|
||||
- WHATSAPP_API_KEY=321
|
||||
- WAHA_DASHBOARD_USERNAME=waha
|
||||
- WAHA_DASHBOARD_PASSWORD=waha
|
||||
#- WHATSAPP_SWAGGER_USERNAME=admin
|
||||
#- WHATSAPP_SWAGGER_PASSWORD=admin
|
||||
|
||||
#
|
||||
# Dashboard
|
||||
#
|
||||
# https://waha.devlike.pro/docs/how-to/waha-dashboard/#configuration
|
||||
# Enabled by default
|
||||
#- WAHA_DASHBOARD_ENABLED=true
|
||||
#- WAHA_DASHBOARD_USERNAME=waha
|
||||
#- WAHA_DASHBOARD_PASSWORD=waha
|
||||
|
||||
#
|
||||
# Logging
|
||||
# https://waha.devlike.pro/docs/how-to/observability/#logging
|
||||
#
|
||||
# Set log format to JSON if you consume logs with log management systems
|
||||
- WAHA_LOG_FORMAT=JSON
|
||||
- WAHA_LOG_LEVEL=info
|
||||
# - WAHA_LOG_FORMAT=PRETTY
|
||||
# If you want to see more logs, you can set the log level to debug
|
||||
#- WAHA_LOG_LEVEL=debug
|
||||
# DEBUG=1 # shortcut for setting log level to debug
|
||||
|
||||
#
|
||||
# Engine
|
||||
#
|
||||
# Choose the right engine for your needs (WEBJS by default)
|
||||
# https://waha.devlike.pro/docs/how-to/engines/
|
||||
- WHATSAPP_DEFAULT_ENGINE=WEBJS
|
||||
# Use NOWEB engine for the fastest performance
|
||||
#- WHATSAPP_DEFAULT_ENGINE=NOWEB
|
||||
|
||||
#
|
||||
# Sessions
|
||||
#
|
||||
# https://waha.devlike.pro/docs/how-to/config/#sessions
|
||||
# Restart all sessions when container starts
|
||||
- WHATSAPP_RESTART_ALL_SESSIONS=True
|
||||
# Do not print QR codes in logs
|
||||
- WAHA_PRINT_QR=False
|
||||
#
|
||||
# OR you can specify sessions by name, if you have handful amount of sessions
|
||||
#- WHATSAPP_START_SESSION=session1,session2
|
||||
# Do not use both WHATSAPP_RESTART_ALL_SESSIONS and WHATSAPP_START_SESSION parameters
|
||||
#
|
||||
|
||||
#
|
||||
# Swagger
|
||||
#
|
||||
# https://waha.devlike.pro/docs/how-to/swagger/
|
||||
#- WHATSAPP_SWAGGER_USERNAME=admin
|
||||
#- WHATSAPP_SWAGGER_PASSWORD=admin
|
||||
# Disable Swagger
|
||||
#- WHATSAPP_SWAGGER_ENABLED=false
|
||||
# Enable advanced Swagger configuration
|
||||
#- WHATSAPP_SWAGGER_CONFIG_ADVANCED=true
|
||||
#
|
||||
# Swagger White Label customization
|
||||
# https://waha.devlike.pro/docs/how-to/swagger/#white-label
|
||||
#- WHATSAPP_SWAGGER_TITLE=WAHA API
|
||||
#
|
||||
|
||||
#
|
||||
# Global Proxy Settings
|
||||
# https://waha.devlike.pro/docs/how-to/config/#global-proxy-configuration
|
||||
# You can also set proxy settings for each session later when starting it
|
||||
# https://waha.devlike.pro/docs/how-to/sessions/#configure-proxy
|
||||
#- WHATSAPP_PROXY_SERVER=proxy.example.com:3128
|
||||
#-WHATSAPP_PROXY_SERVER_USERNAME=user
|
||||
#-WHATSAPP_PROXY_SERVER_PASSWORD=pass
|
||||
#
|
||||
|
||||
#
|
||||
# HTTPS Configuration
|
||||
#
|
||||
# https://waha.devlike.pro/docs/how-to/config/#https
|
||||
# Consider using certbot for HTTPS
|
||||
# https://waha.devlike.pro/blog/setting-up-https-for-waha/#lets-encrypt-certbot--waha
|
||||
#- WAHA_HTTPS_ENABLED=true
|
||||
#- WAHA_HTTPS_PATH_KEY=/etc/letsencrypt/live/waha.example.pro/privkey.pem
|
||||
#- WAHA_HTTPS_PATH_CERT=/etc/letsencrypt/live/waha.example.pro/cert.pem \
|
||||
#- WAHA_HTTPS_PATH_CA=/etc/letsencrypt/live/waha.example.pro/chain.pem
|
||||
#- WHATSAPP_API_SCHEMA=https
|
||||
#- WHATSAPP_API_PORT=3000
|
||||
#- WHATSAPP_API_HOSTNAME=waha.example.pro
|
||||
#
|
||||
|
||||
#
|
||||
# Media Configuration
|
||||
#
|
||||
# https://waha.devlike.pro/docs/how-to/config/#files
|
||||
#
|
||||
# Disable media (images, videos, files) download for incoming messages
|
||||
#- WHATSAPP_DOWNLOAD_MEDIA=false
|
||||
#
|
||||
# Download only specific media types
|
||||
#- WHATSAPP_FILES_MIMETYPES=image/jpeg,image/png
|
||||
#
|
||||
|
||||
# Media - Local Storage
|
||||
# https://waha.devlike.pro/docs/how-to/storages/#media---local
|
||||
#
|
||||
# Set to 0 to keep media files forever
|
||||
# https://waha.devlike.pro/docs/how-to/storages/#save-media-files-between-the-container-restarts
|
||||
- WAHA_MEDIA_STORAGE=LOCAL
|
||||
- WHATSAPP_FILES_LIFETIME=0
|
||||
- WHATSAPP_FILES_FOLDER=/app/.media
|
||||
# Keep media files for 180 seconds (3 minutes)
|
||||
#- WHATSAPP_FILES_LIFETIME=180
|
||||
#
|
||||
|
||||
# Media - S3 Storage
|
||||
# https://waha.devlike.pro/docs/how-to/storages/#media---s3
|
||||
#- WAHA_MEDIA_STORAGE=S3
|
||||
#- WAHA_S3_REGION=eu-west-2
|
||||
#- WAHA_S3_BUCKET=waha
|
||||
#- WAHA_S3_ACCESS_KEY_ID=minioadmin
|
||||
#- WAHA_S3_SECRET_ACCESS_KEY=minioadmin
|
||||
#- WAHA_S3_ENDPOINT=http://minio:9000 # Not required if you're using AWS S3
|
||||
#- WAHA_S3_FORCE_PATH_STYLE=True # Required for Minio
|
||||
#- WAHA_S3_PROXY_FILES=True # Required for docker-compose setup
|
||||
|
||||
#
|
||||
# Global Webhooks
|
||||
#
|
||||
# https://waha.devlike.pro/docs/how-to/config/#webhooks
|
||||
# https://waha.devlike.pro/docs/how-to/webhooks/
|
||||
#- WHATSAPP_HOOK_URL=https://webhook.site/11111111-1111-1111-1111-11111111
|
||||
#- WHATSAPP_HOOK_EVENTS=session.status,message,message.reaction
|
||||
#
|
||||
|
||||
#
|
||||
# MongoDB Configuration
|
||||
# https://waha.devlike.pro/docs/how-to/storages/#sessions---mongodb
|
||||
#- WHATSAPP_SESSIONS_MONGO_URL=mongodb://mongodb:27017
|
||||
#
|
||||
env_file:
|
||||
- .env
|
||||
|
||||
# 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
|
||||
# mongodb:
|
||||
# image: mongo
|
||||
# container_name: mongodb
|
||||
# ports:
|
||||
# - '127.0.0.1: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"
|
||||
# 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:
|
||||
# - '127.0.0.1:9000:9000'
|
||||
# - '127.0.0.1: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
|
||||
|
||||
@@ -0,0 +1,97 @@
|
||||
# https://waha.devlike.pro/docs/how-to/install/
|
||||
services:
|
||||
# Just to connect
|
||||
waha-dashboard:
|
||||
restart: unless-stopped
|
||||
image: devlikeapro/waha-plus:latest
|
||||
ports:
|
||||
- '127.0.0.1:3000:3000/tcp'
|
||||
env_file:
|
||||
- .env
|
||||
environment:
|
||||
- WAHA_WORKER_ID=waha-dashboard
|
||||
|
||||
waha1:
|
||||
restart: unless-stopped
|
||||
image: devlikeapro/waha-plus:latest
|
||||
ports:
|
||||
- '127.0.0.1:3001:3000/tcp'
|
||||
volumes:
|
||||
- './.media:/app/.media'
|
||||
env_file:
|
||||
- .env
|
||||
environment:
|
||||
- WAHA_WORKER_ID=waha1
|
||||
- WAHA_BASE_URL=http://localhost:3001
|
||||
- WHATSAPP_DEFAULT_ENGINE=NOWEB
|
||||
# MongoDB
|
||||
- WHATSAPP_SESSIONS_MONGO_URL=mongodb://mongodb:27017
|
||||
# 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
|
||||
|
||||
waha2:
|
||||
restart: unless-stopped
|
||||
image: devlikeapro/waha-plus:latest
|
||||
ports:
|
||||
- '127.0.0.1:3002:3000/tcp'
|
||||
volumes:
|
||||
- './.media:/app/.media'
|
||||
env_file:
|
||||
- .env
|
||||
environment:
|
||||
- WAHA_WORKER_ID=waha2
|
||||
- WAHA_BASE_URL=http://localhost:3002
|
||||
- WHATSAPP_DEFAULT_ENGINE=NOWEB
|
||||
# MongoDB
|
||||
- WHATSAPP_SESSIONS_MONGO_URL=mongodb://mongodb:27017
|
||||
# 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
|
||||
|
||||
# NOTE: Only if you're using MongoDB
|
||||
# https://waha.devlike.pro/docs/how-to/storages/#sessions---mongodb
|
||||
# Comment this block if you're not using MongoDB
|
||||
mongodb:
|
||||
image: mongo
|
||||
container_name: mongodb
|
||||
ports:
|
||||
- '127.0.0.1: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 S3
|
||||
minio:
|
||||
image: quay.io/minio/minio
|
||||
container_name: minio
|
||||
restart: always
|
||||
ports:
|
||||
- '127.0.0.1:9000:9000'
|
||||
- '127.0.0.1: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: {}
|
||||
@@ -14,7 +14,6 @@ services:
|
||||
- WAHA_DASHBOARD_USERNAME=waha
|
||||
- WAHA_DASHBOARD_PASSWORD=waha
|
||||
- WHATSAPP_DEFAULT_ENGINE=WEBJS
|
||||
- WHATSAPP_RESTART_ALL_SESSIONS=True
|
||||
- WAHA_PRINT_QR=False
|
||||
- WAHA_MEDIA_STORAGE=LOCAL
|
||||
- WHATSAPP_FILES_LIFETIME=0
|
||||
|
||||
@@ -25,7 +25,6 @@ services:
|
||||
- './.sessions:/app/.sessions'
|
||||
environment:
|
||||
# Environment variables from https://waha.devlike.pro/docs/how-to/config/
|
||||
# - WHATSAPP_RESTART_ALL_SESSIONS=True
|
||||
# - WHATSAPP_HOOK_URL=https://webhook.site/11111111-1111-1111-1111-11111111
|
||||
# - WHATSAPP_HOOK_EVENTS=message
|
||||
# - WHATSAPP_API_HOSTNAME=localhost
|
||||
@@ -54,7 +53,6 @@ services:
|
||||
- './.sessions:/app/.sessions'
|
||||
environment:
|
||||
# Environment variables from https://waha.devlike.pro/docs/how-to/config/
|
||||
# - WHATSAPP_RESTART_ALL_SESSIONS=True
|
||||
# - WHATSAPP_HOOK_URL=https://webhook.site/11111111-1111-1111-1111-11111111
|
||||
# - WHATSAPP_HOOK_EVENTS=message
|
||||
# - WHATSAPP_API_KEY=321
|
||||
|
||||
+1
-1
@@ -8,4 +8,4 @@ uv_threadpool_size=$(($cpus * 2))
|
||||
export UV_THREADPOOL_SIZE="${UV_THREADPOOL_SIZE:-$uv_threadpool_size}"
|
||||
|
||||
# Start your application using yarn
|
||||
node dist/main
|
||||
exec node dist/main
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 17 KiB |
+16
-6
@@ -23,7 +23,7 @@
|
||||
"test:e2e": "jest --config ./test/jest-e2e.json"
|
||||
},
|
||||
"dependencies": {
|
||||
"@adiwajshing/baileys": "github:devlikeapro/Baileys#fork-master-channels-2",
|
||||
"@adiwajshing/baileys": "github:devlikeapro/Baileys#fork-master-channels-678",
|
||||
"@adiwajshing/keyed-db": "^0.2.4",
|
||||
"@aws-sdk/client-s3": "^3.633.0",
|
||||
"@aws-sdk/s3-request-presigner": "^3.633.0",
|
||||
@@ -42,7 +42,10 @@
|
||||
"@types/lodash": "^4.14.194",
|
||||
"@types/ws": "^8.5.4",
|
||||
"adm-zip": "0.5.10",
|
||||
"agentkeepalive": "^4.5.0",
|
||||
"async-lock": "^1.4.1",
|
||||
"axios": "^1.7.7",
|
||||
"axios-retry": "^4.5.0",
|
||||
"better-sqlite3": "11.3.0",
|
||||
"check-disk-space": "^3.4.0",
|
||||
"chokidar": "^3.6.0",
|
||||
@@ -59,7 +62,7 @@
|
||||
"link-preview-js": "^3.0.4",
|
||||
"lodash": "^4.17.21",
|
||||
"mime-types": "^2.1.27",
|
||||
"mongodb": "^6.3.0",
|
||||
"mongodb": "6.9.0",
|
||||
"nestjs-pino": "^4.1.0",
|
||||
"passport": "^0.6.0",
|
||||
"passport-headerapikey": "^1.2.2",
|
||||
@@ -67,17 +70,24 @@
|
||||
"pino-pretty": "^11.2.1",
|
||||
"pretty-bytes": "5.6.0",
|
||||
"promise-retry": "^2.0.1",
|
||||
"puppeteer": "^21.0.0",
|
||||
"puppeteer": "^23.6.0",
|
||||
"qrcode": "^1.5.1",
|
||||
"qrcode-terminal": "^0.12.0",
|
||||
"reflect-metadata": "^0.1.13",
|
||||
"request": "^2.88.2",
|
||||
"requestretry": "^4.1.1",
|
||||
"rimraf": "^3.0.2",
|
||||
"rxjs": "^7.1.0",
|
||||
"sharp": "^0.33.4",
|
||||
"swagger-ui-express": "^4.1.4",
|
||||
"whatsapp-web.js": "github:devlikeapro/whatsapp-web.js#fork-webpack-exodus-channels"
|
||||
"whatsapp-web.js": "github:devlikeapro/whatsapp-web.js#fork-main-channels"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"bufferutil": "^4.0.8"
|
||||
},
|
||||
"resolutions": {
|
||||
"bufferutil": "^4.0.8",
|
||||
"ws": "^8.18.0",
|
||||
"puppeteer": "^23.6.0",
|
||||
"whatwg-url": "13.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@nestjs/cli": "^9.0.0",
|
||||
|
||||
@@ -14,7 +14,6 @@ docker run -d \
|
||||
--env WAHA_LOG_FORMAT=JSON \
|
||||
--env WAHA_LOG_LEVEL=info \
|
||||
--env WHATSAPP_DEFAULT_ENGINE=WEBJS \
|
||||
--env WHATSAPP_RESTART_ALL_SESSIONS=True \
|
||||
--env WAHA_PRINT_QR=False \
|
||||
--env WHATSAPP_FILES_LIFETIME=0 \
|
||||
--env WHATSAPP_FILES_FOLDER=/app/.media \
|
||||
|
||||
@@ -82,7 +82,7 @@ export class ChannelsController {
|
||||
}
|
||||
}
|
||||
|
||||
@Post('follow')
|
||||
@Post(':id/follow')
|
||||
@SessionApiParam
|
||||
@NewsletterIdApiParam
|
||||
@ApiOperation({ summary: 'Follow the channel.' })
|
||||
@@ -93,7 +93,7 @@ export class ChannelsController {
|
||||
return session.channelsFollowChannel(id);
|
||||
}
|
||||
|
||||
@Post('unfollow')
|
||||
@Post(':id/unfollow')
|
||||
@SessionApiParam
|
||||
@NewsletterIdApiParam
|
||||
@ApiOperation({ summary: 'Unfollow the channel.' })
|
||||
@@ -104,7 +104,7 @@ export class ChannelsController {
|
||||
return session.channelsUnfollowChannel(id);
|
||||
}
|
||||
|
||||
@Post('mute')
|
||||
@Post(':id/mute')
|
||||
@SessionApiParam
|
||||
@NewsletterIdApiParam
|
||||
@ApiOperation({ summary: 'Mute the channel.' })
|
||||
@@ -115,7 +115,7 @@ export class ChannelsController {
|
||||
return session.channelsMuteChannel(id);
|
||||
}
|
||||
|
||||
@Post('unmute')
|
||||
@Post(':id/unmute')
|
||||
@SessionApiParam
|
||||
@NewsletterIdApiParam
|
||||
@ApiOperation({ summary: 'Unmute the channel.' })
|
||||
|
||||
@@ -3,6 +3,7 @@ import {
|
||||
Controller,
|
||||
Delete,
|
||||
Get,
|
||||
NotFoundException,
|
||||
Param,
|
||||
Post,
|
||||
Put,
|
||||
@@ -21,7 +22,12 @@ import {
|
||||
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 {
|
||||
ChatsPaginationParams,
|
||||
GetChatMessageQuery,
|
||||
GetChatMessagesFilter,
|
||||
GetChatMessagesQuery,
|
||||
} from '../structures/chats.dto';
|
||||
import { EditMessageRequest } from '../structures/chatting.dto';
|
||||
|
||||
@ApiSecurity('api_key')
|
||||
@@ -36,9 +42,9 @@ class ChatsController {
|
||||
@ApiOperation({ summary: 'Get chats' })
|
||||
getChats(
|
||||
@WorkingSessionParam session: WhatsappSession,
|
||||
@Query() query: GetChatsQuery,
|
||||
@Query() pagination: ChatsPaginationParams,
|
||||
) {
|
||||
return session.getChats(query);
|
||||
return session.getChats(pagination);
|
||||
}
|
||||
|
||||
@Delete(':chatId')
|
||||
@@ -56,13 +62,31 @@ class ChatsController {
|
||||
@SessionApiParam
|
||||
@ApiOperation({ summary: 'Gets messages in the chat' })
|
||||
@ChatIdApiParam
|
||||
@UsePipes(new ValidationPipe({ transform: true, whitelist: true }))
|
||||
getChatMessages(
|
||||
@Query() query: GetChatMessagesQuery,
|
||||
@Query() filter: GetChatMessagesFilter,
|
||||
@WorkingSessionParam session: WhatsappSession,
|
||||
@Param('chatId') chatId: string,
|
||||
) {
|
||||
const downloadMedia = parseBool(query.downloadMedia);
|
||||
return session.getChatMessages(chatId, query.limit, downloadMedia);
|
||||
return session.getChatMessages(chatId, query, filter);
|
||||
}
|
||||
|
||||
@Get(':chatId/messages/:messageId')
|
||||
@SessionApiParam
|
||||
@ApiOperation({ summary: 'Gets message by id' })
|
||||
@ChatIdApiParam
|
||||
async getChatMessage(
|
||||
@Query() query: GetChatMessageQuery,
|
||||
@WorkingSessionParam session: WhatsappSession,
|
||||
@Param('chatId') chatId: string,
|
||||
@Param('messageId') messageId: string,
|
||||
) {
|
||||
const message = await session.getChatMessage(chatId, messageId, query);
|
||||
if (!message) {
|
||||
throw new NotFoundException('Message not found');
|
||||
}
|
||||
return message;
|
||||
}
|
||||
|
||||
@Delete(':chatId/messages')
|
||||
@@ -124,6 +148,17 @@ class ChatsController {
|
||||
) {
|
||||
return session.chatsUnarchiveChat(chatId);
|
||||
}
|
||||
|
||||
@Post(':chatId/unread')
|
||||
@SessionApiParam
|
||||
@ChatIdApiParam
|
||||
@ApiOperation({ summary: 'Unread the chat' })
|
||||
unreadChat(
|
||||
@WorkingSessionParam session: WhatsappSession,
|
||||
@Param('chatId') chatId: string,
|
||||
) {
|
||||
return session.chatsUnreadChat(chatId);
|
||||
}
|
||||
}
|
||||
|
||||
export { ChatsController };
|
||||
@@ -1,5 +1,16 @@
|
||||
import { Body, Controller, Get, Post, Put, Query } from '@nestjs/common';
|
||||
import {
|
||||
Body,
|
||||
Controller,
|
||||
Get,
|
||||
Post,
|
||||
Put,
|
||||
Query,
|
||||
UsePipes,
|
||||
} from '@nestjs/common';
|
||||
import { ApiOperation, ApiSecurity, ApiTags } from '@nestjs/swagger';
|
||||
import { WAHAValidationPipe } from '@waha/nestjs/pipes/WAHAValidationPipe';
|
||||
import { GetChatMessagesFilter } from '@waha/structures/chats.dto';
|
||||
import { SendButtonsRequest } from '@waha/structures/chatting.buttons.dto';
|
||||
|
||||
import { SessionManager } from '../core/abc/manager.abc';
|
||||
import {
|
||||
@@ -8,6 +19,7 @@ import {
|
||||
GetMessageQuery,
|
||||
MessageContactVcardRequest,
|
||||
MessageFileRequest,
|
||||
MessageForwardRequest,
|
||||
MessageImageRequest,
|
||||
MessageLinkPreviewRequest,
|
||||
MessageLocationRequest,
|
||||
@@ -81,6 +93,25 @@ export class ChattingController {
|
||||
return whatsapp.sendVideo(request);
|
||||
}
|
||||
|
||||
@Post('/sendButtons')
|
||||
@ApiOperation({
|
||||
summary: 'Send buttons (interactive message)',
|
||||
description: 'Send Buttons',
|
||||
})
|
||||
@UsePipes(new WAHAValidationPipe())
|
||||
async sendButtons(@Body() request: SendButtonsRequest) {
|
||||
const whatsapp = await this.manager.getWorkingSession(request.session);
|
||||
return whatsapp.sendButtons(request);
|
||||
}
|
||||
|
||||
@Post('/forwardMessage')
|
||||
async forwardMessage(
|
||||
@Body() request: MessageForwardRequest,
|
||||
): Promise<WAMessage> {
|
||||
const whatsapp = await this.manager.getWorkingSession(request.session);
|
||||
return await whatsapp.forwardMessage(request);
|
||||
}
|
||||
|
||||
@Post('/sendSeen')
|
||||
async sendSeen(@Body() chat: SendSeenRequest) {
|
||||
const whatsapp = await this.manager.getWorkingSession(chat.session);
|
||||
@@ -141,9 +172,12 @@ export class ChattingController {
|
||||
|
||||
@Get('/messages')
|
||||
@ApiOperation({ summary: 'Get messages in a chat' })
|
||||
async getMessages(@Query() query: GetMessageQuery) {
|
||||
async getMessages(
|
||||
@Query() query: GetMessageQuery,
|
||||
@Query() filter: GetChatMessagesFilter,
|
||||
) {
|
||||
const whatsapp = await this.manager.getWorkingSession(query.session);
|
||||
return whatsapp.getMessages(query);
|
||||
return whatsapp.getChatMessages(query.chatId, query, filter);
|
||||
}
|
||||
|
||||
@Get('/sendText')
|
||||
|
||||
@@ -1,4 +1,12 @@
|
||||
import { Body, Controller, Get, Post, Query } from '@nestjs/common';
|
||||
import {
|
||||
Body,
|
||||
Controller,
|
||||
Get,
|
||||
Post,
|
||||
Query,
|
||||
UsePipes,
|
||||
ValidationPipe,
|
||||
} from '@nestjs/common';
|
||||
import { ApiOperation, ApiSecurity, ApiTags } from '@nestjs/swagger';
|
||||
|
||||
import { SessionManager } from '../core/abc/manager.abc';
|
||||
@@ -7,7 +15,11 @@ import {
|
||||
CheckNumberStatusQuery,
|
||||
WANumberExistResult,
|
||||
} from '../structures/chatting.dto';
|
||||
import { ContactQuery, ContactRequest } from '../structures/contacts.dto';
|
||||
import {
|
||||
ContactQuery,
|
||||
ContactRequest,
|
||||
ContactsPaginationParams,
|
||||
} from '../structures/contacts.dto';
|
||||
|
||||
@ApiSecurity('api_key')
|
||||
@Controller('api/contacts')
|
||||
@@ -15,6 +27,17 @@ import { ContactQuery, ContactRequest } from '../structures/contacts.dto';
|
||||
export class ContactsController {
|
||||
constructor(private manager: SessionManager) {}
|
||||
|
||||
@Get('/all')
|
||||
@ApiOperation({ summary: 'Get all contacts' })
|
||||
@UsePipes(new ValidationPipe({ transform: true, whitelist: true }))
|
||||
async getAll(
|
||||
@Query() query: SessionQuery,
|
||||
@Query() pagination: ContactsPaginationParams,
|
||||
) {
|
||||
const whatsapp = await this.manager.getWorkingSession(query.session);
|
||||
return whatsapp.getContacts(pagination);
|
||||
}
|
||||
|
||||
@Get('/')
|
||||
@ApiOperation({
|
||||
summary: 'Get contact basic info',
|
||||
@@ -26,13 +49,6 @@ export class ContactsController {
|
||||
return whatsapp.getContact(query);
|
||||
}
|
||||
|
||||
@Get('/all')
|
||||
@ApiOperation({ summary: 'Get all contacts' })
|
||||
async getAll(@Query() query: SessionQuery) {
|
||||
const whatsapp = await this.manager.getWorkingSession(query.session);
|
||||
return whatsapp.getContacts();
|
||||
}
|
||||
|
||||
@Get('/check-exists')
|
||||
@ApiOperation({ summary: 'Check phone number is registered in WhatsApp.' })
|
||||
async checkExists(
|
||||
|
||||
@@ -9,14 +9,11 @@ import {
|
||||
NotFoundException,
|
||||
Post,
|
||||
Query,
|
||||
Res,
|
||||
StreamableFile,
|
||||
UseInterceptors,
|
||||
UsePipes,
|
||||
} from '@nestjs/common';
|
||||
import { ApiOperation, ApiSecurity, ApiTags } from '@nestjs/swagger';
|
||||
import { WhatsappConfigService } from '@waha/config.service';
|
||||
import { getApp } from '@waha/main';
|
||||
import { WAHAValidationPipe } from '@waha/nestjs/pipes/WAHAValidationPipe';
|
||||
import { WAHAEnvironment } from '@waha/structures/environment.dto';
|
||||
import {
|
||||
@@ -25,6 +22,7 @@ import {
|
||||
StopRequest,
|
||||
StopResponse,
|
||||
} from '@waha/structures/server.dto';
|
||||
import { sleep } from '@waha/utils/promiseTimeout';
|
||||
import { VERSION } from '@waha/version';
|
||||
import * as lodash from 'lodash';
|
||||
|
||||
@@ -34,7 +32,7 @@ import * as lodash from 'lodash';
|
||||
export class ServerController {
|
||||
private logger: Logger;
|
||||
|
||||
constructor() {
|
||||
constructor(private config: WhatsappConfigService) {
|
||||
this.logger = new Logger('ServerController');
|
||||
}
|
||||
|
||||
@@ -79,6 +77,9 @@ export class ServerController {
|
||||
return {
|
||||
startTimestamp: startTimestamp,
|
||||
uptime: uptime,
|
||||
worker: {
|
||||
id: this.config.workerId,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -97,16 +98,14 @@ export class ServerController {
|
||||
setTimeout(() => {
|
||||
this.logger.log('Force stopping the server');
|
||||
process.kill(process.pid, 'SIGKILL');
|
||||
process.exit(0);
|
||||
}, 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.kill(process.pid, 'SIGTERM');
|
||||
await sleep(10_000);
|
||||
process.exit(0);
|
||||
}, timeout);
|
||||
}
|
||||
|
||||
@@ -35,21 +35,14 @@ import {
|
||||
SessionUpdateRequest,
|
||||
} from '../structures/sessions.dto';
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
const AsyncLock = require('async-lock');
|
||||
|
||||
@ApiSecurity('api_key')
|
||||
@Controller('api/sessions')
|
||||
@ApiTags('🖥️ Sessions')
|
||||
class SessionsController {
|
||||
private lock: any;
|
||||
|
||||
constructor(private manager: SessionManager) {
|
||||
this.lock = new AsyncLock({ maxPending: Infinity });
|
||||
}
|
||||
constructor(private manager: SessionManager) {}
|
||||
|
||||
private withLock(name: string, fn: () => any) {
|
||||
return this.lock.acquire(name, fn);
|
||||
return this.manager.withLock(name, fn);
|
||||
}
|
||||
|
||||
@Get('/')
|
||||
@@ -98,6 +91,7 @@ class SessionsController {
|
||||
const start = request.start || false;
|
||||
await this.manager.upsert(name, config);
|
||||
if (start) {
|
||||
await this.manager.assign(name);
|
||||
await this.manager.start(name);
|
||||
}
|
||||
});
|
||||
@@ -140,6 +134,8 @@ class SessionsController {
|
||||
@UsePipes(new WAHAValidationPipe())
|
||||
async delete(@Param('session') name: string): Promise<void> {
|
||||
await this.withLock(name, async () => {
|
||||
await this.manager.unassign(name);
|
||||
await this.manager.unpair(name);
|
||||
await this.manager.stop(name, true);
|
||||
await this.manager.logout(name);
|
||||
await this.manager.delete(name);
|
||||
@@ -151,7 +147,7 @@ class SessionsController {
|
||||
@ApiOperation({
|
||||
summary: 'Start the session',
|
||||
description:
|
||||
'Start the session with the given name. The session must exist. Identity operation.',
|
||||
'Start the session with the given name. The session must exist. Idempotent operation.',
|
||||
})
|
||||
@UsePipes(new WAHAValidationPipe())
|
||||
async start(@Param('session') name: string): Promise<SessionDTO> {
|
||||
@@ -160,10 +156,8 @@ class SessionsController {
|
||||
if (!exists) {
|
||||
throw new NotFoundException('Session not found');
|
||||
}
|
||||
const isRunning = this.manager.isRunning(name);
|
||||
if (!isRunning) {
|
||||
await this.manager.start(name);
|
||||
}
|
||||
await this.manager.assign(name);
|
||||
await this.manager.start(name);
|
||||
});
|
||||
return await this.manager.getSessionInfo(name);
|
||||
}
|
||||
@@ -177,10 +171,7 @@ class SessionsController {
|
||||
@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.unassign(name);
|
||||
await this.manager.stop(name, false);
|
||||
});
|
||||
return await this.manager.getSessionInfo(name);
|
||||
@@ -200,6 +191,7 @@ class SessionsController {
|
||||
throw new NotFoundException('Session not found');
|
||||
}
|
||||
const isRunning = this.manager.isRunning(name);
|
||||
await this.manager.unpair(name);
|
||||
await this.manager.stop(name, true);
|
||||
await this.manager.logout(name);
|
||||
if (isRunning) {
|
||||
@@ -222,6 +214,7 @@ class SessionsController {
|
||||
if (!exists) {
|
||||
throw new NotFoundException('Session not found');
|
||||
}
|
||||
await this.manager.assign(name);
|
||||
await this.manager.stop(name, true);
|
||||
await this.manager.start(name);
|
||||
});
|
||||
@@ -248,6 +241,7 @@ class SessionsController {
|
||||
return await this.withLock(name, async () => {
|
||||
const config = request.config;
|
||||
await this.manager.upsert(name, config);
|
||||
await this.manager.assign(name);
|
||||
return await this.manager.start(name);
|
||||
});
|
||||
}
|
||||
@@ -265,12 +259,15 @@ class SessionsController {
|
||||
if (request.logout) {
|
||||
// Old API did remove the session complete
|
||||
await this.withLock(name, async () => {
|
||||
await this.manager.unassign(name);
|
||||
await this.manager.unpair(name);
|
||||
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.unassign(name);
|
||||
await this.manager.stop(name, false);
|
||||
});
|
||||
}
|
||||
@@ -288,6 +285,8 @@ class SessionsController {
|
||||
): Promise<void> {
|
||||
const name = request.name;
|
||||
await this.withLock(name, async () => {
|
||||
await this.manager.unassign(name);
|
||||
await this.manager.unpair(name);
|
||||
await this.manager.stop(name, true);
|
||||
await this.manager.logout(name);
|
||||
await this.manager.delete(name);
|
||||
|
||||
@@ -18,9 +18,43 @@ export class WhatsappConfigService {
|
||||
}
|
||||
|
||||
get port(): string {
|
||||
if (this.configService.get('PORT')) {
|
||||
return this.configService.get('PORT');
|
||||
}
|
||||
return this.configService.get('WHATSAPP_API_PORT', '3000');
|
||||
}
|
||||
|
||||
get baseUrl(): string {
|
||||
let baseUrl = this.configService.get('WAHA_BASE_URL', '');
|
||||
if (!baseUrl) {
|
||||
// combine schema+hostname+port
|
||||
baseUrl = `${this.schema}://${this.hostname}:${this.port}`;
|
||||
}
|
||||
// remove / at the end
|
||||
return baseUrl.replace(/\/$/, '');
|
||||
}
|
||||
|
||||
get workerId(): string {
|
||||
return this.configService.get('WAHA_WORKER_ID', '');
|
||||
}
|
||||
|
||||
get shouldRestartWorkerSessions(): boolean {
|
||||
const value = this.configService.get(
|
||||
'WAHA_WORKER_RESTART_SESSIONS',
|
||||
'true',
|
||||
);
|
||||
return parseBool(value);
|
||||
}
|
||||
|
||||
get autoStartDelaySeconds(): number {
|
||||
const value = this.configService.get('WAHA_AUTO_START_DELAY_SECONDS', '0');
|
||||
try {
|
||||
return parseInt(value, 10);
|
||||
} catch (error) {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
get mimetypes(): string[] {
|
||||
if (!this.shouldDownloadMedia) {
|
||||
return ['mimetype/ignore-all-media'];
|
||||
|
||||
@@ -20,11 +20,11 @@ export class SwaggerConfiguratorCore {
|
||||
|
||||
get description() {
|
||||
return (
|
||||
'WhatsApp HTTP API that you can run in a click!<br/>' +
|
||||
'<b>WhatsApp HTTP API</b> that you can run in a click!<br/>' +
|
||||
'<a href="/dashboard"><b>📊 Dashboard</b></a><br/>' +
|
||||
'<br/>' +
|
||||
'Learn more:' +
|
||||
'<ul>' +
|
||||
'<li><a href="/dashboard">WAHA Dashboard</a></li>' +
|
||||
'<li><a href="https://waha.devlike.pro/" target="_blank">Documentation</a></li>' +
|
||||
'<li><a href="https://waha.devlike.pro/docs/how-to/engines/#features" target="_blank">Supported features in engines</a></li>' +
|
||||
'<li><a href="https://github.com/devlikeapro/waha" target="_blank">GitHub - WAHA Core</a></li>' +
|
||||
|
||||
@@ -2,11 +2,14 @@ import {
|
||||
BeforeApplicationShutdown,
|
||||
UnprocessableEntityException,
|
||||
} from '@nestjs/common';
|
||||
import { WhatsappConfigService } from '@waha/config.service';
|
||||
import { ISessionMeRepository } from '@waha/core/storage/ISessionMeRepository';
|
||||
import { ISessionWorkerRepository } from '@waha/core/storage/ISessionWorkerRepository';
|
||||
import { WAHAWebhook } from '@waha/structures/webhooks.dto';
|
||||
import { waitUntil } from '@waha/utils/promiseTimeout';
|
||||
import { VERSION } from '@waha/version';
|
||||
import { EventEmitter } from 'events';
|
||||
import { PinoLogger } from 'nestjs-pino';
|
||||
|
||||
import {
|
||||
WAHAEngine,
|
||||
@@ -24,15 +27,47 @@ import { ISessionConfigRepository } from '../storage/ISessionConfigRepository';
|
||||
import { WhatsappSession } from './session.abc';
|
||||
import { WebhookConductor } from './webhooks.abc';
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
const AsyncLock = require('async-lock');
|
||||
|
||||
export abstract class SessionManager implements BeforeApplicationShutdown {
|
||||
public store: any;
|
||||
public sessionAuthRepository: ISessionAuthRepository;
|
||||
public sessionConfigRepository: ISessionConfigRepository;
|
||||
protected sessionMeRepository: ISessionMeRepository;
|
||||
protected sessionWorkerRepository: ISessionWorkerRepository;
|
||||
public events: EventEmitter;
|
||||
private lock: any;
|
||||
|
||||
WAIT_STATUS_INTERVAL = 500;
|
||||
WAIT_STATUS_TIMEOUT = 5_000;
|
||||
|
||||
protected constructor(
|
||||
protected config: WhatsappConfigService,
|
||||
protected log: PinoLogger,
|
||||
) {
|
||||
this.lock = new AsyncLock({ maxPending: Infinity });
|
||||
this.log.setContext(SessionManager.name);
|
||||
}
|
||||
|
||||
protected startPredefinedSessions() {
|
||||
const startSessions = this.config.startSessions;
|
||||
startSessions.forEach((sessionName) => {
|
||||
this.withLock(sessionName, async () => {
|
||||
const log = this.log.logger.child({ session: sessionName });
|
||||
log.info(`Restarting PREDEFINED session...`);
|
||||
await this.start(sessionName).catch((error) => {
|
||||
log.error(`Failed to start PREDEFINED session: ${error}`);
|
||||
log.error(error.stack);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
public withLock(name: string, fn: () => any) {
|
||||
return this.lock.acquire(name, fn);
|
||||
}
|
||||
|
||||
protected abstract getEngine(engine: WAHAEngine): typeof WhatsappSession;
|
||||
|
||||
protected abstract get EngineClass(): typeof WhatsappSession;
|
||||
@@ -59,13 +94,27 @@ export abstract class SessionManager implements BeforeApplicationShutdown {
|
||||
|
||||
abstract logout(name: string): Promise<void>;
|
||||
|
||||
abstract unpair(name: string): Promise<void>;
|
||||
|
||||
abstract getSession(name: string): WhatsappSession;
|
||||
|
||||
abstract getSessionInfo(name: string): Promise<SessionDetailedInfo | null>;
|
||||
|
||||
abstract getSessions(all: boolean): Promise<SessionInfo[]>;
|
||||
|
||||
handleSessionEvent(event: WAHAEvents, session: WhatsappSession) {
|
||||
get workerId() {
|
||||
return this.config.workerId;
|
||||
}
|
||||
|
||||
async assign(name: string) {
|
||||
await this.sessionWorkerRepository?.assign(name, this.workerId);
|
||||
}
|
||||
|
||||
async unassign(name: string) {
|
||||
await this.sessionWorkerRepository?.unassign(name, this.workerId);
|
||||
}
|
||||
|
||||
protected handleSessionEvent(event: WAHAEvents, session: WhatsappSession) {
|
||||
return (payload: any) => {
|
||||
const me = session.getSessionMeInfo();
|
||||
const data: WAHAWebhook = {
|
||||
|
||||
+44
-10
@@ -3,8 +3,15 @@ import {
|
||||
CreateChannelRequest,
|
||||
ListChannelsQuery,
|
||||
} from '@waha/structures/channels.dto';
|
||||
import { GetChatsQuery } from '@waha/structures/chats.dto';
|
||||
import {
|
||||
GetChatMessageQuery,
|
||||
GetChatMessagesFilter,
|
||||
GetChatMessagesQuery,
|
||||
} from '@waha/structures/chats.dto';
|
||||
import { SendButtonsRequest } from '@waha/structures/chatting.buttons.dto';
|
||||
import { Label, LabelID } from '@waha/structures/labels.dto';
|
||||
import { PaginationParams } from '@waha/structures/pagination.dto';
|
||||
import { WAMessage } from '@waha/structures/responses.dto';
|
||||
import { LoggerBuilder } from '@waha/utils/logging';
|
||||
import { EventEmitter } from 'events';
|
||||
import * as fs from 'fs';
|
||||
@@ -15,9 +22,9 @@ import {
|
||||
ChatRequest,
|
||||
CheckNumberStatusQuery,
|
||||
EditMessageRequest,
|
||||
GetMessageQuery,
|
||||
MessageContactVcardRequest,
|
||||
MessageFileRequest,
|
||||
MessageForwardRequest,
|
||||
MessageImageRequest,
|
||||
MessageLinkPreviewRequest,
|
||||
MessageLocationRequest,
|
||||
@@ -108,6 +115,7 @@ export abstract class WhatsappSession {
|
||||
protected proxyConfig?: ProxyConfig;
|
||||
public sessionConfig?: SessionConfig;
|
||||
protected engineConfig?: any;
|
||||
protected unpairing: boolean = false;
|
||||
|
||||
private _status: WAHASessionStatus;
|
||||
private shouldPrintQR: boolean;
|
||||
@@ -135,6 +143,11 @@ export abstract class WhatsappSession {
|
||||
}
|
||||
|
||||
protected set status(value: WAHASessionStatus) {
|
||||
if (this.unpairing && value !== WAHASessionStatus.STOPPED) {
|
||||
// In case of unpairing
|
||||
// wait for STOPPED event, ignore the rest
|
||||
return;
|
||||
}
|
||||
this._status = value;
|
||||
const body: WASessionStatusBody = { name: this.name, status: value };
|
||||
this.events.emit(WAHAEvents.SESSION_STATUS, body);
|
||||
@@ -165,6 +178,7 @@ export abstract class WhatsappSession {
|
||||
'--disable-default-apps',
|
||||
'--disable-dev-shm-usage',
|
||||
'--disable-extensions',
|
||||
'--disable-features=site-per-process',
|
||||
'--disable-gpu',
|
||||
'--disable-offer-store-unmasked-wallet-cards',
|
||||
'--disable-offline-load-stale-cache',
|
||||
@@ -192,6 +206,7 @@ export abstract class WhatsappSession {
|
||||
'--safebrowsing-disable-auto-update',
|
||||
'--single-process',
|
||||
'--use-mock-keychain',
|
||||
'--window-size=1280,720',
|
||||
];
|
||||
}
|
||||
|
||||
@@ -205,6 +220,11 @@ export abstract class WhatsappSession {
|
||||
/** Stop the session */
|
||||
abstract stop(): Promise<void>;
|
||||
|
||||
/* Unpair the account */
|
||||
async unpair(): Promise<void> {
|
||||
return;
|
||||
}
|
||||
|
||||
/** Subscribe the handler to specific hook */
|
||||
subscribeSessionEvent(
|
||||
hook: WAHAEvents | string,
|
||||
@@ -279,6 +299,8 @@ export abstract class WhatsappSession {
|
||||
throw new NotImplementedByEngineError();
|
||||
}
|
||||
|
||||
abstract forwardMessage(request: MessageForwardRequest): Promise<WAMessage>;
|
||||
|
||||
abstract sendImage(request: MessageImageRequest);
|
||||
|
||||
abstract sendFile(request: MessageFileRequest);
|
||||
@@ -289,6 +311,10 @@ export abstract class WhatsappSession {
|
||||
throw new NotImplementedByEngineError();
|
||||
}
|
||||
|
||||
sendButtons(request: SendButtonsRequest) {
|
||||
throw new NotImplementedByEngineError();
|
||||
}
|
||||
|
||||
abstract reply(request: MessageReplyRequest);
|
||||
|
||||
abstract sendSeen(chat: ChatRequest);
|
||||
@@ -297,10 +323,6 @@ export abstract class WhatsappSession {
|
||||
|
||||
abstract stopTyping(chat: ChatRequest);
|
||||
|
||||
getMessages(query: GetMessageQuery) {
|
||||
throw new NotImplementedByEngineError();
|
||||
}
|
||||
|
||||
abstract setReaction(request: MessageReactionRequest);
|
||||
|
||||
setStar(request: MessageStarRequest): Promise<void> {
|
||||
@@ -310,7 +332,7 @@ export abstract class WhatsappSession {
|
||||
/**
|
||||
* Chats methods
|
||||
*/
|
||||
public getChats(query: GetChatsQuery) {
|
||||
public getChats(pagination: PaginationParams) {
|
||||
throw new NotImplementedByEngineError();
|
||||
}
|
||||
|
||||
@@ -320,12 +342,20 @@ export abstract class WhatsappSession {
|
||||
|
||||
public getChatMessages(
|
||||
chatId: string,
|
||||
limit: number,
|
||||
downloadMedia: boolean,
|
||||
query: GetChatMessagesQuery,
|
||||
filter: GetChatMessagesFilter,
|
||||
) {
|
||||
throw new NotImplementedByEngineError();
|
||||
}
|
||||
|
||||
public getChatMessage(
|
||||
chatId: string,
|
||||
messageId: string,
|
||||
query: GetChatMessageQuery,
|
||||
): Promise<null | WAMessage> {
|
||||
throw new NotImplementedByEngineError();
|
||||
}
|
||||
|
||||
public deleteMessage(chatId: string, messageId: string) {
|
||||
throw new NotImplementedByEngineError();
|
||||
}
|
||||
@@ -350,6 +380,10 @@ export abstract class WhatsappSession {
|
||||
throw new NotImplementedByEngineError();
|
||||
}
|
||||
|
||||
public chatsUnreadChat(chatId: string): Promise<any> {
|
||||
throw new NotImplementedByEngineError();
|
||||
}
|
||||
|
||||
/**
|
||||
* Labels methods
|
||||
*/
|
||||
@@ -377,7 +411,7 @@ export abstract class WhatsappSession {
|
||||
throw new NotImplementedByEngineError();
|
||||
}
|
||||
|
||||
public getContacts() {
|
||||
public getContacts(pagination: PaginationParams) {
|
||||
throw new NotImplementedByEngineError();
|
||||
}
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ import { WhatsappSession } from './session.abc';
|
||||
export abstract class WebhookSender {
|
||||
protected url: string;
|
||||
protected logger: Logger;
|
||||
protected readonly config: WebhookConfig;
|
||||
|
||||
constructor(
|
||||
loggerBuilder: LoggerBuilder,
|
||||
@@ -14,9 +15,10 @@ export abstract class WebhookSender {
|
||||
) {
|
||||
this.url = webhookConfig.url;
|
||||
this.logger = loggerBuilder.child({ name: WebhookSender.name });
|
||||
this.config = webhookConfig;
|
||||
}
|
||||
|
||||
abstract send(json);
|
||||
abstract send(json: any): void;
|
||||
}
|
||||
|
||||
export abstract class WebhookConductor {
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
ServerDebugController,
|
||||
} from '@waha/api/server.controller';
|
||||
import { WebsocketGatewayCore } from '@waha/core/api/websocket.gateway.core';
|
||||
import { MediaLocalStorageModule } from '@waha/core/media/local/media.local.storage.module';
|
||||
import { MediaLocalStorageConfig } from '@waha/core/media/local/MediaLocalStorageConfig';
|
||||
import { BufferJsonReplacerInterceptor } from '@waha/nestjs/BufferJsonReplacerInterceptor';
|
||||
import {
|
||||
@@ -44,7 +45,7 @@ import { SwaggerConfigServiceCore } from './config/SwaggerConfigServiceCore';
|
||||
import { WAHAHealthCheckServiceCore } from './health/WAHAHealthCheckServiceCore';
|
||||
import { SessionManagerCore } from './manager.core';
|
||||
|
||||
export const IMPORTS = [
|
||||
export const IMPORTS_CORE = [
|
||||
LoggerModule.forRoot({
|
||||
renameContext: 'name',
|
||||
pinoHttp: {
|
||||
@@ -100,6 +101,18 @@ export const IMPORTS = [
|
||||
PassportModule,
|
||||
TerminusModule,
|
||||
];
|
||||
|
||||
const IMPORTS_MEDIA = [
|
||||
ConfigModule.forRoot({
|
||||
validationSchema: Joi.object({
|
||||
WAHA_MEDIA_STORAGE: Joi.string().valid('LOCAL', 'S3').default('LOCAL'),
|
||||
}),
|
||||
}),
|
||||
MediaLocalStorageModule,
|
||||
];
|
||||
|
||||
const IMPORTS = [...IMPORTS_CORE, ...IMPORTS_MEDIA];
|
||||
|
||||
export const CONTROLLERS = [
|
||||
AuthController,
|
||||
SessionsController,
|
||||
|
||||
@@ -0,0 +1,107 @@
|
||||
import { generateWAMessageFromContent, proto } from '@adiwajshing/baileys';
|
||||
import { Button, ButtonType } from '@waha/structures/chatting.buttons.dto';
|
||||
|
||||
function toName(type: ButtonType) {
|
||||
switch (type) {
|
||||
case ButtonType.REPLY:
|
||||
return 'quick_reply';
|
||||
case ButtonType.URL:
|
||||
return 'cta_url';
|
||||
case ButtonType.CALL:
|
||||
return 'cta_call';
|
||||
case ButtonType.COPY:
|
||||
return 'cta_copy';
|
||||
}
|
||||
}
|
||||
|
||||
export function randomId() {
|
||||
// generate 16 random digits
|
||||
return Math.random().toString().slice(2, 18);
|
||||
}
|
||||
|
||||
export function buttonToJson(button: Button) {
|
||||
const name = toName(button.type);
|
||||
const buttonParams: any = {
|
||||
display_text: button.text,
|
||||
id: button.id || randomId(),
|
||||
disabled: false,
|
||||
};
|
||||
switch (button.type) {
|
||||
case ButtonType.REPLY:
|
||||
break;
|
||||
case ButtonType.CALL:
|
||||
buttonParams.phone_number = button.phoneNumber;
|
||||
break;
|
||||
case ButtonType.COPY:
|
||||
buttonParams.copy_code = button.copyCode;
|
||||
break;
|
||||
case ButtonType.URL:
|
||||
buttonParams.url = button.url;
|
||||
buttonParams.merchant_url = button.url;
|
||||
break;
|
||||
}
|
||||
return {
|
||||
name: name,
|
||||
buttonParamsJson: JSON.stringify(buttonParams),
|
||||
};
|
||||
}
|
||||
|
||||
export async function sendButtonMessage(
|
||||
sock: any,
|
||||
chatId: string,
|
||||
buttons: Button[],
|
||||
header?: string,
|
||||
headerImage?: any,
|
||||
body?: string,
|
||||
footer?: string,
|
||||
) {
|
||||
const data = {
|
||||
viewOnceMessage: {
|
||||
message: {
|
||||
messageContextInfo: {
|
||||
deviceListMetadata: {},
|
||||
deviceListMetadataVersion: 2,
|
||||
},
|
||||
interactiveMessage: {
|
||||
body: undefined,
|
||||
header: undefined,
|
||||
footer: undefined,
|
||||
nativeFlowMessage: {
|
||||
buttons: buttons.map(buttonToJson),
|
||||
messageParamsJson: JSON.stringify({
|
||||
from: 'api',
|
||||
templateId: randomId(),
|
||||
}),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
if (header || headerImage) {
|
||||
data.viewOnceMessage.message.interactiveMessage.header = {
|
||||
title: header,
|
||||
hasMediaAttachment: !!headerImage,
|
||||
imageMessage: headerImage,
|
||||
};
|
||||
}
|
||||
if (body) {
|
||||
data.viewOnceMessage.message.interactiveMessage.body = {
|
||||
text: body,
|
||||
};
|
||||
}
|
||||
if (footer) {
|
||||
data.viewOnceMessage.message.interactiveMessage.footer = {
|
||||
text: footer,
|
||||
};
|
||||
}
|
||||
|
||||
const msg = proto.Message.fromObject(data);
|
||||
const fullMessage = generateWAMessageFromContent(chatId, msg, {
|
||||
userJid: sock?.user?.id,
|
||||
});
|
||||
await sock.relayMessage(chatId, fullMessage.message, {
|
||||
messageId: fullMessage.key.id,
|
||||
});
|
||||
return fullMessage;
|
||||
}
|
||||
@@ -2,6 +2,7 @@ import makeWASocket, {
|
||||
Browsers,
|
||||
Contact,
|
||||
DisconnectReason,
|
||||
downloadMediaMessage,
|
||||
extractMessageContent,
|
||||
getAggregateVotesInPollMessage,
|
||||
getContentType,
|
||||
@@ -25,9 +26,10 @@ import { LabelAssociationType } from '@adiwajshing/baileys/lib/Types/LabelAssoci
|
||||
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 { sendButtonMessage } from '@waha/core/engines/noweb/noweb.buttons';
|
||||
import { NowebInMemoryStore } from '@waha/core/engines/noweb/store/NowebInMemoryStore';
|
||||
import { IMediaEngineProcessor } from '@waha/core/media/IMediaEngineProcessor';
|
||||
import { flipObject, parseBool, splitAt } from '@waha/helpers';
|
||||
import { flipObject, splitAt } from '@waha/helpers';
|
||||
import { PairingCodeResponse } from '@waha/structures/auth.dto';
|
||||
import { CallData } from '@waha/structures/calls.dto';
|
||||
import {
|
||||
@@ -36,36 +38,43 @@ import {
|
||||
CreateChannelRequest,
|
||||
ListChannelsQuery,
|
||||
} from '@waha/structures/channels.dto';
|
||||
import { GetChatsQuery } from '@waha/structures/chats.dto';
|
||||
import {
|
||||
GetChatMessageQuery,
|
||||
GetChatMessagesFilter,
|
||||
GetChatMessagesQuery,
|
||||
} from '@waha/structures/chats.dto';
|
||||
import { SendButtonsRequest } from '@waha/structures/chatting.buttons.dto';
|
||||
import { ContactQuery, ContactRequest } from '@waha/structures/contacts.dto';
|
||||
import { BinaryFile, RemoteFile } from '@waha/structures/files.dto';
|
||||
import {
|
||||
Label,
|
||||
LabelChatAssociation,
|
||||
LabelID,
|
||||
} from '@waha/structures/labels.dto';
|
||||
import { ReplyToMessage } from '@waha/structures/message.dto';
|
||||
import { PaginationParams } from '@waha/structures/pagination.dto';
|
||||
import {
|
||||
PollVote,
|
||||
PollVotePayload,
|
||||
WAMessageAckBody,
|
||||
} from '@waha/structures/webhooks.dto';
|
||||
import { waitUntil } from '@waha/utils/promiseTimeout';
|
||||
import { LoggerBuilder } from '@waha/utils/logging';
|
||||
import { sleep, waitUntil } from '@waha/utils/promiseTimeout';
|
||||
import { SingleDelayedJobRunner } from '@waha/utils/SingleDelayedJobRunner';
|
||||
import { SinglePeriodicJobRunner } from '@waha/utils/SinglePeriodicJobRunner';
|
||||
import * as Buffer from 'buffer';
|
||||
import { Agent } from 'https';
|
||||
import * as lodash from 'lodash';
|
||||
import { toNumber } from 'lodash';
|
||||
import * as NodeCache from 'node-cache';
|
||||
|
||||
import {
|
||||
ChatRequest,
|
||||
CheckNumberStatusQuery,
|
||||
EditMessageRequest,
|
||||
GetMessageQuery,
|
||||
MessageContactVcardRequest,
|
||||
MessageDestination,
|
||||
MessageFileRequest,
|
||||
MessageForwardRequest,
|
||||
MessageImageRequest,
|
||||
MessageLinkPreviewRequest,
|
||||
MessageLocationRequest,
|
||||
@@ -161,6 +170,8 @@ export class WhatsappSessionNoWebCore extends WhatsappSession {
|
||||
private msgRetryCounterCache: NodeCache;
|
||||
protected engineLogger: BaileysLogger;
|
||||
|
||||
private authNOWEBStore: any;
|
||||
|
||||
get listenConnectionEventsFromTheStart() {
|
||||
return true;
|
||||
}
|
||||
@@ -200,11 +211,23 @@ export class WhatsappSessionNoWebCore extends WhatsappSession {
|
||||
delay * SECOND,
|
||||
this.logger,
|
||||
);
|
||||
this.authNOWEBStore = null;
|
||||
}
|
||||
|
||||
async start() {
|
||||
this.status = WAHASessionStatus.STARTING;
|
||||
await this.buildClient();
|
||||
await this.buildClient().catch((err) => {
|
||||
this.logger.error('Failed to start the client');
|
||||
this.logger.error(err, err.stack);
|
||||
this.status = WAHASessionStatus.FAILED;
|
||||
this.restartClient();
|
||||
});
|
||||
}
|
||||
|
||||
async unpair() {
|
||||
this.unpairing = true;
|
||||
this.shouldRestart = false;
|
||||
await this.sock?.logout();
|
||||
}
|
||||
|
||||
getSocketConfig(agent, state): any {
|
||||
@@ -238,10 +261,13 @@ export class WhatsappSessionNoWebCore extends WhatsappSession {
|
||||
}
|
||||
|
||||
async makeSocket(): Promise<any> {
|
||||
const { state, saveCreds } = await this.authFactory.buildAuth(
|
||||
this.sessionStore,
|
||||
this.name,
|
||||
);
|
||||
if (!this.authNOWEBStore) {
|
||||
this.authNOWEBStore = await this.authFactory.buildAuth(
|
||||
this.sessionStore,
|
||||
this.name,
|
||||
);
|
||||
}
|
||||
const { state, saveCreds } = this.authNOWEBStore;
|
||||
const agent = this.makeAgent();
|
||||
const socketConfig = this.getSocketConfig(agent, state);
|
||||
const sock = makeWASocket(socketConfig);
|
||||
@@ -271,12 +297,7 @@ export class WhatsappSessionNoWebCore extends WhatsappSession {
|
||||
this.loggerBuilder.child({ name: NowebPersistentStore.name }),
|
||||
storage,
|
||||
);
|
||||
await this.store.init().catch((err) => {
|
||||
this.logger.error(`Failed to initialize storage or store: ${err}`);
|
||||
this.status = WAHASessionStatus.FAILED;
|
||||
this.end();
|
||||
throw err;
|
||||
});
|
||||
await this.store.init();
|
||||
} else {
|
||||
this.logger.debug('Using NowebInMemoryStore');
|
||||
this.store = new NowebInMemoryStore();
|
||||
@@ -351,6 +372,7 @@ export class WhatsappSessionNoWebCore extends WhatsappSession {
|
||||
);
|
||||
return;
|
||||
}
|
||||
await this.end();
|
||||
await this.start();
|
||||
});
|
||||
}
|
||||
@@ -384,8 +406,7 @@ export class WhatsappSessionNoWebCore extends WhatsappSession {
|
||||
this.logger.error(
|
||||
`Connection closed due to '${lastDisconnect.error}', do not reconnect the session.`,
|
||||
);
|
||||
await this.end();
|
||||
this.status = WAHASessionStatus.FAILED;
|
||||
await this.failed();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -401,11 +422,40 @@ export class WhatsappSessionNoWebCore extends WhatsappSession {
|
||||
|
||||
async stop() {
|
||||
this.shouldRestart = false;
|
||||
this.startDelayedJob.cancel();
|
||||
this.autoRestartJob.stop();
|
||||
|
||||
if (this.authNOWEBStore && this.status == WAHASessionStatus.WORKING) {
|
||||
this.logger.info('Saving creds before stopping...');
|
||||
await this.authNOWEBStore.saveCreds().catch((e) => {
|
||||
this.logger.error('Failed to save creds');
|
||||
this.logger.error(e, e.stack);
|
||||
});
|
||||
this.logger.info('Creds saved');
|
||||
this.authNOWEBStore = null;
|
||||
}
|
||||
this.status = WAHASessionStatus.STOPPED;
|
||||
this.events.removeAllListeners();
|
||||
this.startDelayedJob.cancel();
|
||||
|
||||
await this.end();
|
||||
return;
|
||||
await this.store?.close();
|
||||
}
|
||||
|
||||
protected async failed() {
|
||||
this.shouldRestart = false;
|
||||
this.startDelayedJob.cancel();
|
||||
this.autoRestartJob.stop();
|
||||
|
||||
// We'll restart the client if it's in the process of unpairing
|
||||
this.status = WAHASessionStatus.FAILED;
|
||||
|
||||
if (this.unpairing) {
|
||||
// Wait for unpairing to complete before ending the socket
|
||||
await sleep(1_000);
|
||||
}
|
||||
|
||||
await this.end();
|
||||
await this.store?.close();
|
||||
}
|
||||
|
||||
private issueMessageUpdateOnEdits() {
|
||||
@@ -471,7 +521,6 @@ 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);
|
||||
@@ -641,6 +690,30 @@ export class WhatsappSessionNoWebCore extends WhatsappSession {
|
||||
throw new AvailableInPlusVersion();
|
||||
}
|
||||
|
||||
protected async uploadMedia(
|
||||
file: RemoteFile | BinaryFile,
|
||||
type,
|
||||
): Promise<any> {
|
||||
if (file && ('url' in file || 'data' in file)) {
|
||||
throw new AvailableInPlusVersion('Sending media (image, video, pdf)');
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
async sendButtons(request: SendButtonsRequest) {
|
||||
const chatId = toJID(this.ensureSuffix(request.chatId));
|
||||
const headerImage = await this.uploadMedia(request.headerImage, 'image');
|
||||
return await sendButtonMessage(
|
||||
this.sock,
|
||||
chatId,
|
||||
request.buttons,
|
||||
request.header,
|
||||
headerImage,
|
||||
request.body,
|
||||
request.footer,
|
||||
);
|
||||
}
|
||||
|
||||
sendLocation(request: MessageLocationRequest) {
|
||||
return this.sock.sendMessage(request.chatId, {
|
||||
location: {
|
||||
@@ -650,6 +723,23 @@ export class WhatsappSessionNoWebCore extends WhatsappSession {
|
||||
});
|
||||
}
|
||||
|
||||
async forwardMessage(request: MessageForwardRequest): Promise<WAMessage> {
|
||||
const key = parseMessageIdSerialized(request.messageId);
|
||||
const forwardMessage = await this.store.loadMessage(key.remoteJid, key.id);
|
||||
if (!forwardMessage) {
|
||||
throw new UnprocessableEntityException(
|
||||
`Message with id '${request.messageId}' not found`,
|
||||
);
|
||||
}
|
||||
const chatId = toJID(this.ensureSuffix(request.chatId));
|
||||
const message = {
|
||||
forward: forwardMessage,
|
||||
force: true,
|
||||
};
|
||||
const result = await this.sock.sendMessage(chatId, message, {});
|
||||
return this.toWAMessage(result);
|
||||
}
|
||||
|
||||
sendLinkPreview(request: MessageLinkPreviewRequest) {
|
||||
const text = `${request.title}\n${request.url}`;
|
||||
const chatId = toJID(this.ensureSuffix(request.chatId));
|
||||
@@ -679,19 +769,17 @@ export class WhatsappSessionNoWebCore extends WhatsappSession {
|
||||
return this.sock.sendPresenceUpdate('paused', chatId);
|
||||
}
|
||||
|
||||
async getMessages(query: GetMessageQuery) {
|
||||
return this.getChatMessages(query.chatId, query.limit, query.downloadMedia);
|
||||
}
|
||||
|
||||
public async getChatMessages(
|
||||
chatId: string,
|
||||
limit: number,
|
||||
downloadMedia: boolean,
|
||||
query: GetChatMessagesQuery,
|
||||
filter: GetChatMessagesFilter,
|
||||
) {
|
||||
downloadMedia = parseBool(downloadMedia);
|
||||
const downloadMedia = query.downloadMedia;
|
||||
const pagination = query as PaginationParams;
|
||||
const messages = await this.store.getMessagesByJid(
|
||||
toJID(chatId),
|
||||
toNumber(limit),
|
||||
filter,
|
||||
pagination,
|
||||
);
|
||||
|
||||
const promises = [];
|
||||
@@ -703,6 +791,17 @@ export class WhatsappSessionNoWebCore extends WhatsappSession {
|
||||
return result;
|
||||
}
|
||||
|
||||
public async getChatMessage(
|
||||
chatId: string,
|
||||
messageId: string,
|
||||
query: GetChatMessageQuery,
|
||||
): Promise<null | WAMessage> {
|
||||
const key = parseMessageIdSerialized(messageId, true);
|
||||
const message = await this.store.getMessageById(toJID(chatId), key.id);
|
||||
if (!message) return null;
|
||||
return await this.processIncomingMessage(message, query.downloadMedia);
|
||||
}
|
||||
|
||||
async setReaction(request: MessageReactionRequest) {
|
||||
const key = parseMessageIdSerialized(request.messageId);
|
||||
const reactionMessage = {
|
||||
@@ -731,8 +830,8 @@ export class WhatsappSessionNoWebCore extends WhatsappSession {
|
||||
* Chats methods
|
||||
*/
|
||||
|
||||
async getChats(query: GetChatsQuery) {
|
||||
const chats = await this.store.getChats(query.limit, query.offset);
|
||||
async getChats(pagination: PaginationParams) {
|
||||
const chats = await this.store.getChats(pagination);
|
||||
// Remove unreadCount, it's not ready yet
|
||||
chats.forEach((chat) => delete chat.unreadCount);
|
||||
return chats;
|
||||
@@ -743,7 +842,7 @@ export class WhatsappSessionNoWebCore extends WhatsappSession {
|
||||
archive: boolean,
|
||||
): Promise<any> {
|
||||
const jid = toJID(chatId);
|
||||
const messages = await this.store.getMessagesByJid(jid, 1);
|
||||
const messages = await this.store.getMessagesByJid(jid, {}, { limit: 1 });
|
||||
return await this.sock.chatModify(
|
||||
{ archive: archive, lastMessages: messages },
|
||||
jid,
|
||||
@@ -758,6 +857,15 @@ export class WhatsappSessionNoWebCore extends WhatsappSession {
|
||||
return this.chatsPutArchive(chatId, false);
|
||||
}
|
||||
|
||||
public async chatsUnreadChat(chatId: string): Promise<any> {
|
||||
const jid = toJID(chatId);
|
||||
const messages = await this.store.getMessagesByJid(jid, {}, { limit: 1 });
|
||||
return await this.sock.chatModify(
|
||||
{ markRead: false, lastMessages: messages },
|
||||
jid,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Labels methods
|
||||
*/
|
||||
@@ -817,8 +925,8 @@ export class WhatsappSessionNoWebCore extends WhatsappSession {
|
||||
return this.toWAContact(contact);
|
||||
}
|
||||
|
||||
async getContacts() {
|
||||
const contacts = await this.store.getContacts();
|
||||
async getContacts(pagination: PaginationParams) {
|
||||
const contacts = await this.store.getContacts(pagination);
|
||||
return contacts.map(this.toWAContact);
|
||||
}
|
||||
|
||||
@@ -1329,6 +1437,10 @@ export class WhatsappSessionNoWebCore extends WhatsappSession {
|
||||
// @ts-ignore - AudioMessage doesn't have caption field
|
||||
body = mediaContent?.caption;
|
||||
}
|
||||
// Response for buttons
|
||||
if (!body) {
|
||||
body = message.templateButtonReplyMessage?.selectedDisplayText;
|
||||
}
|
||||
return body;
|
||||
}
|
||||
|
||||
@@ -1509,7 +1621,7 @@ export class WhatsappSessionNoWebCore extends WhatsappSession {
|
||||
}
|
||||
|
||||
protected downloadMedia(message) {
|
||||
const processor = new EngineMediaProcessor(this);
|
||||
const processor = new NOWEBEngineMediaProcessor(this, this.loggerBuilder);
|
||||
return this.mediaManager.processMedia(processor, message, this.name);
|
||||
}
|
||||
|
||||
@@ -1526,23 +1638,66 @@ export class WhatsappSessionNoWebCore extends WhatsappSession {
|
||||
}
|
||||
}
|
||||
|
||||
export class EngineMediaProcessor implements IMediaEngineProcessor<any> {
|
||||
constructor(public session: WhatsappSessionNoWebCore) {}
|
||||
function hasPath(url: string) {
|
||||
if (!url) {
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
const urlObj = new URL(url);
|
||||
return urlObj.pathname !== '/';
|
||||
} catch (error) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export class NOWEBEngineMediaProcessor implements IMediaEngineProcessor<any> {
|
||||
private readonly logger: BaileysLogger;
|
||||
|
||||
constructor(
|
||||
public session: WhatsappSessionNoWebCore,
|
||||
loggerBuilder: LoggerBuilder,
|
||||
) {
|
||||
this.logger = loggerBuilder.child({
|
||||
name: NOWEBEngineMediaProcessor.name,
|
||||
}) as unknown as BaileysLogger;
|
||||
}
|
||||
|
||||
hasMedia(message: any): boolean {
|
||||
return Boolean(extractMediaContent(message.message));
|
||||
}
|
||||
|
||||
getMessageId(message: any): string {
|
||||
return '';
|
||||
return message.key.id;
|
||||
}
|
||||
|
||||
getMimetype(message: any): string {
|
||||
return '';
|
||||
const content = extractMediaContent(message.message);
|
||||
return content.mimetype;
|
||||
}
|
||||
|
||||
getMediaBuffer(message: any): Promise<Buffer | null> {
|
||||
return Promise.resolve(undefined);
|
||||
async getMediaBuffer(message: any): Promise<Buffer | null> {
|
||||
const content = extractMediaContent(message.message);
|
||||
// Fix Stickers
|
||||
// https://github.com/devlikeapro/waha/issues/504
|
||||
const url = content.url;
|
||||
if (!hasPath(url)) {
|
||||
// Set it to null so the engine handles it right
|
||||
content.url = null;
|
||||
}
|
||||
|
||||
return (await downloadMediaMessage(
|
||||
message,
|
||||
'buffer',
|
||||
{},
|
||||
{
|
||||
logger: this.logger,
|
||||
reuploadRequest: this.session.sock.updateMediaMessage,
|
||||
},
|
||||
).finally(() => {
|
||||
// Fix Stickers - set url back, just to have it in the response
|
||||
// https://github.com/devlikeapro/waha/issues/504
|
||||
content.url = url;
|
||||
})) as Buffer;
|
||||
}
|
||||
|
||||
getFilename(message: any): string | null {
|
||||
@@ -1580,6 +1735,8 @@ function toCusFormat(remoteJid) {
|
||||
return ensureSuffix(number);
|
||||
}
|
||||
|
||||
export const ALL_JID = 'all@s.whatsapp.net';
|
||||
|
||||
/**
|
||||
* Convert from 11111111111@c.us to 11111111111@s.whatsapp.net
|
||||
* @param chatId
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import { Chat } from '@adiwajshing/baileys';
|
||||
import { PaginationParams } from '@waha/structures/pagination.dto';
|
||||
|
||||
export interface IChatRepository {
|
||||
getAll(): Promise<Chat[]>;
|
||||
|
||||
getAllByIds(ids: string[]): Promise<Chat[]>;
|
||||
|
||||
getAllWithMessages(limit?: number, offset?: number): Promise<Chat[]>;
|
||||
getAllWithMessages(pagination: PaginationParams): Promise<Chat[]>;
|
||||
|
||||
getById(id: string): Promise<Chat | null>;
|
||||
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { Contact } from '@adiwajshing/baileys';
|
||||
import { PaginationParams } from '@waha/structures/pagination.dto';
|
||||
|
||||
export interface IContactRepository {
|
||||
getAll(): Promise<Contact[]>;
|
||||
getAll(pagination?: PaginationParams): Promise<Contact[]>;
|
||||
|
||||
getById(id: string): Promise<Contact | null>;
|
||||
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
import { GetChatMessagesFilter } from '@waha/structures/chats.dto';
|
||||
import { PaginationParams } from '@waha/structures/pagination.dto';
|
||||
|
||||
export interface IMessagesRepository {
|
||||
deleteAll(): Promise<void>;
|
||||
|
||||
@@ -5,7 +8,11 @@ export interface IMessagesRepository {
|
||||
|
||||
upsertOne(message: any): Promise<void>;
|
||||
|
||||
getAllByJid(jid: string, limit: number): Promise<any[]>;
|
||||
getAllByJid(
|
||||
jid: string,
|
||||
filter: GetChatMessagesFilter,
|
||||
pagination: PaginationParams,
|
||||
): Promise<any[]>;
|
||||
|
||||
getByJidById(jid: string, id: string): Promise<any | null>;
|
||||
|
||||
|
||||
@@ -5,6 +5,8 @@ import {
|
||||
proto,
|
||||
} from '@adiwajshing/baileys';
|
||||
import { Label } from '@adiwajshing/baileys/lib/Types/Label';
|
||||
import { GetChatMessagesFilter } from '@waha/structures/chats.dto';
|
||||
import { PaginationParams } from '@waha/structures/pagination.dto';
|
||||
|
||||
export interface INowebStore {
|
||||
presences: any;
|
||||
@@ -17,11 +19,17 @@ export interface INowebStore {
|
||||
|
||||
loadMessage(jid: string, id: string): Promise<proto.IWebMessageInfo>;
|
||||
|
||||
getMessagesByJid(chatId: string, limit: number): Promise<any>;
|
||||
getMessagesByJid(
|
||||
chatId: string,
|
||||
filter: GetChatMessagesFilter,
|
||||
pagination: PaginationParams,
|
||||
): Promise<any>;
|
||||
|
||||
getChats(limit?: number, offset?: number): Promise<Chat[]>;
|
||||
getMessageById(chatId: string, messageId: string): Promise<any>;
|
||||
|
||||
getContacts(): Promise<Contact[]>;
|
||||
getChats(pagination: PaginationParams): Promise<Chat[]>;
|
||||
|
||||
getContacts(pagination: PaginationParams): Promise<Contact[]>;
|
||||
|
||||
getContactById(jid: string): Promise<Contact>;
|
||||
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { Chat, Contact, makeInMemoryStore, proto } from '@adiwajshing/baileys';
|
||||
import { Label } from '@adiwajshing/baileys/lib/Types/Label';
|
||||
import { BadRequestException } from '@nestjs/common';
|
||||
import { GetChatMessagesFilter } from '@waha/structures/chats.dto';
|
||||
import { PaginationParams } from '@waha/structures/pagination.dto';
|
||||
|
||||
import { INowebStore } from './INowebStore';
|
||||
|
||||
@@ -37,15 +39,23 @@ export class NowebInMemoryStore implements INowebStore {
|
||||
return this.store.loadMessage(jid, id);
|
||||
}
|
||||
|
||||
getMessagesByJid(chatId: string, limit: number): Promise<any> {
|
||||
getMessagesByJid(
|
||||
chatId: string,
|
||||
filter: GetChatMessagesFilter,
|
||||
pagination: PaginationParams,
|
||||
): Promise<any> {
|
||||
throw new BadRequestException(this.errorMessage);
|
||||
}
|
||||
|
||||
getChats(limit?: number, offset?: number): Promise<Chat[]> {
|
||||
getMessageById(chatId: string, messageId: string): Promise<any> {
|
||||
throw new BadRequestException(this.errorMessage);
|
||||
}
|
||||
|
||||
getContacts(): Promise<Contact[]> {
|
||||
getChats(pagination: PaginationParams): Promise<Chat[]> {
|
||||
throw new BadRequestException(this.errorMessage);
|
||||
}
|
||||
|
||||
getContacts(pagination: PaginationParams): Promise<Contact[]> {
|
||||
throw new BadRequestException(this.errorMessage);
|
||||
}
|
||||
|
||||
|
||||
@@ -16,6 +16,8 @@ import {
|
||||
} from '@adiwajshing/baileys/lib/Types/LabelAssociation';
|
||||
import { ILabelAssociationRepository } from '@waha/core/engines/noweb/store/ILabelAssociationsRepository';
|
||||
import { ILabelsRepository } from '@waha/core/engines/noweb/store/ILabelsRepository';
|
||||
import { GetChatMessagesFilter } from '@waha/structures/chats.dto';
|
||||
import { PaginationParams, SortOrder } from '@waha/structures/pagination.dto';
|
||||
import { toNumber } from 'lodash';
|
||||
import { Logger } from 'pino';
|
||||
|
||||
@@ -322,20 +324,32 @@ export class NowebPersistentStore implements INowebStore {
|
||||
return proto.WebMessageInfo.fromObject(data);
|
||||
}
|
||||
|
||||
getMessagesByJid(chatId: string, limit: number) {
|
||||
return this.messagesRepo.getAllByJid(toJID(chatId), toNumber(limit));
|
||||
getMessagesByJid(
|
||||
chatId: string,
|
||||
filter: GetChatMessagesFilter,
|
||||
pagination: PaginationParams,
|
||||
): Promise<any> {
|
||||
pagination.sortBy = 'messageTimestamp';
|
||||
pagination.sortOrder = SortOrder.DESC;
|
||||
return this.messagesRepo.getAllByJid(chatId, filter, pagination);
|
||||
}
|
||||
|
||||
getChats(limit?: number, offset?: number): Promise<Chat[]> {
|
||||
return this.chatRepo.getAllWithMessages(limit, offset);
|
||||
getMessageById(chatId: string, messageId: string): Promise<any> {
|
||||
return this.messagesRepo.getByJidById(chatId, messageId);
|
||||
}
|
||||
|
||||
getChats(pagination: PaginationParams): Promise<Chat[]> {
|
||||
pagination.sortBy ||= 'conversationTimestamp';
|
||||
pagination.sortOrder ||= SortOrder.DESC;
|
||||
return this.chatRepo.getAllWithMessages(pagination);
|
||||
}
|
||||
|
||||
getContactById(jid) {
|
||||
return this.contactRepo.getById(jid);
|
||||
}
|
||||
|
||||
getContacts() {
|
||||
return this.contactRepo.getAll();
|
||||
getContacts(pagination: PaginationParams) {
|
||||
return this.contactRepo.getAll(pagination);
|
||||
}
|
||||
|
||||
getLabels(): Promise<Label[]> {
|
||||
|
||||
@@ -1,23 +1,24 @@
|
||||
import { Chat } from '@adiwajshing/baileys';
|
||||
import { PaginationParams } from '@waha/structures/pagination.dto';
|
||||
import { KnexPaginator } from '@waha/utils/Paginator';
|
||||
|
||||
import { IChatRepository } from '../IChatRepository';
|
||||
import { NOWEBSqlite3KVRepository } from './NOWEBSqlite3KVRepository';
|
||||
|
||||
class ChatPaginator extends KnexPaginator {
|
||||
indexes = ['id', 'conversationTimestamp'];
|
||||
}
|
||||
|
||||
export class Sqlite3ChatRepository
|
||||
extends NOWEBSqlite3KVRepository<Chat>
|
||||
implements IChatRepository
|
||||
{
|
||||
async getAllWithMessages(limit?: number, offset?: number): Promise<Chat[]> {
|
||||
protected Paginator = ChatPaginator;
|
||||
|
||||
async getAllWithMessages(pagination: PaginationParams): Promise<Chat[]> {
|
||||
// Get chats with conversationTimestamp is not Null
|
||||
let query = this.select()
|
||||
.whereNotNull('conversationTimestamp')
|
||||
.orderBy('conversationTimestamp', 'desc');
|
||||
if (limit != null) {
|
||||
query = query.limit(limit);
|
||||
}
|
||||
if (offset != null) {
|
||||
query = query.offset(offset);
|
||||
}
|
||||
let query = this.select().whereNotNull('conversationTimestamp');
|
||||
query = this.pagination(query, pagination);
|
||||
return await this.all(query);
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,16 @@
|
||||
import { Contact } from '@adiwajshing/baileys';
|
||||
import { KnexPaginator } from '@waha/utils/Paginator';
|
||||
|
||||
import { IContactRepository } from '../IContactRepository';
|
||||
import { NOWEBSqlite3KVRepository } from './NOWEBSqlite3KVRepository';
|
||||
|
||||
class ContactPaginator extends KnexPaginator {
|
||||
indexes = ['id'];
|
||||
}
|
||||
|
||||
export class Sqlite3ContactRepository
|
||||
extends NOWEBSqlite3KVRepository<Contact>
|
||||
implements IContactRepository {}
|
||||
implements IContactRepository
|
||||
{
|
||||
protected Paginator = ContactPaginator;
|
||||
}
|
||||
@@ -1,3 +1,7 @@
|
||||
import { ALL_JID } from '@waha/core/engines/noweb/session.noweb.core';
|
||||
import { GetChatMessagesFilter } from '@waha/structures/chats.dto';
|
||||
import { PaginationParams } from '@waha/structures/pagination.dto';
|
||||
|
||||
import { IMessagesRepository } from '../IMessagesRepository';
|
||||
import { NOWEBSqlite3KVRepository } from './NOWEBSqlite3KVRepository';
|
||||
|
||||
@@ -9,11 +13,36 @@ export class Sqlite3MessagesRepository
|
||||
return this.upsertMany(messages);
|
||||
}
|
||||
|
||||
getAllByJid(jid: string, limit: number): Promise<any[]> {
|
||||
const query = this.select()
|
||||
.where({ jid: jid })
|
||||
.limit(limit)
|
||||
.orderBy('messageTimestamp', 'DESC');
|
||||
async getAllByJid(
|
||||
jid: string,
|
||||
filter: GetChatMessagesFilter,
|
||||
pagination: PaginationParams,
|
||||
): Promise<any[]> {
|
||||
let query = this.select();
|
||||
if (jid !== ALL_JID) {
|
||||
query = this.select().where({ jid: jid });
|
||||
}
|
||||
if (filter['filter.timestamp.lte'] != null) {
|
||||
query = query.where(
|
||||
'messageTimestamp',
|
||||
'<=',
|
||||
filter['filter.timestamp.lte'],
|
||||
);
|
||||
}
|
||||
if (filter['filter.timestamp.gte'] != null) {
|
||||
query = query.where(
|
||||
'messageTimestamp',
|
||||
'>=',
|
||||
filter['filter.timestamp.gte'],
|
||||
);
|
||||
}
|
||||
if (filter['filter.fromMe'] != null) {
|
||||
// filter by data->"$.key.fromMe"
|
||||
query = query.whereRaw("data->'$.key.fromMe' = ?", [
|
||||
filter['filter.fromMe'] ? 'true' : 'false',
|
||||
]);
|
||||
}
|
||||
query = this.pagination(query, pagination);
|
||||
return this.all(query);
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,141 @@
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import pino, { Logger } from 'pino';
|
||||
import { AuthStrategy, Client } from 'whatsapp-web.js';
|
||||
|
||||
async function isValidPath(path: string) {
|
||||
try {
|
||||
await fs.promises.access(path);
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export class LocalAuth implements AuthStrategy {
|
||||
private readonly logger: Logger;
|
||||
|
||||
private client: Client;
|
||||
private readonly clientId: string;
|
||||
private readonly dataPath: string;
|
||||
private readonly rmMaxRetries: number;
|
||||
|
||||
private userDataDir: string;
|
||||
|
||||
setup(client: Client) {
|
||||
this.client = client;
|
||||
}
|
||||
|
||||
constructor({ clientId, dataPath, rmMaxRetries, logger }) {
|
||||
const idRegex = /^[-_\w]+$/i;
|
||||
if (clientId && !idRegex.test(clientId)) {
|
||||
throw new Error(
|
||||
'Invalid clientId. Only alphanumeric characters, underscores and hyphens are allowed.',
|
||||
);
|
||||
}
|
||||
|
||||
this.dataPath = path.resolve(dataPath || './.wwebjs_auth/');
|
||||
this.clientId = clientId;
|
||||
this.rmMaxRetries = rmMaxRetries ?? 4;
|
||||
this.logger = logger || pino({ name: LocalAuth.name });
|
||||
}
|
||||
|
||||
async beforeBrowserInitialized() {
|
||||
// @ts-ignore
|
||||
const puppeteerOpts = this.client.options.puppeteer;
|
||||
const sessionDirName = this.clientId
|
||||
? `session-${this.clientId}`
|
||||
: 'session';
|
||||
const dirPath = path.join(this.dataPath, sessionDirName);
|
||||
|
||||
if (puppeteerOpts.userDataDir && puppeteerOpts.userDataDir !== dirPath) {
|
||||
throw new Error(
|
||||
'LocalAuth is not compatible with a user-supplied userDataDir.',
|
||||
);
|
||||
}
|
||||
|
||||
fs.mkdirSync(dirPath, { recursive: true });
|
||||
|
||||
// @ts-ignore
|
||||
this.client.options.puppeteer = {
|
||||
...puppeteerOpts,
|
||||
userDataDir: dirPath,
|
||||
};
|
||||
|
||||
this.userDataDir = dirPath;
|
||||
await this.removeSingletonFiles(dirPath);
|
||||
}
|
||||
|
||||
/**
|
||||
* Find in direction Singleton* files and try to remove it
|
||||
* Fix for SingletonLock and other files
|
||||
*/
|
||||
private async removeSingletonFiles(dir: string) {
|
||||
const files = await fs.promises.readdir(dir);
|
||||
for (const file of files) {
|
||||
if (file.startsWith('Singleton')) {
|
||||
const filePath = path.join(dir, file);
|
||||
try {
|
||||
await fs.promises.rm(filePath, {
|
||||
maxRetries: this.rmMaxRetries,
|
||||
recursive: true,
|
||||
force: true,
|
||||
});
|
||||
} catch (err) {
|
||||
this.logger.error(err, `Error deleting: ${filePath}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async logout() {
|
||||
if (this.userDataDir) {
|
||||
await this.removePathSilently(this.userDataDir);
|
||||
}
|
||||
}
|
||||
|
||||
private async removePathSilently(path: string) {
|
||||
const exists = await isValidPath(path);
|
||||
if (!exists) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await fs.promises.rm(path, {
|
||||
maxRetries: this.rmMaxRetries,
|
||||
recursive: true,
|
||||
force: true,
|
||||
});
|
||||
} catch (err) {
|
||||
this.logger.error(err, `Error deleting: ${path}`);
|
||||
}
|
||||
}
|
||||
|
||||
async afterBrowserInitialized() {
|
||||
return;
|
||||
}
|
||||
|
||||
async onAuthenticationNeeded() {
|
||||
return {
|
||||
failed: false,
|
||||
restart: false,
|
||||
failureEventPayload: undefined,
|
||||
};
|
||||
}
|
||||
|
||||
async getAuthEventPayload() {
|
||||
return;
|
||||
}
|
||||
|
||||
async afterAuthReady() {
|
||||
return;
|
||||
}
|
||||
|
||||
async disconnect() {
|
||||
return;
|
||||
}
|
||||
|
||||
async destroy() {
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
exports.LoadUtils = () => {
|
||||
window.WAHA = {};
|
||||
|
||||
window.WAHA.getChats = async (limit, offset) => {
|
||||
let chats = window.Store.Chat.getModelsArray();
|
||||
if (limit || offset) {
|
||||
offset = offset || 0;
|
||||
limit = limit || Infinity;
|
||||
chats = chats.slice(offset, offset + limit);
|
||||
}
|
||||
|
||||
const chatPromises = chats.map((chat) => window.WWebJS.getChatModel(chat));
|
||||
return await Promise.all(chatPromises);
|
||||
};
|
||||
};
|
||||
@@ -1,7 +1,17 @@
|
||||
import { GetChatMessagesFilter } from '@waha/structures/chats.dto';
|
||||
import { PaginationParams } from '@waha/structures/pagination.dto';
|
||||
import * as lodash from 'lodash';
|
||||
import { Client } from 'whatsapp-web.js';
|
||||
import { Message } from 'whatsapp-web.js/src/structures';
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
const { LoadUtils } = require('./WahaInjected');
|
||||
const { LoadWAHA } = require('./_WAHA.js');
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
const { LoadLodash } = require('./_lodash.js');
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
const { LoadPaginator } = require('./_Paginator.js');
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
const ChatFactory = require('whatsapp-web.js/src/factories/ChatFactory');
|
||||
@@ -12,27 +22,110 @@ export class WebjsClient extends Client {
|
||||
// We can still call and override it
|
||||
// @ts-ignore
|
||||
await super.inject();
|
||||
//Load util functions (serializers, helper functions)
|
||||
await this.pupPage.evaluate(LoadUtils);
|
||||
|
||||
// Load util functions (serializers, helper functions)
|
||||
await this.pupPage.evaluate(LoadLodash);
|
||||
await this.pupPage.evaluate(LoadPaginator);
|
||||
await this.pupPage.evaluate(LoadWAHA);
|
||||
}
|
||||
|
||||
async getChats(limit?: number, offset?: number) {
|
||||
if (limit == null && offset == null) {
|
||||
async unpair() {
|
||||
await this.pupPage.evaluate(async () => {
|
||||
if (
|
||||
// @ts-ignore
|
||||
window.Store &&
|
||||
// @ts-ignore
|
||||
window.Store.AppState &&
|
||||
// @ts-ignore
|
||||
typeof window.Store.AppState.logout === 'function'
|
||||
) {
|
||||
// @ts-ignore
|
||||
await window.Store.AppState.logout();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async getChats(pagination?: PaginationParams) {
|
||||
if (lodash.isEmpty(pagination)) {
|
||||
return await super.getChats();
|
||||
}
|
||||
return await this.getChats2(limit, offset);
|
||||
}
|
||||
|
||||
private async getChats2(limit?: number, offset?: number) {
|
||||
const chats = await this.pupPage.evaluate(
|
||||
async (limit, offset) => {
|
||||
// @ts-ignore
|
||||
return await window.WAHA.getChats(limit, offset);
|
||||
},
|
||||
limit,
|
||||
offset,
|
||||
);
|
||||
// Get paginated chats
|
||||
pagination.limit ||= Infinity;
|
||||
pagination.offset ||= 0;
|
||||
|
||||
const chats = await this.pupPage.evaluate(async (pagination) => {
|
||||
// @ts-ignore
|
||||
return await window.WAHA.getChats(pagination);
|
||||
}, pagination);
|
||||
|
||||
return chats.map((chat) => ChatFactory.create(this, chat));
|
||||
}
|
||||
|
||||
async getMessages(
|
||||
chatId: string,
|
||||
filter: GetChatMessagesFilter,
|
||||
pagination: PaginationParams,
|
||||
) {
|
||||
const messages = await this.pupPage.evaluate(
|
||||
async (chatId, filter, pagination) => {
|
||||
pagination.limit ||= Infinity;
|
||||
pagination.offset ||= 0;
|
||||
|
||||
const msgFilter = (m) => {
|
||||
if (m.isNotification) {
|
||||
return false;
|
||||
}
|
||||
if (
|
||||
filter['filter.fromMe'] != null &&
|
||||
m.id.fromMe !== filter['filter.fromMe']
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
if (
|
||||
filter['filter.timestamp.gte'] != null &&
|
||||
m.t < filter['filter.timestamp.gte']
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
if (
|
||||
filter['filter.timestamp.lte'] != null &&
|
||||
m.t > filter['filter.timestamp.lte']
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
// @ts-ignore
|
||||
const chat = await window.WWebJS.getChat(chatId, { getAsModel: false });
|
||||
let msgs = chat.msgs.getModelsArray().filter(msgFilter);
|
||||
|
||||
while (msgs.length < pagination.limit + pagination.offset) {
|
||||
const loadedMessages =
|
||||
// @ts-ignore
|
||||
await window.Store.ConversationMsgs.loadEarlierMsgs(chat);
|
||||
if (!loadedMessages || !loadedMessages.length) break;
|
||||
msgs = [...loadedMessages.filter(msgFilter), ...msgs];
|
||||
}
|
||||
|
||||
if (msgs.length > pagination.limit + pagination.offset) {
|
||||
// sort by t - new first
|
||||
msgs = msgs.sort((a, b) => b.t - a.t);
|
||||
msgs = msgs.slice(
|
||||
pagination.offset,
|
||||
pagination.limit + pagination.offset,
|
||||
);
|
||||
}
|
||||
|
||||
// @ts-ignore
|
||||
return msgs.map((m) => window.WWebJS.getMessageModel(m));
|
||||
},
|
||||
chatId,
|
||||
filter,
|
||||
pagination,
|
||||
);
|
||||
|
||||
return messages.map((m) => new Message(this, m));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
exports.LoadPaginator = () => {
|
||||
class Paginator {
|
||||
constructor(pagination) {
|
||||
this.pagination = pagination;
|
||||
}
|
||||
|
||||
apply(data) {
|
||||
if (window.lodash.isEmpty(this.pagination)) {
|
||||
return data;
|
||||
}
|
||||
return this.limit(this.sort(data));
|
||||
}
|
||||
|
||||
sort(data) {
|
||||
if (!this.pagination?.sortBy) {
|
||||
return data;
|
||||
}
|
||||
return window.lodash.orderBy(
|
||||
data,
|
||||
[this.pagination.sortBy],
|
||||
[this.pagination.sortOrder || 'asc'],
|
||||
);
|
||||
}
|
||||
|
||||
limit(data) {
|
||||
if (!this.pagination?.limit) {
|
||||
return data;
|
||||
}
|
||||
const offset = this.pagination.offset || 0;
|
||||
const limit = this.pagination.limit || Infinity;
|
||||
return data.slice(offset, offset + limit);
|
||||
}
|
||||
}
|
||||
window.Paginator = Paginator;
|
||||
};
|
||||
@@ -0,0 +1,11 @@
|
||||
exports.LoadWAHA = () => {
|
||||
window.WAHA = {};
|
||||
|
||||
window.WAHA.getChats = async (pagination) => {
|
||||
let chats = window.Store.Chat.getModelsArray().slice();
|
||||
const paginator = new window.Paginator(pagination);
|
||||
chats = paginator.apply(chats);
|
||||
const chatPromises = chats.map((chat) => window.WWebJS.getChatModel(chat));
|
||||
return await Promise.all(chatPromises);
|
||||
};
|
||||
};
|
||||
File diff suppressed because it is too large.
Load diff
@@ -3,6 +3,7 @@ import {
|
||||
WAHAInternalEvent,
|
||||
WhatsappSession,
|
||||
} from '@waha/core/abc/session.abc';
|
||||
import { LocalAuth } from '@waha/core/engines/webjs/LocalAuth';
|
||||
import { WebjsClient } from '@waha/core/engines/webjs/WebjsClient';
|
||||
import {
|
||||
AvailableInPlusVersion,
|
||||
@@ -10,7 +11,7 @@ import {
|
||||
} from '@waha/core/exceptions';
|
||||
import { IMediaEngineProcessor } from '@waha/core/media/IMediaEngineProcessor';
|
||||
import { QR } from '@waha/core/QR';
|
||||
import { parseBool, splitAt } from '@waha/helpers';
|
||||
import { splitAt } from '@waha/helpers';
|
||||
import { PairingCodeResponse } from '@waha/structures/auth.dto';
|
||||
import { CallData } from '@waha/structures/calls.dto';
|
||||
import {
|
||||
@@ -19,13 +20,19 @@ import {
|
||||
CreateChannelRequest,
|
||||
ListChannelsQuery,
|
||||
} from '@waha/structures/channels.dto';
|
||||
import { ChatArchiveEvent, GetChatsQuery } from '@waha/structures/chats.dto';
|
||||
import {
|
||||
ChatArchiveEvent,
|
||||
ChatSortField,
|
||||
GetChatMessageQuery,
|
||||
GetChatMessagesFilter,
|
||||
GetChatMessagesQuery,
|
||||
} from '@waha/structures/chats.dto';
|
||||
import {
|
||||
ChatRequest,
|
||||
CheckNumberStatusQuery,
|
||||
EditMessageRequest,
|
||||
GetMessageQuery,
|
||||
MessageFileRequest,
|
||||
MessageForwardRequest,
|
||||
MessageImageRequest,
|
||||
MessageLocationRequest,
|
||||
MessageReactionRequest,
|
||||
@@ -53,10 +60,12 @@ import {
|
||||
} from '@waha/structures/groups.dto';
|
||||
import { Label, LabelID } from '@waha/structures/labels.dto';
|
||||
import { ReplyToMessage } from '@waha/structures/message.dto';
|
||||
import { PaginationParams } from '@waha/structures/pagination.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 { PaginatorInMemory } from '@waha/utils/Paginator';
|
||||
import { sleep, waitUntil } from '@waha/utils/promiseTimeout';
|
||||
import { SingleDelayedJobRunner } from '@waha/utils/SingleDelayedJobRunner';
|
||||
import {
|
||||
Call,
|
||||
@@ -69,7 +78,9 @@ import {
|
||||
Label as WEBJSLabel,
|
||||
Location,
|
||||
Message,
|
||||
MessageMedia,
|
||||
Reaction,
|
||||
WAState,
|
||||
} from 'whatsapp-web.js';
|
||||
import { Message as MessageInstance } from 'whatsapp-web.js/src/structures';
|
||||
|
||||
@@ -87,6 +98,7 @@ export class WhatsappSessionWebJSCore extends WhatsappSession {
|
||||
protected engineConfig?: WebJSConfig;
|
||||
|
||||
private startDelayedJob: SingleDelayedJobRunner;
|
||||
private engineStateCheckDelayedJob: SingleDelayedJobRunner;
|
||||
private shouldRestart: boolean;
|
||||
|
||||
whatsapp: WebjsClient;
|
||||
@@ -103,6 +115,11 @@ export class WhatsappSessionWebJSCore extends WhatsappSession {
|
||||
this.START_ATTEMPT_DELAY_SECONDS * SECOND,
|
||||
this.logger,
|
||||
);
|
||||
this.engineStateCheckDelayedJob = new SingleDelayedJobRunner(
|
||||
'engine-state-check',
|
||||
2 * SECOND,
|
||||
this.logger,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -135,6 +152,12 @@ export class WhatsappSessionWebJSCore extends WhatsappSession {
|
||||
|
||||
protected async buildClient() {
|
||||
const clientOptions = this.getClientOptions();
|
||||
clientOptions.authStrategy = new LocalAuth({
|
||||
clientId: this.name,
|
||||
dataPath: undefined,
|
||||
logger: this.logger,
|
||||
rmMaxRetries: undefined,
|
||||
});
|
||||
this.addProxyConfig(clientOptions);
|
||||
return new WebjsClient(clientOptions);
|
||||
}
|
||||
@@ -184,15 +207,13 @@ export class WhatsappSessionWebJSCore extends WhatsappSession {
|
||||
.then(() => {
|
||||
// Listen for browser disconnected event
|
||||
this.whatsapp.pupBrowser.on('disconnected', () => {
|
||||
this.status = WAHASessionStatus.FAILED;
|
||||
this.logger.error('The browser has been disconnected');
|
||||
this.restartClient();
|
||||
this.failed();
|
||||
});
|
||||
// Listen for page close event
|
||||
this.whatsapp.pupPage.on('close', () => {
|
||||
this.status = WAHASessionStatus.FAILED;
|
||||
this.logger.error('The WhatsApp Web page has been closed');
|
||||
this.restartClient();
|
||||
this.failed();
|
||||
});
|
||||
|
||||
// Listen for page error event
|
||||
@@ -207,9 +228,8 @@ export class WhatsappSessionWebJSCore extends WhatsappSession {
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
this.status = WAHASessionStatus.FAILED;
|
||||
this.logger.error(error);
|
||||
this.restartClient();
|
||||
this.failed();
|
||||
return;
|
||||
});
|
||||
if (this.isDebugEnabled()) {
|
||||
@@ -221,7 +241,11 @@ export class WhatsappSessionWebJSCore extends WhatsappSession {
|
||||
|
||||
async start() {
|
||||
this.status = WAHASessionStatus.STARTING;
|
||||
await this.init();
|
||||
await this.init().catch((err) => {
|
||||
this.logger.error('Failed to start the client');
|
||||
this.logger.error(err, err.stack);
|
||||
this.failed();
|
||||
});
|
||||
return this;
|
||||
}
|
||||
|
||||
@@ -233,7 +257,22 @@ export class WhatsappSessionWebJSCore extends WhatsappSession {
|
||||
await this.end();
|
||||
}
|
||||
|
||||
protected failed() {
|
||||
// We'll restart the client if it's in the process of unpairing
|
||||
this.status = WAHASessionStatus.FAILED;
|
||||
this.restartClient();
|
||||
}
|
||||
|
||||
async unpair() {
|
||||
this.unpairing = true;
|
||||
this.shouldRestart = false;
|
||||
await this.whatsapp.unpair();
|
||||
// Wait for unpairing to complete
|
||||
await sleep(2_000);
|
||||
}
|
||||
|
||||
private async end() {
|
||||
this.engineStateCheckDelayedJob.cancel();
|
||||
try {
|
||||
this.whatsapp?.removeAllListeners();
|
||||
this.whatsapp?.pupBrowser?.removeAllListeners();
|
||||
@@ -296,22 +335,61 @@ export class WhatsappSessionWebJSCore extends WhatsappSession {
|
||||
this.logger.info(`Session '${this.name}' is ready!`);
|
||||
});
|
||||
|
||||
this.whatsapp.on(Events.AUTHENTICATED, () => {
|
||||
this.whatsapp.on(Events.AUTHENTICATED, (args) => {
|
||||
this.status = WAHASessionStatus.WORKING;
|
||||
this.qr.save('');
|
||||
this.logger.info(`Session '${this.name}' has been authenticated!`);
|
||||
this.logger.info({ args: args }, `Session has been authenticated!`);
|
||||
});
|
||||
|
||||
this.whatsapp.on(Events.AUTHENTICATION_FAILURE, () => {
|
||||
this.status = WAHASessionStatus.FAILED;
|
||||
this.whatsapp.on(Events.AUTHENTICATION_FAILURE, (args) => {
|
||||
this.failed();
|
||||
this.qr.save('');
|
||||
this.logger.info(`Session '${this.name}' has been disconnected!`);
|
||||
this.logger.info({ args: args }, `Session has failed to authenticate!`);
|
||||
});
|
||||
|
||||
this.whatsapp.on(Events.DISCONNECTED, () => {
|
||||
this.status = WAHASessionStatus.FAILED;
|
||||
this.whatsapp.on(Events.DISCONNECTED, (args) => {
|
||||
this.failed();
|
||||
this.qr.save('');
|
||||
this.logger.info(`Session '${this.name}' has been disconnected!`);
|
||||
this.logger.info({ args: args }, `Session has been disconnected!`);
|
||||
});
|
||||
|
||||
this.whatsapp.on(Events.STATE_CHANGED, (state: WAState) => {
|
||||
const badStates = [WAState.OPENING, WAState.TIMEOUT];
|
||||
const log = this.logger.child({ state: state, event: 'change_state' });
|
||||
|
||||
log.info('Session engine state changed');
|
||||
if (!badStates.includes(state)) {
|
||||
return;
|
||||
}
|
||||
|
||||
log.info(`Session state changed to bad state, waiting for recovery...`);
|
||||
this.engineStateCheckDelayedJob.schedule(async () => {
|
||||
if (this.startDelayedJob.scheduled) {
|
||||
log.info('Session is restarting already, skip check.');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.whatsapp) {
|
||||
log.warn('Session is not initialized, skip recovery.');
|
||||
return;
|
||||
}
|
||||
|
||||
const currentState = await this.whatsapp.getState().catch((error) => {
|
||||
log.error('Failed to get current state');
|
||||
log.error(error, error.stack);
|
||||
return null;
|
||||
});
|
||||
log.setBindings({ currentState: currentState });
|
||||
if (!currentState) {
|
||||
log.warn('Session has no current state, skip restarting.');
|
||||
return;
|
||||
} else if (badStates.includes(currentState)) {
|
||||
log.info('Session is still in bad state, restarting...');
|
||||
this.restartClient();
|
||||
return;
|
||||
}
|
||||
log.info('Session has recovered, no need to restart.');
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -336,7 +414,7 @@ export class WhatsappSessionWebJSCore extends WhatsappSession {
|
||||
// show it as ABCD-ABCD
|
||||
const parts = splitAt(code, 4);
|
||||
const codeRepr = parts.join('-');
|
||||
this.logger.info(`Your code: ${codeRepr}`);
|
||||
this.logger.debug(`Your code: ${codeRepr}`);
|
||||
return { code: codeRepr };
|
||||
}
|
||||
|
||||
@@ -415,6 +493,15 @@ export class WhatsappSessionWebJSCore extends WhatsappSession {
|
||||
return this.whatsapp.sendMessage(request.chatId, location, options);
|
||||
}
|
||||
|
||||
async forwardMessage(request: MessageForwardRequest): Promise<WAMessage> {
|
||||
const forwardMessage = this.recreateMessage(request.messageId);
|
||||
const msg = await forwardMessage.forward(request.chatId);
|
||||
// Return "sent: true" for now
|
||||
// need to research how to get the data from WebJS
|
||||
// @ts-ignore
|
||||
return { sent: msg || false };
|
||||
}
|
||||
|
||||
async sendSeen(request: SendSeenRequest) {
|
||||
const chat: Chat = await this.whatsapp.getChatById(request.chatId);
|
||||
await chat.sendSeen();
|
||||
@@ -430,10 +517,6 @@ export class WhatsappSessionWebJSCore extends WhatsappSession {
|
||||
await chat.clearState();
|
||||
}
|
||||
|
||||
async getMessages(query: GetMessageQuery) {
|
||||
return this.getChatMessages(query.chatId, query.limit, query.downloadMedia);
|
||||
}
|
||||
|
||||
async setReaction(request: MessageReactionRequest) {
|
||||
const message = this.recreateMessage(request.messageId);
|
||||
return message.react(request.reaction);
|
||||
@@ -462,18 +545,39 @@ export class WhatsappSessionWebJSCore extends WhatsappSession {
|
||||
/**
|
||||
* Chats methods
|
||||
*/
|
||||
getChats(query: GetChatsQuery) {
|
||||
return this.whatsapp.getChats(query.limit, query.offset);
|
||||
getChats(pagination: PaginationParams) {
|
||||
switch (pagination.sortBy) {
|
||||
case ChatSortField.ID:
|
||||
pagination.sortBy = 'id._serialized';
|
||||
break;
|
||||
case ChatSortField.CONVERSATION_TIMESTAMP:
|
||||
pagination.sortBy = 't';
|
||||
break;
|
||||
}
|
||||
return this.whatsapp.getChats(pagination);
|
||||
}
|
||||
|
||||
async getChatMessages(chatId: string, limit: number, downloadMedia: boolean) {
|
||||
downloadMedia = parseBool(downloadMedia);
|
||||
const chat: Chat = await this.whatsapp.getChatById(
|
||||
public async getChatMessages(
|
||||
chatId: string,
|
||||
query: GetChatMessagesQuery,
|
||||
filter: GetChatMessagesFilter,
|
||||
) {
|
||||
if (chatId == 'all') {
|
||||
throw new NotImplementedByEngineError(
|
||||
"Can not get messages from 'all' in WEBJS",
|
||||
);
|
||||
}
|
||||
|
||||
const limit = query.limit;
|
||||
const downloadMedia = query.downloadMedia;
|
||||
// Test there's chat with id
|
||||
await this.whatsapp.getChatById(this.ensureSuffix(chatId));
|
||||
const pagination: PaginationParams = query;
|
||||
const messages = await this.whatsapp.getMessages(
|
||||
this.ensureSuffix(chatId),
|
||||
filter,
|
||||
pagination,
|
||||
);
|
||||
const messages = await chat.fetchMessages({
|
||||
limit: limit,
|
||||
});
|
||||
const promises = [];
|
||||
for (const msg of messages) {
|
||||
promises.push(this.processIncomingMessage(msg, downloadMedia));
|
||||
@@ -483,6 +587,16 @@ export class WhatsappSessionWebJSCore extends WhatsappSession {
|
||||
return result;
|
||||
}
|
||||
|
||||
public async getChatMessage(
|
||||
chatId: string,
|
||||
messageId: string,
|
||||
query: GetChatMessageQuery,
|
||||
): Promise<null | WAMessage> {
|
||||
const message = await this.whatsapp.getMessageById(messageId);
|
||||
if (!message) return null;
|
||||
return await this.processIncomingMessage(message, query.downloadMedia);
|
||||
}
|
||||
|
||||
async deleteChat(chatId) {
|
||||
const chat = await this.whatsapp.getChatById(chatId);
|
||||
return chat.delete();
|
||||
@@ -503,6 +617,11 @@ export class WhatsappSessionWebJSCore extends WhatsappSession {
|
||||
return this.whatsapp.unarchiveChat(id);
|
||||
}
|
||||
|
||||
public chatsUnreadChat(chatId: string): Promise<any> {
|
||||
const id = this.ensureSuffix(chatId);
|
||||
return this.whatsapp.markChatUnread(id);
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* Label methods
|
||||
@@ -548,10 +667,11 @@ export class WhatsappSessionWebJSCore extends WhatsappSession {
|
||||
.then(this.toWAContact);
|
||||
}
|
||||
|
||||
getContacts() {
|
||||
return this.whatsapp
|
||||
.getContacts()
|
||||
.then((contacts) => contacts.map(this.toWAContact));
|
||||
async getContacts(pagination: PaginationParams) {
|
||||
const contactsWEBJS = await this.whatsapp.getContacts();
|
||||
const contacts = contactsWEBJS.map(this.toWAContact);
|
||||
const paginator = new PaginatorInMemory(pagination);
|
||||
return paginator.apply(contacts);
|
||||
}
|
||||
|
||||
public async getContactAbout(query: ContactQuery) {
|
||||
@@ -841,6 +961,11 @@ export class WhatsappSessionWebJSCore extends WhatsappSession {
|
||||
this.processIncomingMessage(message).then(handler),
|
||||
);
|
||||
return true;
|
||||
case WAHAEvents.MESSAGE_WAITING:
|
||||
this.whatsapp.on(Events.MESSAGE_CIPHERTEXT, (message) =>
|
||||
this.processIncomingMessage(message).then(handler),
|
||||
);
|
||||
return true;
|
||||
case WAHAEvents.MESSAGE_REVOKED:
|
||||
this.whatsapp.on(
|
||||
Events.MESSAGE_REVOKED_EVERYONE,
|
||||
@@ -991,7 +1116,7 @@ export class WhatsappSessionWebJSCore extends WhatsappSession {
|
||||
}
|
||||
|
||||
protected downloadMedia(message: Message) {
|
||||
const processor = new EngineMediaProcessor();
|
||||
const processor = new WEBJSEngineMediaProcessor();
|
||||
return this.mediaManager.processMedia(processor, message, this.name);
|
||||
}
|
||||
|
||||
@@ -1008,7 +1133,9 @@ export class WhatsappSessionWebJSCore extends WhatsappSession {
|
||||
}
|
||||
}
|
||||
|
||||
export class EngineMediaProcessor implements IMediaEngineProcessor<Message> {
|
||||
export class WEBJSEngineMediaProcessor
|
||||
implements IMediaEngineProcessor<Message>
|
||||
{
|
||||
hasMedia(message: Message): boolean {
|
||||
if (!message.hasMedia) {
|
||||
return false;
|
||||
@@ -1018,15 +1145,21 @@ export class EngineMediaProcessor implements IMediaEngineProcessor<Message> {
|
||||
}
|
||||
|
||||
getMessageId(message: Message): string {
|
||||
return '';
|
||||
return message.id._serialized;
|
||||
}
|
||||
|
||||
getMimetype(message: Message): string {
|
||||
return '';
|
||||
// @ts-ignore
|
||||
return message.rawData.mimetype;
|
||||
}
|
||||
|
||||
getMediaBuffer(message: Message): Promise<Buffer | null> {
|
||||
return Promise.resolve(undefined);
|
||||
async getMediaBuffer(message: Message): Promise<Buffer | null> {
|
||||
return message.downloadMedia().then((media: MessageMedia) => {
|
||||
if (!media) {
|
||||
return null;
|
||||
}
|
||||
return Buffer.from(media.data, 'base64');
|
||||
});
|
||||
}
|
||||
|
||||
getFilename(message: Message): string | null {
|
||||
|
||||
@@ -6,16 +6,16 @@ export class NotImplementedByEngineError extends UnprocessableEntityException {
|
||||
constructor(msg = '') {
|
||||
let error = 'The method is not implemented by the engine.';
|
||||
if (msg) {
|
||||
error += ` ${msg}`;
|
||||
error = `${msg} ${error}`;
|
||||
}
|
||||
super(error);
|
||||
}
|
||||
}
|
||||
|
||||
export class AvailableInPlusVersion extends UnprocessableEntityException {
|
||||
constructor() {
|
||||
constructor(feature: string = 'The feature') {
|
||||
super(
|
||||
`The feature is available only in Plus version. Check this out: ${DOCS_URL}`,
|
||||
`${feature} is available only in Plus version. Check this out: ${DOCS_URL}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
+39
-17
@@ -1,10 +1,9 @@
|
||||
import {
|
||||
Injectable,
|
||||
LogLevel,
|
||||
NotFoundException,
|
||||
UnprocessableEntityException,
|
||||
} from '@nestjs/common';
|
||||
import { MediaNoopStorage } from '@waha/core/media/MediaNoopStorage';
|
||||
import { MediaStorageFactory } from '@waha/core/media/MediaStorageFactory';
|
||||
import { getPinoLogLevel, LoggerBuilder } from '@waha/utils/logging';
|
||||
import { promiseTimeout, sleep } from '@waha/utils/promiseTimeout';
|
||||
import { EventEmitter } from 'events';
|
||||
@@ -31,7 +30,7 @@ import { WhatsappSessionNoWebCore } from './engines/noweb/session.noweb.core';
|
||||
import { WhatsappSessionWebJSCore } from './engines/webjs/session.webjs.core';
|
||||
import { DOCS_URL } from './exceptions';
|
||||
import { getProxyConfig } from './helpers.proxy';
|
||||
import { MediaManagerCore } from './media/MediaManagerCore';
|
||||
import { MediaManager } from './media/MediaManager';
|
||||
import { LocalSessionAuthRepository } from './storage/LocalSessionAuthRepository';
|
||||
import { LocalStoreCore } from './storage/LocalStoreCore';
|
||||
import { WebhookConductorCore } from './webhooks.core';
|
||||
@@ -43,6 +42,7 @@ export class OnlyDefaultSessionIsAllowed extends UnprocessableEntityException {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
enum DefaultSessionStatus {
|
||||
REMOVED = undefined,
|
||||
STOPPED = null,
|
||||
@@ -64,26 +64,22 @@ export class SessionManagerCore extends SessionManager {
|
||||
protected readonly EngineClass: typeof WhatsappSession;
|
||||
|
||||
constructor(
|
||||
private config: WhatsappConfigService,
|
||||
config: WhatsappConfigService,
|
||||
private engineConfigService: EngineConfigService,
|
||||
private log: PinoLogger,
|
||||
log: PinoLogger,
|
||||
private mediaStorageFactory: MediaStorageFactory,
|
||||
) {
|
||||
super();
|
||||
super(config, log);
|
||||
this.events = new EventEmitter();
|
||||
this.session = DefaultSessionStatus.STOPPED;
|
||||
this.sessionConfig = null;
|
||||
this.log.setContext(SessionManagerCore.name);
|
||||
const engineName = this.engineConfigService.getDefaultEngineName();
|
||||
this.EngineClass = this.getEngine(engineName);
|
||||
this.store = new LocalStoreCore(engineName.toLowerCase());
|
||||
this.sessionAuthRepository = new LocalSessionAuthRepository(this.store);
|
||||
this.startPredefinedSessions();
|
||||
}
|
||||
|
||||
protected startPredefinedSessions() {
|
||||
const startSessions = this.config.startSessions;
|
||||
startSessions.forEach((sessionName) => {
|
||||
this.start(sessionName);
|
||||
this.clearStorage().catch((error) => {
|
||||
this.log.error({ error }, 'Error while clearing storage');
|
||||
});
|
||||
}
|
||||
|
||||
@@ -110,6 +106,13 @@ export class SessionManagerCore extends SessionManager {
|
||||
await this.stop(this.DEFAULT, true);
|
||||
}
|
||||
|
||||
private async clearStorage() {
|
||||
const storage = this.mediaStorageFactory.build(
|
||||
this.log.logger.child({ name: 'Storage' }),
|
||||
);
|
||||
await storage.purge();
|
||||
}
|
||||
|
||||
//
|
||||
// API Methods
|
||||
//
|
||||
@@ -136,14 +139,20 @@ export class SessionManagerCore extends SessionManager {
|
||||
);
|
||||
}
|
||||
this.log.info(`'${name}' - starting session...`);
|
||||
const mediaManager = new MediaManagerCore(
|
||||
new MediaNoopStorage(),
|
||||
this.config.mimetypes,
|
||||
);
|
||||
const logger = this.log.logger.child({ session: name });
|
||||
logger.level = getPinoLogLevel(this.sessionConfig?.debug);
|
||||
const loggerBuilder: LoggerBuilder = logger;
|
||||
|
||||
const storage = this.mediaStorageFactory.build(
|
||||
loggerBuilder.child({ name: 'Storage' }),
|
||||
);
|
||||
await storage.init();
|
||||
const mediaManager = new MediaManager(
|
||||
storage,
|
||||
this.config.mimetypes,
|
||||
loggerBuilder.child({ name: 'MediaManager' }),
|
||||
);
|
||||
|
||||
const webhook = new this.WebhookConductorClass(loggerBuilder);
|
||||
const proxyConfig = this.getProxyConfig();
|
||||
const sessionConfig: SessionParams = {
|
||||
@@ -202,6 +211,19 @@ export class SessionManagerCore extends SessionManager {
|
||||
await sleep(this.SESSION_STOP_TIMEOUT);
|
||||
}
|
||||
|
||||
async unpair(name: string) {
|
||||
if (!this.session) {
|
||||
return;
|
||||
}
|
||||
const session = this.session as WhatsappSession;
|
||||
|
||||
this.log.info('Unpairing the device from account...', { session: name });
|
||||
await session.unpair().catch((err) => {
|
||||
this.log.warn(`Error while unpairing from device: ${err}`);
|
||||
});
|
||||
await sleep(1000);
|
||||
}
|
||||
|
||||
async logout(name: string): Promise<void> {
|
||||
this.onlyDefault(name);
|
||||
await this.sessionAuthRepository.clean(name);
|
||||
|
||||
@@ -0,0 +1,198 @@
|
||||
import { IMediaEngineProcessor } from '@waha/core/media/IMediaEngineProcessor';
|
||||
import { IMediaManager } from '@waha/core/media/IMediaManager';
|
||||
import {
|
||||
IMediaStorage,
|
||||
MediaData,
|
||||
MediaStorageData,
|
||||
} from '@waha/core/media/IMediaStorage';
|
||||
import { WAMedia } from '@waha/structures/media.dto';
|
||||
import { Logger } from 'pino';
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
const mime = require('mime-types');
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
const promiseRetry = require('promise-retry');
|
||||
|
||||
export class MediaManager implements IMediaManager {
|
||||
// https://github.com/IndigoUnited/node-promise-retry
|
||||
RETRY_OPTIONS = {
|
||||
retries: 5,
|
||||
minTimeout: 100,
|
||||
maxTimeout: 500,
|
||||
};
|
||||
|
||||
constructor(
|
||||
private storage: IMediaStorage,
|
||||
private mimetypes: string[],
|
||||
protected log: Logger,
|
||||
) {
|
||||
// Log mimetypes
|
||||
if (this.mimetypes && this.mimetypes.length > 0) {
|
||||
const mimetypes = this.mimetypes.join(',');
|
||||
const msg = `Only '${mimetypes}' mimetypes will be downloaded for the session`;
|
||||
this.log.info(msg);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check that we need to download files with the mimetype
|
||||
*/
|
||||
private shouldProcessMimetype(mimetype: string) {
|
||||
// No specific mimetypes provided - always download
|
||||
if (!this.mimetypes || this.mimetypes.length === 0) {
|
||||
return true;
|
||||
}
|
||||
// Found "right" mimetype in the list of allowed mimetypes - download it
|
||||
return this.mimetypes.some((type) => mimetype.startsWith(type));
|
||||
}
|
||||
|
||||
private async processMediaInternal<Message>(
|
||||
processor: IMediaEngineProcessor<Message>,
|
||||
message: Message,
|
||||
session: string,
|
||||
): Promise<WAMedia | null> {
|
||||
const messageId = processor.getMessageId(message);
|
||||
const mimetype = processor.getMimetype(message);
|
||||
const filename = processor.getFilename(message);
|
||||
if (!this.shouldProcessMimetype(mimetype)) {
|
||||
this.log.info(
|
||||
`The message '${messageId}' has '${mimetype}' mimetype media, skip it.`,
|
||||
);
|
||||
return null;
|
||||
}
|
||||
|
||||
const extension = mime.extension(mimetype);
|
||||
const mediaData: MediaData = {
|
||||
session: session,
|
||||
message: {
|
||||
id: messageId,
|
||||
},
|
||||
file: {
|
||||
extension: extension,
|
||||
filename: filename,
|
||||
},
|
||||
};
|
||||
|
||||
const exists = await this.withRetry('Checking media', () =>
|
||||
this.exists(mediaData),
|
||||
);
|
||||
|
||||
if (!exists) {
|
||||
this.log.info(`The message ${messageId} has media, downloading it...`);
|
||||
// Fetching media
|
||||
const buffer = await this.withRetry('Fetching media', () =>
|
||||
this.fetchMedia(message, processor),
|
||||
);
|
||||
// Saving media
|
||||
await this.withRetry('Saving media', () =>
|
||||
this.saveMedia(buffer, mediaData),
|
||||
);
|
||||
this.log.info(`The media from '${messageId}' has been saved.`);
|
||||
}
|
||||
|
||||
const data = await this.withRetry('Getting media URL', () =>
|
||||
this.getStorageData(mediaData),
|
||||
);
|
||||
return data;
|
||||
}
|
||||
|
||||
async processMedia<Message>(
|
||||
processor: IMediaEngineProcessor<Message>,
|
||||
message: Message,
|
||||
session: string,
|
||||
): Promise<Message> {
|
||||
let messageId: string;
|
||||
try {
|
||||
messageId = processor.getMessageId(message);
|
||||
if (!processor.hasMedia(message)) {
|
||||
return message;
|
||||
}
|
||||
} catch (error) {
|
||||
this.log.error(
|
||||
error,
|
||||
`Error checking if message has media for message '${messageId}'`,
|
||||
);
|
||||
return message;
|
||||
}
|
||||
|
||||
let media: WAMedia = {
|
||||
url: null,
|
||||
filename: null,
|
||||
mimetype: null,
|
||||
};
|
||||
try {
|
||||
media.filename = processor.getFilename(message);
|
||||
media.mimetype = processor.getMimetype(message);
|
||||
media.filename = processor.getFilename(message);
|
||||
const data = await this.processMediaInternal(processor, message, session);
|
||||
media = { ...media, ...data };
|
||||
} catch (err) {
|
||||
this.log.error(err, `Error processing media for message '${messageId}'`);
|
||||
media.error = err;
|
||||
// @ts-ignore
|
||||
media.error.details = `${err.stack}`;
|
||||
}
|
||||
// @ts-ignore
|
||||
message.media = media;
|
||||
return message;
|
||||
}
|
||||
|
||||
private async fetchMedia(
|
||||
message: any,
|
||||
processor: IMediaEngineProcessor<any>,
|
||||
): Promise<Buffer> {
|
||||
const messageId = processor.getMessageId(message);
|
||||
this.log.debug(`Fetching media from WhatsApp message '${messageId}'...`);
|
||||
const buffer = await processor.getMediaBuffer(message);
|
||||
if (!buffer) {
|
||||
throw new Error(
|
||||
`Message '${messageId}' has no media, but it has media flag in the engine`,
|
||||
);
|
||||
}
|
||||
return buffer;
|
||||
}
|
||||
|
||||
private async saveMedia(
|
||||
buffer: Buffer,
|
||||
mediaData: MediaData,
|
||||
): Promise<boolean> {
|
||||
this.log.debug(
|
||||
`Saving media from WhatsApp the message '${mediaData.message.id}'...`,
|
||||
);
|
||||
const result = await this.storage.save(buffer, mediaData);
|
||||
this.log.debug(`The media from '${mediaData.message.id}' has been saved.`);
|
||||
return result;
|
||||
}
|
||||
|
||||
private async getStorageData(
|
||||
mediaData: MediaData,
|
||||
): Promise<MediaStorageData> {
|
||||
return await this.storage.getStorageData(mediaData);
|
||||
}
|
||||
|
||||
private async exists(mediaData: MediaData): Promise<boolean> {
|
||||
this.log.trace(
|
||||
`Checking if media exists for message '${mediaData.message.id}'...`,
|
||||
);
|
||||
const result = await this.storage.exists(mediaData);
|
||||
this.log.trace(
|
||||
`Media for message '${mediaData.message.id}' exists: ${result}`,
|
||||
);
|
||||
return result;
|
||||
}
|
||||
|
||||
private async withRetry(name: string, fn: CallableFunction) {
|
||||
const retryOptions = this.RETRY_OPTIONS;
|
||||
try {
|
||||
return await promiseRetry((retry: CallableFunction, number: number) => {
|
||||
return fn().catch(retry);
|
||||
}, retryOptions);
|
||||
} catch (error) {
|
||||
this.log.error(
|
||||
error,
|
||||
`Failed to execute '${name}', tried '${retryOptions.retries}' times`,
|
||||
);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,44 +0,0 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
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,6 @@
|
||||
import { IMediaStorage } from '@waha/core/media/IMediaStorage';
|
||||
import { Logger } from 'pino';
|
||||
|
||||
export abstract class MediaStorageFactory {
|
||||
abstract build(logger: Logger): IMediaStorage;
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
import { IMediaStorage, MediaData } from '@waha/core/media/IMediaStorage';
|
||||
import { SECOND } from '@waha/structures/enums.dto';
|
||||
import * as fsp from 'fs/promises';
|
||||
import * as path from 'path';
|
||||
import { Logger } from 'pino';
|
||||
import { promisify } from 'util';
|
||||
import fs = require('fs');
|
||||
import del = require('del');
|
||||
import { fileExists } from '@waha/utils/files';
|
||||
|
||||
const writeFileAsync = promisify(fs.writeFile);
|
||||
|
||||
/**
|
||||
* Save files locally using the filesystem
|
||||
*/
|
||||
export class MediaLocalStorage implements IMediaStorage {
|
||||
private readonly lifetimeMs: number;
|
||||
|
||||
constructor(
|
||||
protected log: Logger,
|
||||
private filesFolder: string,
|
||||
private baseUrl: string,
|
||||
lifetimeSeconds: number,
|
||||
) {
|
||||
this.lifetimeMs = lifetimeSeconds * SECOND;
|
||||
if (this.lifetimeMs === 0) {
|
||||
this.log.info('Files lifetime is 0, files will not be removed');
|
||||
}
|
||||
}
|
||||
|
||||
async init() {
|
||||
return;
|
||||
}
|
||||
|
||||
async exists(data: MediaData): Promise<boolean> {
|
||||
const filepath = this.getFullPath(data);
|
||||
return await fileExists(filepath);
|
||||
}
|
||||
|
||||
public async save(buffer: Buffer, data: MediaData): Promise<boolean> {
|
||||
const filepath = this.getFullPath(data);
|
||||
const folder = path.dirname(filepath);
|
||||
await fsp.mkdir(folder, { recursive: true });
|
||||
await writeFileAsync(filepath, buffer);
|
||||
this.postponeRemoval(filepath);
|
||||
return true;
|
||||
}
|
||||
|
||||
public async getStorageData(data: MediaData) {
|
||||
const filename = this.getKey(data);
|
||||
const url = this.baseUrl + filename;
|
||||
return { url };
|
||||
}
|
||||
|
||||
async purge() {
|
||||
if (this.lifetimeMs === 0) {
|
||||
this.log.info('No need to purge files with lifetime 0');
|
||||
return;
|
||||
}
|
||||
|
||||
if (fs.existsSync(this.filesFolder)) {
|
||||
del([`${this.filesFolder}/*`], { force: true }).then((paths) => {
|
||||
if (paths.length === 0) {
|
||||
return;
|
||||
}
|
||||
this.log.info('Deleted files and directories:\n', paths.join('\n'));
|
||||
});
|
||||
} else {
|
||||
fs.mkdirSync(this.filesFolder);
|
||||
this.log.info(`Directory '${this.filesFolder}' created from scratch`);
|
||||
}
|
||||
}
|
||||
|
||||
private getKey(data: MediaData) {
|
||||
return `${data.session}/${data.message.id}.${data.file.extension}`;
|
||||
}
|
||||
|
||||
private getFullPath(data: MediaData) {
|
||||
const filepath = this.getKey(data);
|
||||
return path.resolve(`${this.filesFolder}/${filepath}`);
|
||||
}
|
||||
|
||||
private postponeRemoval(filepath: string) {
|
||||
if (this.lifetimeMs === 0) {
|
||||
return;
|
||||
}
|
||||
setTimeout(
|
||||
() =>
|
||||
fs.unlink(filepath, () => {
|
||||
this.log.info(`File ${filepath} was removed`);
|
||||
}),
|
||||
this.lifetimeMs,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -8,7 +8,7 @@ export class MediaLocalStorageConfig {
|
||||
constructor(private config: WhatsappConfigService) {}
|
||||
|
||||
get filesURL(): string {
|
||||
return `${this.config.schema}://${this.config.hostname}:${this.config.port}${this.filesUri}/`;
|
||||
return `${this.config.baseUrl}${this.filesUri}/`;
|
||||
}
|
||||
|
||||
get filesFolder(): string {
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { IMediaStorage } from '@waha/core/media/IMediaStorage';
|
||||
import { MediaLocalStorage } from '@waha/core/media/local/MediaLocalStorage';
|
||||
import { MediaLocalStorageConfig } from '@waha/core/media/local/MediaLocalStorageConfig';
|
||||
import { MediaStorageFactory } from '@waha/core/media/MediaStorageFactory';
|
||||
import { Logger } from 'pino';
|
||||
|
||||
@Injectable()
|
||||
export class MediaLocalStorageFactory extends MediaStorageFactory {
|
||||
constructor(private config: MediaLocalStorageConfig) {
|
||||
super();
|
||||
}
|
||||
|
||||
build(logger: Logger): IMediaStorage {
|
||||
return new MediaLocalStorage(
|
||||
logger,
|
||||
this.config.filesFolder,
|
||||
this.config.filesURL,
|
||||
this.config.filesLifetime,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { ServeStaticModule } from '@nestjs/serve-static';
|
||||
import { WhatsappConfigService } from '@waha/config.service';
|
||||
import { MediaLocalStorageConfig } from '@waha/core/media/local/MediaLocalStorageConfig';
|
||||
import { MediaLocalStorageFactory } from '@waha/core/media/local/MediaLocalStorageFactory';
|
||||
import { MediaStorageFactory } from '@waha/core/media/MediaStorageFactory';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
ServeStaticModule.forRootAsync({
|
||||
imports: [],
|
||||
extraProviders: [MediaLocalStorageConfig, WhatsappConfigService],
|
||||
inject: [MediaLocalStorageConfig],
|
||||
useFactory: (config: MediaLocalStorageConfig) => {
|
||||
return [
|
||||
{
|
||||
rootPath: config.filesFolder,
|
||||
serveRoot: config.filesUri,
|
||||
},
|
||||
];
|
||||
},
|
||||
}),
|
||||
],
|
||||
providers: [
|
||||
{
|
||||
provide: MediaStorageFactory,
|
||||
useClass: MediaLocalStorageFactory,
|
||||
},
|
||||
WhatsappConfigService,
|
||||
MediaLocalStorageConfig,
|
||||
],
|
||||
exports: [MediaStorageFactory],
|
||||
})
|
||||
export class MediaLocalStorageModule {}
|
||||
@@ -0,0 +1,18 @@
|
||||
export class SessionWorkerInfo {
|
||||
id: string; // aka session name here
|
||||
worker: string;
|
||||
}
|
||||
|
||||
export abstract class ISessionWorkerRepository {
|
||||
abstract assign(session: string, worker: string): Promise<void>;
|
||||
|
||||
abstract unassign(session: string, worker: string): Promise<void>;
|
||||
|
||||
abstract remove(session: string): Promise<void>;
|
||||
|
||||
abstract getAll(): Promise<SessionWorkerInfo[]>;
|
||||
|
||||
abstract getSessionsByWorker(worker: string): Promise<string[]>;
|
||||
|
||||
abstract init(): Promise<void>;
|
||||
}
|
||||
@@ -1,8 +1,8 @@
|
||||
import { NOWEBSqlite3KVRepository } from '@waha/core/engines/noweb/store/sqlite3/NOWEBSqlite3KVRepository';
|
||||
import { Sqlite3SchemaValidation } from '@waha/core/engines/noweb/store/sqlite3/Sqlite3SchemaValidation';
|
||||
import { ISessionMeRepository } from '@waha/core/storage/ISessionMeRepository';
|
||||
import { LocalStore } from '@waha/core/storage/LocalStore';
|
||||
import { Field, Index, Schema } from '@waha/core/storage/sqlite3/Schema';
|
||||
import { Sqlite3KVRepository } from '@waha/core/storage/sqlite3/Sqlite3KVRepository';
|
||||
import { MeInfo } from '@waha/structures/sessions.dto';
|
||||
import * as path from 'path';
|
||||
|
||||
@@ -21,7 +21,7 @@ class SessionMeInfo {
|
||||
}
|
||||
|
||||
export class LocalSessionMeRepository
|
||||
extends NOWEBSqlite3KVRepository<SessionMeInfo>
|
||||
extends Sqlite3KVRepository<SessionMeInfo>
|
||||
implements ISessionMeRepository
|
||||
{
|
||||
constructor(store: LocalStore) {
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
import { Sqlite3SchemaValidation } from '@waha/core/engines/noweb/store/sqlite3/Sqlite3SchemaValidation';
|
||||
import {
|
||||
ISessionWorkerRepository,
|
||||
SessionWorkerInfo,
|
||||
} from '@waha/core/storage/ISessionWorkerRepository';
|
||||
import { LocalStore } from '@waha/core/storage/LocalStore';
|
||||
import { Field, Index, Schema } from '@waha/core/storage/sqlite3/Schema';
|
||||
import { Sqlite3KVRepository } from '@waha/core/storage/sqlite3/Sqlite3KVRepository';
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
const Database = require('better-sqlite3');
|
||||
|
||||
const SCHEMA = new Schema(
|
||||
'session_worker',
|
||||
[
|
||||
new Field('id', 'TEXT'),
|
||||
new Field('worker', 'TEXT'),
|
||||
new Field('data', 'TEXT'),
|
||||
],
|
||||
[
|
||||
new Index('session_worker_id_idx', ['id']),
|
||||
new Index('session_worker_worker_idx', ['worker']),
|
||||
],
|
||||
);
|
||||
|
||||
export class LocalSessionWorkerRepository
|
||||
extends Sqlite3KVRepository<SessionWorkerInfo>
|
||||
implements ISessionWorkerRepository
|
||||
{
|
||||
constructor(store: LocalStore) {
|
||||
const db = store.getWAHADatabase();
|
||||
super(db, SCHEMA);
|
||||
}
|
||||
|
||||
assign(session: string, worker: string): Promise<void> {
|
||||
return this.upsertOne({ id: session, worker: worker });
|
||||
}
|
||||
|
||||
unassign(session: string, worker: string): Promise<void> {
|
||||
return this.deleteBy({ id: session, worker: worker });
|
||||
}
|
||||
|
||||
remove(session: string) {
|
||||
return this.deleteById(session);
|
||||
}
|
||||
|
||||
async getSessionsByWorker(worker: string): Promise<string[]> {
|
||||
const data = await this.getAllBy({ worker: worker });
|
||||
return data.map((d) => d.id);
|
||||
}
|
||||
|
||||
async init(): Promise<void> {
|
||||
this.migrations();
|
||||
this.validateSchema();
|
||||
}
|
||||
|
||||
private migrations() {
|
||||
this.db.exec(
|
||||
'CREATE TABLE IF NOT EXISTS session_worker (id TEXT, worker TEXT, data TEXT)',
|
||||
);
|
||||
// Session can have only one record
|
||||
this.db.exec(
|
||||
'CREATE UNIQUE INDEX IF NOT EXISTS session_worker_id_idx ON session_worker (id)',
|
||||
);
|
||||
// Worker can have multiple records
|
||||
this.db.exec(
|
||||
'CREATE INDEX IF NOT EXISTS session_worker_worker_idx ON session_worker (worker)',
|
||||
);
|
||||
}
|
||||
|
||||
private validateSchema() {
|
||||
new Sqlite3SchemaValidation(SCHEMA, this.db).validate();
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,7 @@
|
||||
import { PaginationParams } from '@waha/structures/pagination.dto';
|
||||
import { KnexPaginator } from '@waha/utils/Paginator';
|
||||
import { Database } from 'better-sqlite3';
|
||||
import Knex from 'knex';
|
||||
import Knex, { QueryBuilder } from 'knex';
|
||||
|
||||
import { Field, Schema } from './Schema';
|
||||
|
||||
@@ -15,6 +17,8 @@ export class Sqlite3KVRepository<Entity> {
|
||||
private readonly columns: Field[];
|
||||
private knex: Knex.Knex;
|
||||
|
||||
protected Paginator = KnexPaginator;
|
||||
|
||||
constructor(
|
||||
db: Database,
|
||||
schema: Schema,
|
||||
@@ -29,8 +33,10 @@ export class Sqlite3KVRepository<Entity> {
|
||||
this.knex = Knex({ client: 'better-sqlite3', useNullAsDefault: true });
|
||||
}
|
||||
|
||||
getAll() {
|
||||
return this.all(this.select());
|
||||
getAll(pagination?: PaginationParams) {
|
||||
let query = this.select();
|
||||
query = this.pagination(query, pagination);
|
||||
return this.all(query);
|
||||
}
|
||||
|
||||
getAllByIds(ids: string[]) {
|
||||
@@ -163,4 +169,9 @@ export class Sqlite3KVRepository<Entity> {
|
||||
protected parse(row: any) {
|
||||
return JSON.parse(row.data);
|
||||
}
|
||||
|
||||
protected pagination(query: any, pagination?: PaginationParams) {
|
||||
const paginator = new this.Paginator(pagination);
|
||||
return paginator.apply(query);
|
||||
}
|
||||
}
|
||||
+67
-16
@@ -1,29 +1,80 @@
|
||||
import { LoggerBuilder } from '@waha/utils/logging';
|
||||
import Agent from 'agentkeepalive';
|
||||
import axios from 'axios';
|
||||
import { AxiosInstance } from 'axios';
|
||||
import { Logger } from 'pino';
|
||||
|
||||
import { WAHAEvents } from '../structures/enums.dto';
|
||||
import { WebhookConfig } from '../structures/webhooks.config.dto';
|
||||
import { WAHAWebhook } from '../structures/webhooks.dto';
|
||||
import { VERSION } from '../version';
|
||||
import { WAHAInternalEvent, WhatsappSession } from './abc/session.abc';
|
||||
import { WebhookConductor, WebhookSender } from './abc/webhooks.abc';
|
||||
import request = require('request');
|
||||
import { LoggerBuilder } from '@waha/utils/logging';
|
||||
import { Logger } from 'pino';
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
const HttpAgent = require('agentkeepalive');
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
const HttpsAgent = require('agentkeepalive').HttpsAgent;
|
||||
|
||||
export class WebhookSenderCore extends WebhookSender {
|
||||
send(json) {
|
||||
this.logger.info({ url: this.url }, `Sending POST ...`);
|
||||
this.logger.debug({ data: json }, `POST DATA`);
|
||||
protected static AGENTS = {
|
||||
http: new HttpAgent({}),
|
||||
https: new HttpsAgent({}),
|
||||
};
|
||||
|
||||
request.post(this.url, { json: json }, (error, res, body) => {
|
||||
if (error) {
|
||||
this.logger.error(error);
|
||||
return;
|
||||
}
|
||||
this.logger.info(
|
||||
`POST request was sent with status code: ${res.statusCode}`,
|
||||
);
|
||||
this.logger.debug({ body: body }, `Response`);
|
||||
protected axios: AxiosInstance;
|
||||
|
||||
constructor(loggerBuilder: LoggerBuilder, webhookConfig: WebhookConfig) {
|
||||
super(loggerBuilder, webhookConfig);
|
||||
this.axios = this.buildAxiosInstance();
|
||||
}
|
||||
|
||||
protected buildAxiosInstance() {
|
||||
const headers = {
|
||||
'content-type': 'application/json',
|
||||
'User-Agent': `WAHA/${VERSION.version}`,
|
||||
};
|
||||
return axios.create({
|
||||
headers: headers,
|
||||
httpAgent: WebhookSenderCore.AGENTS.http,
|
||||
httpsAgent: WebhookSenderCore.AGENTS.https,
|
||||
});
|
||||
}
|
||||
|
||||
send(json: any, headers?: Record<string, string>) {
|
||||
headers = headers || {};
|
||||
this.logger.info(
|
||||
{ id: headers['X-Webhook-Request-Id'], url: this.url },
|
||||
`Sending POST...`,
|
||||
);
|
||||
this.logger.debug(
|
||||
{ id: headers['X-Webhook-Request-Id'], data: json },
|
||||
`POST DATA`,
|
||||
);
|
||||
|
||||
this.axios
|
||||
.post(this.url, json, { headers: headers })
|
||||
.then((response) => {
|
||||
this.logger.info(
|
||||
{ id: headers['X-Webhook-Request-Id'] },
|
||||
`POST request was sent with status code: ${response.status}`,
|
||||
);
|
||||
this.logger.debug(
|
||||
{ id: headers['X-Webhook-Request-Id'], body: response.data },
|
||||
`Response`,
|
||||
);
|
||||
})
|
||||
.catch((error) => {
|
||||
this.logger.error(
|
||||
{
|
||||
id: headers['X-Webhook-Request-Id'],
|
||||
error: error.message,
|
||||
data: error.response?.data,
|
||||
},
|
||||
`POST request failed: ${error.message}`,
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export class WebhookConductorCore implements WebhookConductor {
|
||||
@@ -121,7 +172,7 @@ export class WebhookConductorCore implements WebhookConductor {
|
||||
if (!found) {
|
||||
return false;
|
||||
}
|
||||
this.logger.info(`Event '${event}' is enabled for url: ${url}`);
|
||||
this.logger.debug(`Event '${event}' is enabled for url: ${url}`);
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
+9
-8
@@ -29,6 +29,13 @@ process.on('unhandledRejection', (reason, promise) => {
|
||||
// @ts-ignore
|
||||
logger.error(reason.stack);
|
||||
});
|
||||
process.on('SIGINT', () => {
|
||||
logger.info('SIGINT received');
|
||||
});
|
||||
|
||||
process.on('SIGTERM', () => {
|
||||
logger.info('SIGTERM received');
|
||||
});
|
||||
|
||||
async function loadModules(): Promise<
|
||||
[typeof AppModuleCore, typeof SwaggerConfiguratorCore]
|
||||
@@ -51,18 +58,12 @@ 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);
|
||||
app = await NestFactory.create(AppModule, {
|
||||
const app = await NestFactory.create(AppModule, {
|
||||
logger: getNestJSLogLevels(),
|
||||
httpsOptions: httpsOptions,
|
||||
bufferLogs: true,
|
||||
@@ -73,7 +74,6 @@ async function bootstrap() {
|
||||
// https://github.com/iamolegga/nestjs-pino?tab=readme-ov-file#expose-stack-trace-and-error-class-in-err-property
|
||||
app.useGlobalInterceptors(new LoggerErrorInterceptor());
|
||||
|
||||
app.enableShutdownHooks();
|
||||
app.useGlobalFilters(new AllExceptionsFilter());
|
||||
app.enableCors();
|
||||
// Ideally, we should apply it globally.
|
||||
@@ -90,6 +90,7 @@ async function bootstrap() {
|
||||
swaggerConfigurator.configure(WAHA_WEBHOOKS);
|
||||
|
||||
AppModule.appReady(app, logger);
|
||||
app.enableShutdownHooks();
|
||||
const config = app.get(WhatsappConfigService);
|
||||
await app.listen(config.port);
|
||||
logger.info(`WhatsApp HTTP API is running on: ${await app.getUrl()}`);
|
||||
|
||||
+68
-12
@@ -1,35 +1,91 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { BooleanString } from '@waha/nestjs/validation/BooleanString';
|
||||
import { PaginationParams } from '@waha/structures/pagination.dto';
|
||||
import { ChatIdProperty } from '@waha/structures/properties.dto';
|
||||
import { Type } from 'class-transformer';
|
||||
import { IsNumber, IsOptional } from 'class-validator';
|
||||
|
||||
import { SessionQuery } from './base.dto';
|
||||
import { Transform, Type } from 'class-transformer';
|
||||
import { IsBoolean, IsEnum, IsNumber, IsOptional } from 'class-validator';
|
||||
|
||||
/**
|
||||
* Queries
|
||||
*/
|
||||
|
||||
export class GetChatMessagesQuery extends SessionQuery {
|
||||
export class GetChatMessagesFilter {
|
||||
@ApiProperty({
|
||||
required: false,
|
||||
description: 'Filter messages before this timestamp (inclusive)',
|
||||
})
|
||||
@IsNumber()
|
||||
@IsOptional()
|
||||
@Type(() => Number)
|
||||
'filter.timestamp.lte'?: number;
|
||||
|
||||
@ApiProperty({
|
||||
required: false,
|
||||
description: 'Filter messages after this timestamp (inclusive)',
|
||||
})
|
||||
@IsNumber()
|
||||
@IsOptional()
|
||||
@Type(() => Number)
|
||||
'filter.timestamp.gte'?: number;
|
||||
|
||||
@ApiProperty({
|
||||
required: false,
|
||||
description: 'From me filter (by default shows all messages)',
|
||||
})
|
||||
@Transform(BooleanString)
|
||||
@IsBoolean()
|
||||
@IsOptional()
|
||||
'filter.fromMe'?: boolean;
|
||||
}
|
||||
|
||||
export class GetChatMessagesQuery {
|
||||
@IsNumber()
|
||||
@IsOptional()
|
||||
@Type(() => Number)
|
||||
limit: number = 100;
|
||||
|
||||
@IsNumber()
|
||||
@IsOptional()
|
||||
@Type(() => Number)
|
||||
offset?: number;
|
||||
|
||||
@ApiProperty({
|
||||
example: true,
|
||||
required: false,
|
||||
description: 'Download media for messages',
|
||||
})
|
||||
@Transform(BooleanString)
|
||||
@IsBoolean()
|
||||
@IsOptional()
|
||||
downloadMedia: boolean = true;
|
||||
}
|
||||
|
||||
export class GetChatsQuery {
|
||||
@IsNumber()
|
||||
export class GetChatMessageQuery {
|
||||
@ApiProperty({
|
||||
example: true,
|
||||
required: false,
|
||||
description: 'Download media for messages',
|
||||
})
|
||||
@Transform(BooleanString)
|
||||
@IsBoolean()
|
||||
@IsOptional()
|
||||
@Type(() => Number)
|
||||
limit?: number;
|
||||
downloadMedia: boolean = true;
|
||||
}
|
||||
|
||||
@IsNumber()
|
||||
export enum ChatSortField {
|
||||
CONVERSATION_TIMESTAMP = 'conversationTimestamp',
|
||||
ID = 'id',
|
||||
NAME = 'name',
|
||||
}
|
||||
|
||||
export class ChatsPaginationParams extends PaginationParams {
|
||||
@ApiProperty({
|
||||
description: 'Sort by field',
|
||||
enum: ChatSortField,
|
||||
})
|
||||
@IsOptional()
|
||||
@Type(() => Number)
|
||||
offset?: number;
|
||||
@IsEnum(ChatSortField)
|
||||
sortBy?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -0,0 +1,137 @@
|
||||
import { ApiExtraModels, ApiProperty, getSchemaPath } from '@nestjs/swagger';
|
||||
import { WHATSAPP_DEFAULT_SESSION_NAME } from '@waha/structures/base.dto';
|
||||
import { ChatRequest } from '@waha/structures/chatting.dto';
|
||||
import { BinaryFile, RemoteFile } from '@waha/structures/files.dto';
|
||||
import { ChatIdProperty } from '@waha/structures/properties.dto';
|
||||
import { Type } from 'class-transformer';
|
||||
import {
|
||||
ArrayMaxSize,
|
||||
ArrayMinSize,
|
||||
IsArray,
|
||||
IsEnum,
|
||||
IsNotEmpty,
|
||||
IsOptional,
|
||||
IsString,
|
||||
ValidateIf,
|
||||
ValidateNested,
|
||||
} from 'class-validator';
|
||||
|
||||
export enum ButtonType {
|
||||
REPLY = 'reply',
|
||||
URL = 'url',
|
||||
CALL = 'call',
|
||||
COPY = 'copy',
|
||||
}
|
||||
|
||||
/**
|
||||
* buttons:
|
||||
* - type: reply|url|call|copy
|
||||
* text: Display Text
|
||||
* url: only for url (required)
|
||||
* phone_number: only for call (required)
|
||||
*/
|
||||
export class Button {
|
||||
@IsEnum(ButtonType)
|
||||
type: ButtonType = ButtonType.REPLY;
|
||||
|
||||
@ApiProperty({
|
||||
example: 'Button Text',
|
||||
})
|
||||
@IsString()
|
||||
text: string;
|
||||
|
||||
@ApiProperty({
|
||||
example: '321321',
|
||||
})
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
id?: string;
|
||||
|
||||
@ApiProperty({
|
||||
example: 'https://example.com',
|
||||
})
|
||||
@ValidateIf((o) => o.type === ButtonType.URL)
|
||||
@IsNotEmpty()
|
||||
url?: string;
|
||||
|
||||
@ApiProperty({
|
||||
example: '+1234567890',
|
||||
})
|
||||
@ValidateIf((o) => o.type === ButtonType.CALL)
|
||||
@IsNotEmpty()
|
||||
phoneNumber?: string;
|
||||
|
||||
@ApiProperty({
|
||||
example: '4321',
|
||||
})
|
||||
@ValidateIf((o) => o.type === ButtonType.COPY)
|
||||
@IsNotEmpty()
|
||||
copyCode?: string;
|
||||
}
|
||||
|
||||
@ApiExtraModels(RemoteFile, BinaryFile)
|
||||
export class SendButtonsRequest {
|
||||
@IsString()
|
||||
session: string = WHATSAPP_DEFAULT_SESSION_NAME;
|
||||
|
||||
@ChatIdProperty()
|
||||
@IsString()
|
||||
chatId: string;
|
||||
|
||||
@ApiProperty({
|
||||
example: 'How are you?',
|
||||
})
|
||||
@IsOptional()
|
||||
header: string;
|
||||
|
||||
@ApiProperty({
|
||||
oneOf: [
|
||||
{ $ref: getSchemaPath(RemoteFile) },
|
||||
{ $ref: getSchemaPath(BinaryFile) },
|
||||
],
|
||||
})
|
||||
@IsOptional()
|
||||
headerImage?: RemoteFile | BinaryFile;
|
||||
|
||||
@ApiProperty({
|
||||
example: 'Tell us how are you please 🙏',
|
||||
})
|
||||
@IsOptional()
|
||||
body: string;
|
||||
|
||||
@ApiProperty({
|
||||
example: 'If you have any questions, please send it in the chat',
|
||||
})
|
||||
@IsOptional()
|
||||
footer: string;
|
||||
|
||||
@ValidateNested({ each: true })
|
||||
@Type(() => Button)
|
||||
@IsArray()
|
||||
@ArrayMinSize(1)
|
||||
@ArrayMaxSize(4)
|
||||
@ApiProperty({
|
||||
example: [
|
||||
{
|
||||
type: 'reply',
|
||||
text: 'I am good!',
|
||||
},
|
||||
{
|
||||
type: 'call',
|
||||
text: 'Call us',
|
||||
phoneNumber: '+1234567890',
|
||||
},
|
||||
{
|
||||
type: 'copy',
|
||||
text: 'Copy code',
|
||||
copyCode: '4321',
|
||||
},
|
||||
{
|
||||
type: 'url',
|
||||
text: 'How did you do that?',
|
||||
url: 'https://waha.devlike.pro',
|
||||
},
|
||||
],
|
||||
})
|
||||
buttons: Button[];
|
||||
}
|
||||
@@ -4,9 +4,14 @@ import {
|
||||
ApiProperty,
|
||||
getSchemaPath,
|
||||
} from '@nestjs/swagger';
|
||||
import { IsNumber, IsString } from 'class-validator';
|
||||
import { GetChatMessagesQuery } from '@waha/structures/chats.dto';
|
||||
import { IsNotEmpty, IsNumber, IsString } from 'class-validator';
|
||||
|
||||
import { SessionBaseRequest, SessionQuery } from './base.dto';
|
||||
import {
|
||||
SessionBaseRequest,
|
||||
SessionQuery,
|
||||
WHATSAPP_DEFAULT_SESSION_NAME,
|
||||
} from './base.dto';
|
||||
import {
|
||||
BinaryFile,
|
||||
RemoteFile,
|
||||
@@ -42,16 +47,12 @@ export class ChatQuery extends SessionQuery {
|
||||
chatId: string;
|
||||
}
|
||||
|
||||
export class GetMessageQuery extends ChatQuery {
|
||||
@IsNumber()
|
||||
limit: number;
|
||||
export class GetMessageQuery extends GetChatMessagesQuery {
|
||||
@IsNotEmpty()
|
||||
session: string = WHATSAPP_DEFAULT_SESSION_NAME;
|
||||
|
||||
@ApiProperty({
|
||||
example: true,
|
||||
required: false,
|
||||
description: 'Download media for messages',
|
||||
})
|
||||
downloadMedia: true;
|
||||
@ChatIdProperty()
|
||||
chatId: string;
|
||||
}
|
||||
|
||||
export class GetPresenceQuery extends ChatQuery {}
|
||||
@@ -243,6 +244,12 @@ export class MessageVideoRequest extends ChatRequest {
|
||||
example: null,
|
||||
})
|
||||
reply_to?: string;
|
||||
|
||||
@ApiProperty({
|
||||
description: 'Send as video note (aka instant or round video).',
|
||||
example: false,
|
||||
})
|
||||
asNote?: boolean;
|
||||
}
|
||||
|
||||
export class MessageLinkPreviewRequest extends ChatRequest {
|
||||
@@ -250,6 +257,13 @@ export class MessageLinkPreviewRequest extends ChatRequest {
|
||||
title: string;
|
||||
}
|
||||
|
||||
export class MessageForwardRequest extends ChatRequest {
|
||||
@ApiProperty({
|
||||
example: 'false_11111111111@c.us_AAAAAAAAAAAAAAAAAAAA',
|
||||
})
|
||||
messageId: string;
|
||||
}
|
||||
|
||||
export class MessageReactionRequest extends MessageRequest {
|
||||
@ApiProperty({
|
||||
description:
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { PaginationParams } from '@waha/structures/pagination.dto';
|
||||
import { ChatIdProperty } from '@waha/structures/properties.dto';
|
||||
import { IsString } from 'class-validator';
|
||||
import { IsEnum, IsOptional, IsString } from 'class-validator';
|
||||
|
||||
import { SessionBaseRequest, SessionQuery } from './base.dto';
|
||||
|
||||
@@ -13,6 +15,21 @@ export class ContactQuery extends SessionQuery {
|
||||
contactId: string;
|
||||
}
|
||||
|
||||
enum ContactSortField {
|
||||
ID = 'id',
|
||||
NAME = 'name',
|
||||
}
|
||||
|
||||
export class ContactsPaginationParams extends PaginationParams {
|
||||
@ApiProperty({
|
||||
description: 'Sort by field',
|
||||
enum: ContactSortField,
|
||||
})
|
||||
@IsOptional()
|
||||
@IsEnum(ContactSortField)
|
||||
sortBy?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Requests
|
||||
*/
|
||||
|
||||
@@ -6,6 +6,7 @@ export enum WAHAEvents {
|
||||
MESSAGE_REACTION = 'message.reaction',
|
||||
MESSAGE_ANY = 'message.any',
|
||||
MESSAGE_ACK = 'message.ack',
|
||||
MESSAGE_WAITING = 'message.waiting',
|
||||
MESSAGE_REVOKED = 'message.revoked',
|
||||
STATE_CHANGE = 'state.change',
|
||||
GROUP_JOIN = 'group.join',
|
||||
|
||||
@@ -21,7 +21,7 @@ export class RemoteFile {
|
||||
@ApiProperty({
|
||||
example:
|
||||
process.env.WHATSAPP_SWAGGER_JPG_EXAMPLE_URL ||
|
||||
'https://github.com/devlikeapro/waha/raw/core/examples/dev.likeapro.jpg',
|
||||
'https://github.com/devlikeapro/waha/raw/core/examples/waha.jpg',
|
||||
})
|
||||
url: string;
|
||||
}
|
||||
@@ -42,7 +42,7 @@ export class BinaryFile {
|
||||
@ApiProperty({
|
||||
description: 'Base64-encoded data of the file',
|
||||
example:
|
||||
'/9j/4AAQSkZJRgABAQAAAQABAAD/4gIoSUNDX1BST0ZJTEUAAQEAAAIYAAAAAAQwAABtbnRyUkdCIFhZWiAAAAAAAAAAAAAAAABhY3NwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAA9tYAAQAAAADTLQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAlkZXNjAAAA8AAAAHRyWFlaAAABZAAAABRnWFlaAAABeAAAABRiWFlaAAABjAAAABRyVFJDAAABoAAAAChnVFJDAAABoAAAAChiVFJDAAABoAAAACh3dHB0AAAByAAAABRjcHJ0AAAB3AAAADxtbHVjAAAAAAAAAAEAAAAMZW5VUwAAAFgAAAAcAHMAUgBHAEIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFhZWiAAAAAAAABvogAAOPUAAAOQWFlaIAAAAAAAAGKZAAC3hQAAGNpYWVogAAAAAAAAJKAAAA+EAAC2z3BhcmEAAAAAAAQAAAACZmYAAPKnAAANWQAAE9AAAApbAAAAAAAAAABYWVogAAAAAAAA9tYAAQAAAADTLW1sdWMAAAAAAAAAAQAAAAxlblVTAAAAIAAAABwARwBvAG8AZwBsAGUAIABJAG4AYwAuACAAMgAwADEANv/bAEMABgQFBgUEBgYFBgcHBggKEAoKCQkKFA4PDBAXFBgYFxQWFhodJR8aGyMcFhYgLCAjJicpKikZHy0wLSgwJSgpKP/bAEMBBwcHCggKEwoKEygaFhooKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKP/AABEIADAAyAMBIgACEQEDEQH/xAAbAAABBQEBAAAAAAAAAAAAAAAAAwQFBgcCAf/EADwQAAEDBAECAwQGBwkBAAAAAAECAwQABQYRIRIxB0FRExQiYRUlcXOBkQgnMjNCdbEWJENSdJKhssHh/8QAFAEBAAAAAAAAAAAAAAAAAAAAAP/EABQRAQAAAAAAAAAAAAAAAAAAAAD/2gAMAwEAAhEDEQA/AMg8UfEO755kUqVMlPJtocUIkILIbab3x8PYqI5JPO/lqqVuk90boFN0bpPdTrOLXR7DJGUobZ+iGJYhLWXR1h0gEDp76+Ic0ENujdJ7o3QKbo3Se6N0Etjh+v7f98mtT3WUY4fr+3/fJrU90Cm6N1INWOc7jj18Qlv6PZfEdaisdXWda+H05HNcSLNcY9njXV6KtFukrLbL5I6VqG9gc7/hPl5UDLdG6kLtZJ1qgWyZMS2li4tF6OUrCiUjXceXcVF7oFN0bpPdG6BTdG6T3RugU3Ruk90boFN0bpPdG6BTde7pLdG6C04Rl1wxe7sPx33DDKwJEYqJQ4jz48iB2NFVbdFBi26N0nuvCdgj1GqDX7PguKY/h1ryLxMuN0bVd0lyBa7WhPtlND/EWpXAB2DrjgjueBZr+jGW/wBGq7qw2Vc3rYu/NKUm5NpQ60vpRtJKeFDWjv5/Ko7x8t0y+454e5HZ4z8q0LsbUQrYbK0tOo7pVrseSPtSfSvHLLc7N+ipc0XaBIhOSb62+yiQgoUtspQArR50SD39KCFRh2IYjYbVO8SJ13Xc7qwJUe02kNpW0wf2VurXwCeeB6efOvL9hOKi1WLLLBdLk9hky4JgT0yEoEuAvudkDpV8PPb07740zxhyq8xoOO5HjmO2G8Y7PtrPTLkWpMpbLg3ttau6QPIHz6h5VlmXZhmN88PnY8/HbfbcZXLbUp2JbPdULe0ekA70o6Sd6B4FBB+LOHnBM3m2ZDrj8NKUPxX1626yobB44JB6gdelPPEPDYWH45ihekyV5DdYnv0uMrp9nHaV+7AGt9R89n+E1q2LY+14xYdgdwlLQZWOy/o28KWoAqhoHtEqO/klKftWr0rFvFfKzmef3e8pJ92dd9nFT5JYR8KBry2Bv7SaCHxw/X9v++TWqbrJ8cP1/b/vk1qm6DRref1GXb+cN/0RURdLMY3h5Y7x9Iznfe5LrXujjm2Gukq+JCfInXP2mpS3H9RV2/nDf/VFGQH9SuKf6+R/VygcZ5HfmYx4dxojS3pD0AobbQNqUo9GgBUBltkteNssW1ya7LyXQXLaYKSxFB7IJ1tS+3AP/m9GXl9uxPHsCenW1x/3i3Kacmtr05EaISFFseatlJ9dJNZxlWNKxK7Q5zbxuNllOJlRZwPX7wnqCiFHzXrv69/XQSM+z4riYjxcwk3WTeXWkvOQ7aEARUq5AWpXdWvIf/TH5bYIMK12y949MdmWW5BYaL6Ql1pxPdtYHG+D+R+03vxUv97t99FwtVmsVwsk9lt6NOdt4fUv4RsKWPP035EVTrjdciv8Ozw8gt8K12F2ehLbkaH7uOtXCiN9/hUTvWt0HKJXhnHbQ1IlZNKf6R1usNIShKtc6BAJG6a5xjjOPy4DlvmGba7jHTKiPKT0qKD5KHqNj86t2a3e/YrlDthw6ww4ENgISw6IHt3ZO0glXUQd8kj145pDxqXPVGw5V4HTcjbyZI6QnTm0dXA4HPkKDjKccwbD7yIt8u93lKW0lxMSGykuoBH7S16CdHyA54qCzjHINrttsvePTXJtiuSFFlbqdONrT3Qrt6H8iKmvHKBMXnftm4clTbkRhKVoZUQogHYBA5pLNWl2Lwoxyx3BJaub7709TCuFtNnqA6h5b6h/z6UC+RYdiuNuwXb5kj8aPJioeTGba9rJWo9yABpKO2iRyd1GWrG7FKYvF+kXSU1iMBxLbb/sv7xJWQNISkjg7Ou3mPmQ58bgn6fsqtDq+h443+KqVtDSr34KXCBbkKem225pmPMoG1ltSeFADv3P+00DeLYsYyqFP/sXJurN1hMmQYNySnb7Y7lCk+fbj5j13VDCtjYrRPBWM7CyCZf5ra2bXbYTyn33ElKdkD4dnueCdfKs4UvqUVa6eo716b8qDvdFJ7ooMW3Rurt4seHF5wDI5caZEeVay4oxJoQS263v4fi7BQHBB538tGqJ1UFzw/xKy7DobkTHL5IhxHFFRZ6UOICj3ISsEJP2U1vWeZPe7fKg3e+TZsSU+JLzby+oKcAAB7caAGgNDjtVW6qOqgtuI+IOVYe2tvHL5LhMrV1KZSQtsn16FAjfz1XmXeIGU5g2hvJL5LnMoV1pZUQlsK9ehICd8nnXnVT6qOqgnrFlV7sEK5Q7Nc5EONcW/ZS22lAB1OiNH8FKHHrUNukuqjqoJbHD9fQPvk1qnVWUY2fr6B98mtS3QP0XW4otq7Yia4m2OOB5cUAdKnBrSvXfA/Kh263F63sW56a4u3R1FbMYgdLajvZHnzs/nTHdG6B9LulwnR4sedNckR4iPZxm1gaZRx8I19g7+ldM3i6M2hdobnu/RCle09zUApCVb3tOxtPPPHz9aj90boJyw5bkmOslixXuRFjEkiOoJcbST30lQOvwpvesgvV/WF3+6yZ+t9KHCAhG+/SkaAqL3RugszOfZmxbU29nJJaYqU9CT0pLqU+gc11fjvdREy7XK4oit3Oe/MRFT7Nj2xBLaOON9z2HJphujdBq3iZ4gXmNl7pxHJvq5UZkaYKHmgvR6tbBAPbeqzKbMl3CW7Luct6bMe/ePPK6lK9B8h8hTRAShOkAJHoK63QP7jdbjdXW3btNcmOtNhptTgAKUDska8hs1zbLncLRORNs05+DMQOkOtHun/KodiPkaZbo3QTt/wAvyXI2UsX68vSoySFewSlLbaiOxUEgb/GobqpPdG6BTqoqyYFh9xy28x48aO6IQWDIklJCG0b5581EcACig//Z',
|
||||
'/9j/4AAQSkZJRgABAQAAAQABAAD//gAfQ29tcHJlc3NlZCBieSBqcGVnLXJlY29tcHJlc3P/2wCEAAQEBAQEBAQEBAQGBgUGBggHBwcHCAwJCQkJCQwTDA4MDA4MExEUEA8QFBEeFxUVFx4iHRsdIiolJSo0MjRERFwBBAQEBAQEBAQEBAYGBQYGCAcHBwcIDAkJCQkJDBMMDgwMDgwTERQQDxAUER4XFRUXHiIdGx0iKiUlKjQyNEREXP/CABEIAgACAAMBIgACEQEDEQH/xAAcAAEAAgIDAQAAAAAAAAAAAAAABgcEBQEDCAL/2gAIAQEAAAAA9/AAABxix7SajXYWN1uzJzdjud1v8vkAAAAAADjRRGL6kAAbaUzDdcgAAAAAOmDQnVAAADazec9oAAAAA6K9gvQAAAA753YXcAAAAD5hNb4oAAAAMuyppyAAAAa2p4+AAAAAJBbmcAAABD6s6AAAAAAd9qy4AAAKzgIAAAAAAn9lgAAOKjiQAAAAAAJdbfIAAcU7GAAAAAAAEnuLkAAVDEwAAAAAABLbdAAKxgYAAAAAc9HR9ZXICfWYABDKoAAAAAOqLRvR4x9bSQSrbgtuXgBrKO6QAAAAPmFQfoAG/sLajuvPYgCk9CAAAABr6u1QAH1PpsN9dgBBquAAAAA1NUYwAAmNii05uB10HigAAAAwagxgPn7+XIE6nhlX13ArevQAAAAcVJpQTi2ZvteuMV1UWvBbG+LDscOig+gAAAAEQrgMr0TZde1zosiW2rm+dK6DZXHy7787xAK0AAAABxTGCO71PtPNUTD7u+6/OFYhZ0oLNnpxRWrAACPxDKnOYAR6qgvyy/JuqAXPeHkbQiQWsbS9eUfpUAANFUxsrj5AV3DRuPYfmiu74sryJ0A9WaDzkPq8fsuvfqygQAAV1DhcO1AVHpRclvePfr2z9+c6tBZnovxb1C3N0T6zOKL1QAAQqvj6uvJA5pLFHpLo86ceytn5QiQNn7N8kxUWdKDa3phUGAAD4q2Pc2VKwHNH9A9RRehVuegvKsLB2e3PLMFFkS4X5H6jAAA1FQba4ADmk8Qekurzkempr5nhFpa6vm49keSYqLMlQt3SV6AABqaf21wACo9KLjuLx0d3o+yYVM/ugqdtL0H4u6xbO9FiaeJgAAamn9rcIAruGja+xfN1bCyroluo8xxT1nEvPAXh3CWa3SgAAamn9tcAAjlWDj19WNIBx39XF4XH5F0w3ltBu8LBAAa7H4NZW2bZ4+8/IHFLYZsfZvlWNbGPh23pcfmavAsmWhnY/SAHEKhOKAA3tgbkhtdlnekYvJ+YLXWk75bZ315xggZ1zfQd3X8gDiro2AAHNnyZxUGpehpLBYItScbfHi9cVPigtKSA+nyAIPAQAAO25cxrqh6AOHH0AmViAfX31AHFJY4AACdTw0dVdQAAlFm8gd2RggGqp4AAA3tsjU1dgAAJvPwDOzNKAaGpwY/fyDr47Q2dxh1QKG/IA21h70AbvYxMA0FUD4ieJ2y7vGnjrf7sbK5AMSIxrTjjLkMrkAAJZt68A+NbF4MNVGje70QnrczkZlq9uwAfGB0fWdlcAALD3NRgx6dwwYEUJFuBD8V3TQCaWEAAAAW5IKDBHqqAaTW5u/5GLoG9zANjcoAAABfmbReqCN1aAAAAZ90cAAAAbW9OazgIRyrAAAADPujgAAACe2a0VJBo6lAAAANpcQAAAC6pA4orVjiuND0YwMHVANrnA2HbPZIAAABtL2ECrIHMeqkGk0ADfbwNjc3AAAABZc/HVQPQCP1SDFh4CXZgbG5QAAADvvvICuK8BoanAjepBtZKDZ3GAAAAWDZIOqhMUMGlwOI9pxuJDyCRWoAAAAyr67gITVYKf1IDpwGd3gLGmAAAAC0ZyAUpoA0FUAAAbK4fsAAADfXYAMCiukIlXHyAANja2eAAAB3XjswAh9Sg10M0eNxxyDjnnYSSY9gAAAC15mABWcBAcgA4AAAAE8s4AAqGJgAAAAAACV2+AAOKdjAAAAAAACT3FyAAHFRxIAAAAAAEst3kAABWcBAAAAAAE8s3kAAAQ6rOkAAAAAHdaMzAAAA19Sx8AAAAASC19mAAAAcQytcMAAAAGZY825AAAAB8V5A8cAAABkTuwO8AAAAAdUIg2qAAAG1m047wAAAAAHGiiMX1IAA20nl2+5AAAAAAA4xY/ptPrMPo6nb35mz2+5kOWAAAD//EABsBAQACAwEBAAAAAAAAAAAAAAAEBQIDBgEH/9oACAECEAAAAAA11NXBi6m2VOtLbYAACJzdLgAM7vo5YADDmeexAAZdD02YAROMggAAndnLAIHE6AAAG/tp4EThNIAAePTd3csMODhAAGuBFwbZc7Kb3mY5XnQACLV4g2223o+pInAYgANFP4trjCigtl1s7+W5CiAAKXT73EShz6Wv5dPsb7rtXzvADGqztBopnUR8ue8drztZne5/RKrjQEenX2RCrHf8D9b+YwUvqeLXmzsayhAKjdYiBXO+4Hs6+lxw7bhV1uvoNeAVm6aIlU7rjdHX9LT8/J5lfZT42kHmKt3S/cvcKPzrqWv1+zuz4DDfct2rwK6D4DKymVsH6Dh740cfoW0p68ESqAPbrZT6fHrwnWR7t0ishPfD14s5uNbDDKwnDdJgGNXFys9EFZe1ixnetUXX7ulZBPn0LCj8e2OiInoBtuwAL6z45rowAMr4ADsrX53hjS4eMto0+Pd9wADP6Jt5CiKuGzsxW61lOAAvuuROAxVkImSkaEWkwAGXfyzledRqgZsD272AA6PqRhwcJDgYA2WUgAE3vMwicJpAAAG7u5YEDiNIAADd288AicXCAABO7OWAGHM89iAAy6HpswAETnKTAAZ3fRywAAa6mrgxdbZKnWlrtAH/xAAbAQEAAgMBAQAAAAAAAAAAAAAABAUCAwYBB//aAAgBAxAAAAAAMIEGJowZ75c6fmAACPTVmIAys7mQAAxpKfwAB7cXeQAaOaigAAldLvAIvMagAAG3p5QEfltQAAePTZ1UgMeVigAGEXRizkSvZXVZCipgADRB8BnYZ3N6R+S8AAaq/wAWFhjVxWdjn1shz1UAAVuv3p49VndRKJKmWvQ4cdiB5AynDVXLzV7UeOmpoWVpl2NfzgDTXrTIjQnWcp9B4eKkXvNLPPo4NUAV+yYIsN1nJ9JErPMem5ZZbLWLEAIWySI8F1POauhuq6p3Ui0yl6NYPPEPOS99xrPOhrImHsnpeSx22Lbr8CHG8B7Mkw4vXYjVzupP3vXgjwQBZ5V+o98EqYe7NQhRnvgE2T5Cjh7LlDbuiHkDTlN1RU32ClyvWGjBs35BLl1LGs8ezNUdLRDOzAAtpvOMKwADK0AA6Sfx2ONdge7Br8e7bAAGXY589VEGOzmiFgmyQALXoUfkvEKMSd7RGJ0gAHvWyCipmmvGTE9sswAXN6MeUjI8Tzwe5TNwAJXVZBH5bUAAANvUyAIvL6wAAG3p5QBH5uKAACV0kgAMaSoxAAZW93kAA0U1XiAMrO5kAAAwgQYujW2b5U6fmAP/xABREAACAQIDAgcJCgwFAwUBAAABAgMEBQAGESExEhMgQVFhcQciMDJAQnKBkRAUFVBSU6GxwdEjMzVDVWJzgpKTorIWJDRU0mN0g0RFZMLhw//aAAgBAQABPwDyiqu1vpNlRVwx6czOAT6sVWdLPESImlm9BNB/Vpioz65BWmt4A6Xf7AMS5zvbghHiiB+Qn/InEuY73N49xk/dAT+0DD3O5P49fUntlbDTzMdWmc9rHBZjvJOAzDcSMLPMh1WZwepiMLc7kniXCpHZK2Isx3uHxLhJ+8A/9wOIs53pAA7xSgfLT/iRilz/ACJsqLeDrvZH+wjFJnSyysOOMsPpp/x1xTXW31ZHvesikPyVcE+zGvxYSBvOK3M1ooeEr1YkceZF35+jYMVuep31WgpFQfLlOp9gxV3y7VuvH10nBPmqeAvsXTyGkvt2oiOIrpQBuVjwx7G1xRZ7nTRa6kVxzvFsPsOKDNForVVI6sRSsfFl7w4BB3H4nq66koo+HUTpFGOdiBri454iTWO2wcYfnJNi+obziuvdzuOoqqtyh8xe9T2Dyehvd0tpHvWrcIPMbvk9h3YtueomKpcoDG3zke1fZvGKOto6xONpp0kj5ypB9XxHW1tLQxGaqnWNF5yfoGLpnd24UVri4I+ekG31LioqqirkMtTM8rnnc6+V09VUUkgmppnicc6nTFrzu6hYbrFwh89GNvay4oq2krIRUUsySod2h5+vy9mVQWYgADUk4vOc4KctBbAJpdxkP4tezpxV11XXymarnaV+s7B2Dm8vo66roJRNSTtG/VuPaOfFlzvFPwYLmBFLzS+Y3b8nCsGAZSCDtBHll1vNFaouMqZdCR3ka7XbsGLxmOuuxZC3FU3NEh3+kef4js+Y6+0MqK3G0+u2Jzu9E82LTeaC7RcZTy7VGrxnYwPWPKr/AJrhoOHS0RWWq3E70j7ek4qKmermeeplaSVjtZviWnqZ6SZJ6aVo5F3MuMv5shuHFUldwY6ncp3I5+/q8nZlRWZiAoGpJxmLNjTF6K1uVj3PMN7dS9XX8U5cza0BSiurloT3qzHevU334RldVZWBUjUEeSSyx08TyzOFRQWZidAAMZizLLc2alpGZKMHsMnWerq+K8uZnltTrS1TF6Jj2mP7x1YhminjSWF1ZHAKlTqCPIp5o4InllcJGgJLHYABjMOYpbtK0EJKUanYu4uRzt8W5dzFNaJVhmYtRsdq86E86/aMQzRzxJNE4aNgGVgdQQfIHdY0Z3YKqjUk7ABjMuYnukrUtMxFGjfzCOc9XR8X5ZzG9pmFNUuTRufXGTzjqwjq6q6MCrDUEbQR4fNmYjUM9son/BKdJnHnkeaOofEmh36Yeppo/wAZUwp6UijBudtG+4U381cLX0D+LXU57JVwrK+1HVvRIPgco5ka
Line truncated
|
||||
})
|
||||
data: string;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { Type } from 'class-transformer';
|
||||
import { IsNumber, IsOptional, ValidateIf } from 'class-validator';
|
||||
|
||||
export enum SortOrder {
|
||||
DESC = 'desc',
|
||||
ASC = 'asc',
|
||||
}
|
||||
|
||||
export class PaginationParams {
|
||||
@IsNumber()
|
||||
@IsOptional()
|
||||
@Type(() => Number)
|
||||
limit?: number;
|
||||
|
||||
@IsNumber()
|
||||
@IsOptional()
|
||||
@Type(() => Number)
|
||||
offset?: number;
|
||||
|
||||
@ApiProperty({
|
||||
description: 'Sort by field',
|
||||
})
|
||||
@IsOptional()
|
||||
sortBy?: string;
|
||||
|
||||
@ApiProperty({
|
||||
description:
|
||||
'Sort order - <b>desc</b>ending (Z => A, New first) or <b>asc</b>ending (A => Z, Old first)',
|
||||
})
|
||||
@IsOptional()
|
||||
sortOrder?: SortOrder;
|
||||
}
|
||||
@@ -36,6 +36,14 @@ export class StopResponse {
|
||||
stopping: boolean = true;
|
||||
}
|
||||
|
||||
export class WorkerInfo {
|
||||
@ApiProperty({
|
||||
example: 'waha',
|
||||
description: 'The worker ID.',
|
||||
})
|
||||
id: string;
|
||||
}
|
||||
|
||||
export class ServerStatusResponse {
|
||||
@ApiProperty({
|
||||
example: 1723788847247,
|
||||
@@ -48,4 +56,6 @@ export class ServerStatusResponse {
|
||||
description: 'The uptime of the server in milliseconds.',
|
||||
})
|
||||
uptime: number;
|
||||
|
||||
worker: WorkerInfo;
|
||||
}
|
||||
@@ -159,6 +159,7 @@ export class MeInfo {
|
||||
|
||||
export class SessionInfo extends SessionDTO {
|
||||
me?: MeInfo;
|
||||
assignedWorker?: string;
|
||||
}
|
||||
|
||||
export class SessionDetailedInfo extends SessionInfo {
|
||||
|
||||
@@ -11,6 +11,12 @@ import {
|
||||
ValidateNested,
|
||||
} from 'class-validator';
|
||||
|
||||
export enum RetryPolicy {
|
||||
LINEAR = 'linear',
|
||||
EXPONENTIAL = 'exponential',
|
||||
CONSTANT = 'constant',
|
||||
}
|
||||
|
||||
export class RetriesConfiguration {
|
||||
@ApiProperty({
|
||||
example: 2,
|
||||
@@ -25,6 +31,13 @@ export class RetriesConfiguration {
|
||||
@IsNumber()
|
||||
@IsOptional()
|
||||
attempts?: number;
|
||||
|
||||
@ApiProperty({
|
||||
example: RetryPolicy.LINEAR,
|
||||
})
|
||||
@IsOptional()
|
||||
@IsEnum(RetryPolicy)
|
||||
policy?: RetryPolicy;
|
||||
}
|
||||
|
||||
export class CustomHeader {
|
||||
|
||||
@@ -0,0 +1,93 @@
|
||||
import { PaginationParams } from '@waha/structures/pagination.dto';
|
||||
import * as lodash from 'lodash';
|
||||
|
||||
export abstract class Paginator {
|
||||
constructor(protected readonly pagination: PaginationParams) {}
|
||||
|
||||
apply<T>(data: T): T {
|
||||
if (lodash.isEmpty(this.pagination)) {
|
||||
return data;
|
||||
}
|
||||
return this.limit(this.sort(data));
|
||||
}
|
||||
|
||||
protected abstract sort(data: any);
|
||||
|
||||
protected abstract limit(data: any);
|
||||
}
|
||||
|
||||
export class PaginatorInMemory extends Paginator {
|
||||
protected sort(data: any[]) {
|
||||
if (!this.pagination?.sortBy) {
|
||||
return data;
|
||||
}
|
||||
return lodash.orderBy(
|
||||
data,
|
||||
this.pagination.sortBy,
|
||||
this.pagination.sortOrder || 'asc',
|
||||
);
|
||||
}
|
||||
|
||||
protected limit(data: any[]) {
|
||||
if (!this.pagination?.limit) {
|
||||
return data;
|
||||
}
|
||||
const offset = this.pagination.offset || 0;
|
||||
const limit = this.pagination.limit || Infinity;
|
||||
return data.slice(offset, offset + limit);
|
||||
}
|
||||
}
|
||||
|
||||
export class KnexPaginator extends Paginator {
|
||||
indexes: string[] = [];
|
||||
dataField: string = 'data';
|
||||
|
||||
protected sort(query: any) {
|
||||
if (!this.pagination?.sortBy) {
|
||||
return query;
|
||||
}
|
||||
const sortBy = this.pagination.sortBy;
|
||||
const direction = this.pagination.sortOrder || 'asc';
|
||||
if (this.indexes.includes(sortBy)) {
|
||||
return query.orderBy(sortBy, direction);
|
||||
}
|
||||
// Use data->"$.{field}"
|
||||
// Make sure to sanitize sortBy to prevent SQL injection
|
||||
// sqlite3 doesn't support binding for column names and direction
|
||||
return query.orderByRaw(`${this.dataField}->'$.${sortBy}' ${direction}`);
|
||||
}
|
||||
|
||||
protected limit(query: any) {
|
||||
const limit = this.pagination.limit;
|
||||
const offset = this.pagination.offset;
|
||||
if (limit != null) {
|
||||
query = query.limit(limit);
|
||||
}
|
||||
if (offset != null) {
|
||||
query = query.offset(offset);
|
||||
}
|
||||
return query;
|
||||
}
|
||||
}
|
||||
|
||||
export class MongoPaginator extends Paginator {
|
||||
protected sort(query: any) {
|
||||
if (!this.pagination?.sortBy) {
|
||||
return query;
|
||||
}
|
||||
const mongoDirection = this.pagination.sortOrder === 'asc' ? 1 : -1;
|
||||
return query.sort({ [this.pagination.sortBy]: mongoDirection });
|
||||
}
|
||||
|
||||
protected limit(query: any) {
|
||||
const limit = this.pagination.limit;
|
||||
const offset = this.pagination.offset;
|
||||
if (limit != null) {
|
||||
query = query.limit(limit);
|
||||
}
|
||||
if (offset != null) {
|
||||
query = query.skip(offset);
|
||||
}
|
||||
return query;
|
||||
}
|
||||
}
|
||||
@@ -22,8 +22,12 @@ export class SingleDelayedJobRunner {
|
||||
});
|
||||
}
|
||||
|
||||
get scheduled(): boolean {
|
||||
return !!this.timeout;
|
||||
}
|
||||
|
||||
schedule(fn: FunctionNoArgs): boolean {
|
||||
if (this.timeout) {
|
||||
if (this.scheduled) {
|
||||
const msg = `Job has been started before, do not schedule it again`;
|
||||
this.log(this.warningOverride, msg);
|
||||
return false;
|
||||
@@ -31,16 +35,24 @@ export class SingleDelayedJobRunner {
|
||||
|
||||
this.timeout = setTimeout(() => {
|
||||
this.logger.debug(`Running job...`);
|
||||
fn().finally(() => {
|
||||
this.timeout = null;
|
||||
this.logger.debug(`Job finished`);
|
||||
});
|
||||
fn()
|
||||
.catch((error) => {
|
||||
this.logger.error(`Job failed: ${error}`);
|
||||
this.logger.error(error.stack);
|
||||
})
|
||||
.finally(() => {
|
||||
this.timeout = null;
|
||||
this.logger.debug(`Job finished`);
|
||||
});
|
||||
}, this.timeoutMs);
|
||||
this.logger.info(`Job scheduled with timeout ${this.timeoutMs} ms`);
|
||||
return true;
|
||||
}
|
||||
|
||||
cancel() {
|
||||
if (!this.timeout) {
|
||||
return;
|
||||
}
|
||||
clearTimeout(this.timeout);
|
||||
this.timeout = null;
|
||||
this.logger.info(`Job cancelled`);
|
||||
|
||||
@@ -40,16 +40,24 @@ export class SinglePeriodicJobRunner {
|
||||
|
||||
this.isWorking = true;
|
||||
this.logger.debug('Running job...');
|
||||
fn().finally(() => {
|
||||
this.isWorking = false;
|
||||
this.logger.debug(`Job finished`);
|
||||
});
|
||||
fn()
|
||||
.catch((error) => {
|
||||
this.logger.error(`Job failed: ${error}`);
|
||||
this.logger.error(error.stack);
|
||||
})
|
||||
.finally(() => {
|
||||
this.isWorking = false;
|
||||
this.logger.debug(`Job finished`);
|
||||
});
|
||||
}, this.intervalMs);
|
||||
this.logger.info(`Job started with interval ${this.intervalMs} ms`);
|
||||
return true;
|
||||
}
|
||||
|
||||
stop() {
|
||||
if (!this.interval) {
|
||||
return;
|
||||
}
|
||||
clearInterval(this.interval);
|
||||
this.interval = null;
|
||||
this.logger.info(`Job stopped`);
|
||||
|
||||
+1
-1
@@ -33,7 +33,7 @@ export function getEngineName(): string {
|
||||
}
|
||||
|
||||
export const VERSION: WAHAEnvironment = {
|
||||
version: '2024.9.6',
|
||||
version: '2024.10.11',
|
||||
engine: getEngineName(),
|
||||
tier: getWAHAVersion(),
|
||||
browser: getBrowserExecutablePath(),
|
||||
|
||||
Reference in new issue
Block a user