Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c9729af139 | ||
|
|
697f267fc3 | ||
|
|
ac49ce3cc5 | ||
|
|
555daf6888 | ||
|
|
8f0c2994a6 | ||
|
|
873e66db19 | ||
|
|
38b542def4 | ||
|
|
a646e5efc0 | ||
|
|
31590c9229 | ||
|
|
98c2172908 | ||
|
|
314b02daf1 | ||
|
|
d2ab78b65c | ||
|
|
db2c352fd1 | ||
|
|
6e79ad2793 | ||
|
|
77eaa287fe | ||
|
|
a64820048c | ||
|
|
4ff4a5a893 | ||
|
|
5ea5681d2b | ||
|
|
2bb327b747 | ||
|
|
4dcdf60309 | ||
|
|
009fd5b2ec | ||
|
|
091e7e5bc1 | ||
|
|
ea1202bb65 | ||
|
|
cb413febaa | ||
|
|
364e55d477 | ||
|
|
680a35a26d | ||
|
|
08ff59e463 | ||
|
|
9db4c8c333 | ||
|
|
2489c2da46 | ||
|
|
fbc58b0707 | ||
|
|
d5a525789e | ||
|
|
34066921e1 | ||
|
|
901c594311 | ||
|
|
5f619e608b | ||
|
|
49295090ad | ||
|
|
a6e43adb0a | ||
|
|
1f165bb6cb | ||
|
|
8fc9d88d2e | ||
|
|
d7e5a5ce05 | ||
|
|
09d446fdeb | ||
|
|
c0bbd60e15 | ||
|
|
af3fc137be | ||
|
|
0ee5e1bd88 | ||
|
|
f92ea18651 | ||
|
|
3cce2bb35f | ||
|
|
e080ce42ee | ||
|
|
5dbe41da78 | ||
|
|
eef3957d4f | ||
|
|
680f4596b9 | ||
|
|
8de344f220 | ||
|
|
8005846955 | ||
|
|
3edd60448b | ||
|
|
4a2fa5c076 | ||
|
|
96e9c2cc4e | ||
|
|
2089a1bb35 | ||
|
|
023c2718e2 | ||
|
|
f7aacb193a | ||
|
|
e710040904 | ||
|
|
fb12826801 | ||
|
|
833a70c6e0 | ||
|
|
c807f9021c | ||
|
|
46b66bcdf0 | ||
|
|
2ebade817e | ||
|
|
080342e619 | ||
|
|
75b06d3ba1 | ||
|
|
3b6fa3343d | ||
|
|
469cdc9b1d | ||
|
|
8a179e7194 | ||
|
|
c133795753 | ||
|
|
941ace7461 | ||
|
|
8589420f5d | ||
|
|
ece24e182f | ||
|
|
38e5bb7bdf | ||
|
|
d09e931827 | ||
|
|
3f3f1e1c20 | ||
|
|
32f2468ba8 | ||
|
|
1d6540e0d9 | ||
|
|
31da4bfca1 | ||
|
|
41d49894e2 | ||
|
|
6dcdd15b1c | ||
|
|
c2b275fd90 | ||
|
|
e4838ef3ef | ||
|
|
55ca4660d7 | ||
|
|
c16c903ec4 | ||
|
|
937cf38ff6 | ||
|
|
07f76e854d | ||
|
|
cde1d084c6 | ||
|
|
9362c5d6ae | ||
|
|
117a9108c0 | ||
|
|
5214e461f2 | ||
|
|
9bdb178b7d | ||
|
|
9c526ffdba | ||
|
|
f80a74edd1 | ||
|
|
7d8c6c0a62 | ||
|
|
17b26b869b | ||
|
|
dfbe54e92f | ||
|
|
2321de409b | ||
|
|
502287a258 | ||
|
|
052ff0ea6e | ||
|
|
2b76789cdf | ||
|
|
7bbf5df231 | ||
|
|
794a04e7d3 | ||
|
|
645467f13a | ||
|
|
7e87637341 | ||
|
|
b7abeed54a | ||
|
|
d53646eef2 | ||
|
|
c54743de6c | ||
|
|
e1fdc41132 | ||
|
|
060aa82294 | ||
|
|
2df08eac3f | ||
|
|
0ecdb9d3d0 | ||
|
|
6a7fe1bd1f | ||
|
|
87c69cb3d0 | ||
|
|
c654855c22 | ||
|
|
11bb006801 | ||
|
|
e5551f3f64 | ||
|
|
0de148005f | ||
|
|
27e4ce628e | ||
|
|
19b752bf15 | ||
|
|
6528e5bba9 | ||
|
|
37e1183e87 | ||
|
|
824512cf8e | ||
|
|
82f296b17d | ||
|
|
8302530954 | ||
|
|
6c271829aa | ||
|
|
3b30d339ee | ||
|
|
c5a268bdbd | ||
|
|
8457ea8e47 | ||
|
|
72d69eeab5 | ||
|
|
31885e41af | ||
|
|
e63726d18b | ||
|
|
3d2ab45d25 | ||
|
|
246478e1d8 | ||
|
|
9418de3b23 | ||
|
|
c6b68cad37 |
No files matched your search
+84
-168
@@ -1,191 +1,107 @@
|
||||
# WAHA Configuration
|
||||
# WAHA - WhatsApp HTTP API
|
||||
#
|
||||
# https://waha.devlike.pro/docs/how-to/config/
|
||||
#
|
||||
# Common
|
||||
#
|
||||
# If you know the right URL, you can use it here.
|
||||
# Use any port in WHATSAPP_API_PORT and domain in WHATSAPP_API_HOSTNAME
|
||||
# https://waha.devlike.pro/docs/how-to/config/#common
|
||||
|
||||
# ==================
|
||||
# ===== COMMON =====
|
||||
# ==================
|
||||
# Base URL for the API (used for webhooks, file URLs, etc.)
|
||||
WAHA_BASE_URL=http://localhost:3000
|
||||
|
||||
#WHATSAPP_API_SCHEMA=http
|
||||
#WHATSAPP_API_PORT=3000
|
||||
#WHATSAPP_API_HOSTNAME=localhost
|
||||
# Server configuration (if you need to customize hostname/port)
|
||||
# WHATSAPP_API_SCHEMA=http
|
||||
# WHATSAPP_API_PORT=3000
|
||||
# WHATSAPP_API_HOSTNAME=localhost
|
||||
|
||||
#
|
||||
# Set your timezone to see the right time on screenshots
|
||||
# Find your timezone name in the list
|
||||
# https://en.wikipedia.org/wiki/List_of_tz_database_time_zones
|
||||
#TZ=Europe/Dublin
|
||||
|
||||
#
|
||||
# Security
|
||||
#
|
||||
# https://waha.devlike.pro/docs/how-to/security/
|
||||
#
|
||||
WHATSAPP_API_KEY=admin
|
||||
# ====================
|
||||
# ===== SECURITY =====
|
||||
# ====================
|
||||
# "sha512:{SHA}" format - below is "admin" api key
|
||||
WAHA_API_KEY=sha512:c7ad44cbad762a5da0a452f9e854fdc1e0e7a52a38015f23f3eab1d80b931dd472634dfac71cd34ebc35d16ab7fb8a90c81f975113d6c7538dc69dd8de9077ec
|
||||
WAHA_DASHBOARD_ENABLED=True
|
||||
WAHA_DASHBOARD_USERNAME=admin
|
||||
WAHA_DASHBOARD_PASSWORD=admin
|
||||
#WHATSAPP_SWAGGER_USERNAME=admin
|
||||
#WHATSAPP_SWAGGER_PASSWORD=admin
|
||||
|
||||
#
|
||||
# Dashboard
|
||||
#
|
||||
# https://waha.devlike.pro/docs/how-to/waha-dashboard/#configuration
|
||||
# Enabled by default
|
||||
#WAHA_DASHBOARD_ENABLED=true
|
||||
#WAHA_DASHBOARD_USERNAME=admin
|
||||
#WAHA_DASHBOARD_PASSWORD=admin
|
||||
|
||||
#
|
||||
# Logging
|
||||
# https://waha.devlike.pro/docs/how-to/observability/#logging
|
||||
#
|
||||
# Set log format to JSON if you consume logs with log management systems
|
||||
WAHA_LOG_FORMAT=JSON
|
||||
WAHA_LOG_LEVEL=info
|
||||
# WAHA_LOG_FORMAT=PRETTY
|
||||
# If you want to see more logs, you can set the log level to debug
|
||||
#WAHA_LOG_LEVEL=debug
|
||||
# DEBUG=1 # shortcut for setting log level to debug
|
||||
|
||||
#
|
||||
# Engine
|
||||
# https://waha.devlike.pro/docs/how-to/engines/
|
||||
#
|
||||
# Choose the right engine for your needs (WEBJS by default)
|
||||
WHATSAPP_DEFAULT_ENGINE=WEBJS
|
||||
# Use NOWEB engine for the fastest performance
|
||||
#WHATSAPP_DEFAULT_ENGINE=NOWEB
|
||||
|
||||
# Worker
|
||||
# WAHA_WORKER_ID=waha1
|
||||
|
||||
#
|
||||
# Sessions
|
||||
# https://waha.devlike.pro/docs/how-to/config/#sessions
|
||||
#
|
||||
# Do not print QR codes in logs
|
||||
WAHA_PRINT_QR=False
|
||||
#
|
||||
# OR you can specify sessions by name, if you have handful amount of sessions
|
||||
#WHATSAPP_START_SESSION=session1,session2
|
||||
#
|
||||
# Restart all sessions when container starts
|
||||
# !!! Do not use it with multiple workers against one database !!!
|
||||
# WHATSAPP_RESTART_ALL_SESSIONS=True
|
||||
|
||||
#
|
||||
# Swagger
|
||||
# https://waha.devlike.pro/docs/how-to/swagger/
|
||||
#
|
||||
WHATSAPP_SWAGGER_ENABLED=True
|
||||
WHATSAPP_SWAGGER_USERNAME=admin
|
||||
WHATSAPP_SWAGGER_PASSWORD=admin
|
||||
# Disable Swagger
|
||||
#WHATSAPP_SWAGGER_ENABLED=false
|
||||
# Enable advanced Swagger configuration
|
||||
#WHATSAPP_SWAGGER_CONFIG_ADVANCED=true
|
||||
#
|
||||
# Swagger White Label customization
|
||||
# https://waha.devlike.pro/docs/how-to/swagger/#white-label
|
||||
#WHATSAPP_SWAGGER_TITLE=WAHA API
|
||||
#
|
||||
|
||||
#
|
||||
# PostgresSQL Configuration
|
||||
# https://waha.devlike.pro/docs/how-to/storages/#sessions---postgresql
|
||||
#WHATSAPP_SESSIONS_POSTGRESQL_URL=postgres://postgres:postgres@postgres:5432/postgres?sslmode=disable
|
||||
#
|
||||
# Media - PostgresSQL Storage
|
||||
# https://waha.devlike.pro/docs/how-to/storages/#media---postgresql
|
||||
#WAHA_MEDIA_STORAGE=POSTGRESQL
|
||||
#WAHA_MEDIA_POSTGRESQL_URL=postgres://postgres:postgres@postgres:5432/postgres?sslmode=disable
|
||||
#
|
||||
# WhatsApp engine (WEBJS is default, GOWS or NOWEB for better performance)
|
||||
WHATSAPP_DEFAULT_ENGINE=WEBJS
|
||||
|
||||
#
|
||||
# Global Proxy Settings
|
||||
# https://waha.devlike.pro/docs/how-to/config/#global-proxy-configuration
|
||||
#
|
||||
# You can also set proxy settings for each session later when starting it
|
||||
# https://waha.devlike.pro/docs/how-to/sessions/#configure-proxy
|
||||
#WHATSAPP_PROXY_SERVER=proxy.example.com:3128
|
||||
#WHATSAPP_PROXY_SERVER_USERNAME=user
|
||||
#WHATSAPP_PROXY_SERVER_PASSWORD=pass
|
||||
#
|
||||
# ===================
|
||||
# ===== LOGGING =====
|
||||
# ===================
|
||||
# Log format: JSON (for log management systems) or PRETTY (for development)
|
||||
WAHA_LOG_FORMAT=JSON
|
||||
|
||||
#
|
||||
# Media Configuration
|
||||
#
|
||||
# https://waha.devlike.pro/docs/how-to/config/#files
|
||||
#
|
||||
# Disable media (images, videos, files) download for incoming messages
|
||||
#WHATSAPP_DOWNLOAD_MEDIA=false
|
||||
#
|
||||
# Download only specific media types
|
||||
#WHATSAPP_FILES_MIMETYPES=image/jpeg,image/png
|
||||
#
|
||||
# Log level: info, debug, error, warn
|
||||
WAHA_LOG_LEVEL=info
|
||||
|
||||
# Media - Local Storage
|
||||
# https://waha.devlike.pro/docs/how-to/storages/#media---local
|
||||
#
|
||||
# Set to 0 to keep media files forever
|
||||
# https://waha.devlike.pro/docs/how-to/storages/#save-media-files-between-the-container-restarts
|
||||
# Don't print QR codes in logs
|
||||
WAHA_PRINT_QR=False
|
||||
|
||||
# =========================
|
||||
# ===== MEDIA STORAGE =====
|
||||
# =========================
|
||||
# Local storage (default)
|
||||
WAHA_MEDIA_STORAGE=LOCAL
|
||||
WHATSAPP_FILES_LIFETIME=0
|
||||
WHATSAPP_FILES_FOLDER=/app/.media
|
||||
# Keep media files for 180 seconds (3 minutes)
|
||||
#WHATSAPP_FILES_LIFETIME=180
|
||||
#
|
||||
|
||||
# Media - S3 Storage
|
||||
# https://waha.devlike.pro/docs/how-to/storages/#media---s3
|
||||
#WAHA_MEDIA_STORAGE=S3
|
||||
#WAHA_S3_REGION=eu-west-2
|
||||
#WAHA_S3_BUCKET=waha
|
||||
#WAHA_S3_ACCESS_KEY_ID=minioadmin
|
||||
#WAHA_S3_SECRET_ACCESS_KEY=minioadmin
|
||||
#WAHA_S3_ENDPOINT=http://minio:9000 # Not required if you're using AWS S3
|
||||
#WAHA_S3_FORCE_PATH_STYLE=True # Required for Minio
|
||||
#WAHA_S3_PROXY_FILES=True # Required for docker-compose setup
|
||||
# Media download settings
|
||||
# WHATSAPP_DOWNLOAD_MEDIA=true
|
||||
# WHATSAPP_FILES_MIMETYPES=image/jpeg,image/png
|
||||
|
||||
#
|
||||
# Global Webhooks
|
||||
#
|
||||
# https://waha.devlike.pro/docs/how-to/config/#webhooks
|
||||
# https://waha.devlike.pro/docs/how-to/webhooks/
|
||||
#WHATSAPP_HOOK_URL=https://webhook.site/11111111-1111-1111-1111-11111111
|
||||
#WHATSAPP_HOOK_EVENTS=session.status,message,message.reaction
|
||||
#
|
||||
# S3 storage (uncomment to use)
|
||||
# WAHA_MEDIA_STORAGE=S3
|
||||
# WAHA_S3_REGION=eu-west-2
|
||||
# WAHA_S3_BUCKET=waha
|
||||
# WAHA_S3_ACCESS_KEY_ID=minioadmin
|
||||
# WAHA_S3_SECRET_ACCESS_KEY=minioadmin
|
||||
# WAHA_S3_ENDPOINT=http://minio:9000
|
||||
# WAHA_S3_FORCE_PATH_STYLE=True
|
||||
# WAHA_S3_PROXY_FILES=True
|
||||
|
||||
#
|
||||
# Local Configuration
|
||||
#
|
||||
# WAHA_LOCAL_STORE_BASE_DIR=/app/sessions
|
||||
# Remember to map the volume to the host machine to the right direction in "volumes" field in docker-compose.yml
|
||||
# volumes:
|
||||
# - './.sessions:/app/sessions'
|
||||
# PostgreSQL storage (uncomment to use)
|
||||
# WAHA_MEDIA_STORAGE=POSTGRESQL
|
||||
# WAHA_MEDIA_POSTGRESQL_URL=postgres://postgres:postgres@postgres:5432/postgres?sslmode=disable
|
||||
|
||||
# ===========================
|
||||
# ===== SESSION STORAGE =====
|
||||
# ===========================
|
||||
# PostgreSQL for sessions (uncomment to use)
|
||||
# WHATSAPP_SESSIONS_POSTGRESQL_URL=postgres://postgres:postgres@postgres:5432/postgres?sslmode=disable
|
||||
|
||||
# MongoDB for sessions (uncomment to use)
|
||||
# WHATSAPP_SESSIONS_MONGO_URL=mongodb://mongouser:mongopassword@mongodb:27017
|
||||
|
||||
|
||||
#
|
||||
# MongoDB Configuration
|
||||
# https://waha.devlike.pro/docs/how-to/storages/#sessions---mongodb
|
||||
#WHATSAPP_SESSIONS_MONGO_URL=mongodb://mongouser:mongopassword@mongodb:27017
|
||||
#
|
||||
# ==================================
|
||||
# ===== ADVANCED CONFIGURATION =====
|
||||
# ==================================
|
||||
|
||||
#
|
||||
# HTTPS Configuration
|
||||
# https://waha.devlike.pro/docs/how-to/config/#https
|
||||
#
|
||||
# Consider using certbot for HTTPS
|
||||
# https://waha.devlike.pro/blog/setting-up-https-for-waha/#lets-encrypt-certbot--waha
|
||||
#
|
||||
#WAHA_HTTPS_ENABLED=true
|
||||
#WAHA_HTTPS_PATH_KEY=/etc/letsencrypt/live/waha.example.pro/privkey.pem
|
||||
#WAHA_HTTPS_PATH_CERT=/etc/letsencrypt/live/waha.example.pro/cert.pem \
|
||||
#WAHA_HTTPS_PATH_CA=/etc/letsencrypt/live/waha.example.pro/chain.pem
|
||||
#WHATSAPP_API_SCHEMA=https
|
||||
#WHATSAPP_API_PORT=3000
|
||||
#WHATSAPP_API_HOSTNAME=waha.example.pro
|
||||
#
|
||||
# Timezone for screenshots and logs
|
||||
# TZ=Europe/Dublin
|
||||
|
||||
# Session management
|
||||
# WHATSAPP_START_SESSION=session1,session2
|
||||
# WHATSAPP_RESTART_ALL_SESSIONS=False
|
||||
|
||||
# Webhooks
|
||||
# WHATSAPP_HOOK_URL=https://webhook.site/11111111-1111-1111-1111-11111111
|
||||
# WHATSAPP_HOOK_EVENTS=session.status,message,message.reaction
|
||||
|
||||
# Proxy configuration
|
||||
# WHATSAPP_PROXY_SERVER=proxy.example.com:3128
|
||||
# WHATSAPP_PROXY_SERVER_USERNAME=user
|
||||
# WHATSAPP_PROXY_SERVER_PASSWORD=pass
|
||||
|
||||
# HTTPS configuration
|
||||
# !DEPRECATED!
|
||||
# Setup nginx reverse proxy to handle TLS connection
|
||||
# using Let's encrypt or self-issued certificate
|
||||
# WAHA_HTTPS_ENABLED=true
|
||||
# WAHA_HTTPS_PATH_KEY=/etc/letsencrypt/live/waha.example.pro/privkey.pem
|
||||
# WAHA_HTTPS_PATH_CERT=/etc/letsencrypt/live/waha.example.pro/cert.pem
|
||||
# WAHA_HTTPS_PATH_CA=/etc/letsencrypt/live/waha.example.pro/chain.pem
|
||||
@@ -126,7 +126,11 @@ jobs:
|
||||
working-directory: tests/smoke
|
||||
timeout-minutes: 2
|
||||
run: |
|
||||
docker run -d --name smoke --rm -p3000:3000 ${{ vars.DOCKER_IMAGE }}:${{ matrix.tag }}
|
||||
docker run -d \
|
||||
--name smoke \
|
||||
--rm -p3000:3000 \
|
||||
${{ vars.DOCKER_IMAGE }}:${{ matrix.tag }}
|
||||
|
||||
sleep 3
|
||||
docker logs smoke
|
||||
goss validate --retry-timeout 30s --sleep 1s
|
||||
|
||||
@@ -39,6 +39,7 @@ repos:
|
||||
(?x)^(
|
||||
docs/.*|
|
||||
README.md|
|
||||
tests/smoke/goss.yaml|
|
||||
)$
|
||||
- repo: https://github.com/Lucas-C/pre-commit-hooks-nodejs
|
||||
rev: v1.1.2
|
||||
|
||||
+47
-6
@@ -1,8 +1,10 @@
|
||||
ARG NODE_IMAGE_TAG=22.16-bookworm-slim
|
||||
ARG GOLANG_IMAGE_TAG=1.23-bookworm
|
||||
|
||||
#
|
||||
# Build
|
||||
#
|
||||
ARG NODE_VERSION=22.8-bullseye
|
||||
FROM node:${NODE_VERSION} AS build
|
||||
FROM node:${NODE_IMAGE_TAG} AS build
|
||||
ENV PUPPETEER_SKIP_DOWNLOAD=True
|
||||
|
||||
# npm packages
|
||||
@@ -10,6 +12,10 @@ WORKDIR /git
|
||||
COPY package.json .
|
||||
COPY yarn.lock .
|
||||
ENV YARN_CHECKSUM_BEHAVIOR=update
|
||||
|
||||
# git
|
||||
RUN apt-get update && apt-get install -y git
|
||||
|
||||
RUN npm install -g corepack && corepack enable
|
||||
RUN yarn set version 3.6.3
|
||||
RUN yarn install
|
||||
@@ -23,11 +29,14 @@ RUN yarn build && find ./dist -name "*.d.ts" -delete
|
||||
#
|
||||
# Dashboard
|
||||
#
|
||||
FROM node:${NODE_VERSION} AS dashboard
|
||||
FROM node:${NODE_IMAGE_TAG} AS dashboard
|
||||
|
||||
# jq to parse json
|
||||
RUN apt-get update && apt-get install -y jq && rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# wget, unzip
|
||||
RUN apt-get update && apt-get install -y wget unzip && rm -rf /var/lib/apt/lists/*
|
||||
|
||||
COPY waha.config.json /tmp/waha.config.json
|
||||
RUN \
|
||||
WAHA_DASHBOARD_GITHUB_REPO=$(jq -r '.waha.dashboard.repo' /tmp/waha.config.json) && \
|
||||
@@ -42,7 +51,7 @@ RUN \
|
||||
#
|
||||
# GOWS
|
||||
#
|
||||
FROM golang:1.23-bullseye AS gows
|
||||
FROM golang:${GOLANG_IMAGE_TAG} AS gows
|
||||
|
||||
# jq to parse json
|
||||
RUN apt-get update && apt-get install -y jq && rm -rf /var/lib/apt/lists/*
|
||||
@@ -73,7 +82,7 @@ RUN \
|
||||
#
|
||||
# Final
|
||||
#
|
||||
FROM node:${NODE_VERSION} AS release
|
||||
FROM node:${NODE_IMAGE_TAG} AS release
|
||||
ENV PUPPETEER_SKIP_DOWNLOAD=True
|
||||
# Quick fix for memory potential memory leaks
|
||||
# https://github.com/devlikeapro/waha/issues/347
|
||||
@@ -98,6 +107,13 @@ RUN if [ "$USE_BROWSER" = "chromium" ] || [ "$USE_BROWSER" = "chrome" ]; then \
|
||||
&& rm -rf /var/lib/apt/lists/*; \
|
||||
fi
|
||||
|
||||
# Install wget - either for chromium or chrome
|
||||
RUN if [ "$USE_BROWSER" = "chromium" ] || [ "$USE_BROWSER" = "chrome" ]; then \
|
||||
apt-get update \
|
||||
&& apt-get install -y wget \
|
||||
&& rm -rf /var/lib/apt/lists/*; \
|
||||
fi
|
||||
|
||||
# Install fonts if using either chromium or chrome
|
||||
RUN if [ "$USE_BROWSER" = "chromium" ] || [ "$USE_BROWSER" = "chrome" ]; then \
|
||||
apt-get update \
|
||||
@@ -119,6 +135,21 @@ RUN if [ "$USE_BROWSER" = "chromium" ] || [ "$USE_BROWSER" = "chrome" ]; then \
|
||||
&& rm -rf /var/lib/apt/lists/*; \
|
||||
fi
|
||||
|
||||
# Install xvfb, xauth
|
||||
RUN if [ "$USE_BROWSER" = "chromium" ] || [ "$USE_BROWSER" = "chrome" ]; then \
|
||||
apt-get update && apt-get install -y --no-install-recommends \
|
||||
xvfb \
|
||||
xauth \
|
||||
libnss3 \
|
||||
libxss1 \
|
||||
libasound2 \
|
||||
libatk-bridge2.0-0 \
|
||||
libgtk-3-0 \
|
||||
libdrm2 \
|
||||
ca-certificates \
|
||||
&& rm -rf /var/lib/apt/lists/*; \
|
||||
fi
|
||||
|
||||
# Install Chromium
|
||||
RUN if [ "$USE_BROWSER" = "chromium" ]; then \
|
||||
apt-get update \
|
||||
@@ -131,7 +162,7 @@ RUN if [ "$USE_BROWSER" = "chromium" ]; then \
|
||||
# Install Chrome
|
||||
# Available versions:
|
||||
# https://www.ubuntuupdates.org/package/google_chrome/stable/main/base/google-chrome-stable
|
||||
ARG CHROME_VERSION="135.0.7049.114-1"
|
||||
ARG CHROME_VERSION="137.0.7151.103-1"
|
||||
RUN if [ "$USE_BROWSER" = "chrome" ]; then \
|
||||
wget --no-verbose -O /tmp/chrome.deb https://dl.google.com/linux/chrome/deb/pool/main/g/google-chrome-stable/google-chrome-stable_${CHROME_VERSION}_amd64.deb \
|
||||
&& apt-get update \
|
||||
@@ -140,12 +171,20 @@ RUN if [ "$USE_BROWSER" = "chrome" ]; then \
|
||||
&& rm -rf /var/lib/apt/lists/*; \
|
||||
fi
|
||||
|
||||
# curl
|
||||
RUN apt-get update \
|
||||
&& apt-get install -y curl \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# GOWS requirements
|
||||
# libc6
|
||||
RUN apt-get update \
|
||||
&& apt-get install -y libc6 \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Install tini for proper init process
|
||||
RUN apt-get update && apt-get install -y tini && rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Set the ENV for docker image
|
||||
ENV WHATSAPP_DEFAULT_ENGINE=$WHATSAPP_DEFAULT_ENGINE
|
||||
|
||||
@@ -170,4 +209,6 @@ ENV WAHA_ZIPPER=ZIPUNZIP
|
||||
|
||||
# Run command, etc
|
||||
EXPOSE 3000
|
||||
# Use tini as init system to handle zombie processes properly
|
||||
ENTRYPOINT ["/usr/bin/tini", "--"]
|
||||
CMD ["/entrypoint.sh"]
|
||||
@@ -1,20 +1,20 @@
|
||||
build-all: build-plus build-chrome build-gows build-noweb
|
||||
|
||||
build:
|
||||
docker build . -t devlikeapro/waha
|
||||
|
||||
build-plus:
|
||||
docker build . -t devlikeapro/waha-plus
|
||||
|
||||
build-chrome:
|
||||
docker build . -t devlikeapro/waha-plus:chrome --build-arg USE_BROWSER=chrome
|
||||
|
||||
build-noweb:
|
||||
docker build . -t devlikeapro/waha:noweb --build-arg USE_BROWSER=none --build-arg WHATSAPP_DEFAULT_ENGINE=NOWEB
|
||||
docker build . -t devlikeapro/waha-plus:noweb --build-arg USE_BROWSER=none --build-arg WHATSAPP_DEFAULT_ENGINE=NOWEB
|
||||
|
||||
build-plus-gows:
|
||||
build-gows:
|
||||
docker build . -t devlikeapro/waha-plus:gows --build-arg USE_BROWSER=none --build-arg WHATSAPP_DEFAULT_ENGINE=GOWS
|
||||
|
||||
build-all: build build-chrome build-noweb
|
||||
|
||||
build-plus:
|
||||
docker build . -t devlikeapro/waha-plus
|
||||
|
||||
build-ssh:
|
||||
# check IMAGE provided
|
||||
@[ "${IMAGE}" ] || ( echo "Add APP: make build-ssh image=devlikeapro/waha"; exit 1 );
|
||||
@@ -41,7 +41,7 @@ up-noweb-libsignal:
|
||||
. ${NVM_DIR}/nvm.sh && nvm exec yarn up libsignal@github:devlikeapro/libsignal-node#fork-master
|
||||
|
||||
up-webjs:
|
||||
. ${NVM_DIR}/nvm.sh && nvm exec yarn up whatsapp-web.js@github:devlikeapro/whatsapp-web.js#fork-main-channels
|
||||
. ${NVM_DIR}/nvm.sh && nvm exec yarn up whatsapp-web.js@github:devlikeapro/whatsapp-web.js#fork-main-2025-06-09
|
||||
|
||||
start-proxy:
|
||||
docker run --rm -d --name squid-container -e TZ=UTC -p 3128:3128 ubuntu/squid:5.2-22.04_beta
|
||||
|
||||
@@ -146,7 +146,7 @@ curl -d "{\"chatId\": \"${PHONE}@c.us\", \"text\": \"Hello from WhatsApp HTTP AP
|
||||
|
||||
## Start the project
|
||||
1. Clone the repository
|
||||
2. Make sure you're using node>=22
|
||||
2. Make sure you're using node>=22 (check [.nvmrc](/.nvmrc) to get the version)
|
||||
3. Run the following commands:
|
||||
```bash
|
||||
# Install dependencies
|
||||
|
||||
+22
-1
@@ -8,6 +8,12 @@ services:
|
||||
# WAHA Core
|
||||
# image: devlikeapro/waha:latest
|
||||
|
||||
logging:
|
||||
driver: 'json-file'
|
||||
options:
|
||||
max-size: '100m'
|
||||
max-file: '10'
|
||||
|
||||
ports:
|
||||
- '127.0.0.1:3000:3000/tcp'
|
||||
|
||||
@@ -32,13 +38,18 @@ services:
|
||||
# POSTGRES_PASSWORD: postgres
|
||||
# POSTGRES_DB: postgres
|
||||
# ports:
|
||||
# - "5432:5432"
|
||||
# - "127.0.0.1:5432:5432"
|
||||
# volumes:
|
||||
# - pg_data:/var/lib/postgresql/data
|
||||
# command:
|
||||
# - postgres
|
||||
# - "-c"
|
||||
# - "max_connections=3000"
|
||||
# logging:
|
||||
# driver: "json-file"
|
||||
# options:
|
||||
# max-size: "100m"
|
||||
# max-file: "10"
|
||||
|
||||
# NOTE: Only if you're using MongoDB
|
||||
# https://waha.devlike.pro/docs/how-to/storages/#sessions---mongodb
|
||||
@@ -53,6 +64,11 @@ services:
|
||||
# environment:
|
||||
# - MONGO_INITDB_ROOT_USERNAME=mongouser
|
||||
# - MONGO_INITDB_ROOT_PASSWORD=mongopassword
|
||||
# logging:
|
||||
# driver: "json-file"
|
||||
# options:
|
||||
# max-size: "100m"
|
||||
# max-file: "10"
|
||||
|
||||
# NOTE: Only if you're using S3 to save media files
|
||||
# https://waha.devlike.pro/docs/how-to/storages/#media---s3
|
||||
@@ -71,6 +87,11 @@ services:
|
||||
# volumes:
|
||||
# - minio_data:/data
|
||||
# command: server /data --console-address ":9001"
|
||||
# logging:
|
||||
# driver: "json-file"
|
||||
# options:
|
||||
# max-size: "100m"
|
||||
# max-file: "10"
|
||||
|
||||
volumes:
|
||||
mongodb_data: {}
|
||||
|
||||
@@ -10,7 +10,7 @@ services:
|
||||
- './.sessions:/app/.sessions'
|
||||
- './.media:/app/.media'
|
||||
environment:
|
||||
- WHATSAPP_API_KEY=321
|
||||
- WAHA_API_KEY=321
|
||||
- WAHA_DASHBOARD_USERNAME=admin
|
||||
- WAHA_DASHBOARD_PASSWORD=admin
|
||||
- WHATSAPP_DEFAULT_ENGINE=WEBJS
|
||||
|
||||
@@ -29,7 +29,7 @@ services:
|
||||
# - WHATSAPP_HOOK_EVENTS=message
|
||||
# - WHATSAPP_API_HOSTNAME=localhost
|
||||
- WHATSAPP_DEFAULT_ENGINE=WEBJS
|
||||
- WHATSAPP_API_KEY=321
|
||||
- WAHA_API_KEY=321
|
||||
# Username and password for Swagger
|
||||
- WHATSAPP_SWAGGER_USERNAME=swagger
|
||||
- WHATSAPP_SWAGGER_PASSWORD=admin
|
||||
@@ -55,7 +55,7 @@ services:
|
||||
# Environment variables from https://waha.devlike.pro/docs/how-to/config/
|
||||
# - WHATSAPP_HOOK_URL=https://webhook.site/11111111-1111-1111-1111-11111111
|
||||
# - WHATSAPP_HOOK_EVENTS=message
|
||||
# - WHATSAPP_API_KEY=321
|
||||
# - WAHA_API_KEY=321
|
||||
# - WHATSAPP_SWAGGER_USERNAME=admin
|
||||
# - WHATSAPP_SWAGGER_PASSWORD=123
|
||||
# - WAHA_DASHBOARD_USERNAME=admin
|
||||
|
||||
+57
-2
@@ -1,11 +1,66 @@
|
||||
#!/bin/sh
|
||||
|
||||
|
||||
#
|
||||
# Calculate UV_THREADPOOL_SIZE based on number of CPUs
|
||||
#
|
||||
cpus=$(node -e "const os = require('os'); console.log(os.cpus().length);")
|
||||
uv_threadpool_size=$(($cpus * 1))
|
||||
|
||||
# Set UV_THREADPOOL_SIZE as an environment variable
|
||||
export UV_THREADPOOL_SIZE="${UV_THREADPOOL_SIZE:-$uv_threadpool_size}"
|
||||
|
||||
# Start your application using yarn
|
||||
exec node dist/main
|
||||
#
|
||||
# Handle API key hashing
|
||||
#
|
||||
# Save WHATSAPP_API_KEY or WAHA_API_KEY in a variable (WHATSAPP_API_KEY has priority)
|
||||
if [ -n "$WHATSAPP_API_KEY" ]; then
|
||||
key="$WHATSAPP_API_KEY"
|
||||
elif [ -n "$WAHA_API_KEY" ]; then
|
||||
key="$WAHA_API_KEY"
|
||||
fi
|
||||
|
||||
# Unset both environment variables
|
||||
unset WHATSAPP_API_KEY
|
||||
unset WAHA_API_KEY
|
||||
|
||||
# Process the key if it exists
|
||||
if [ -n "$key" ]; then
|
||||
# Check if key is already hashed
|
||||
if echo "$key" | grep -q "^sha512:"; then
|
||||
# If already hashed, use it as is
|
||||
export WAHA_API_KEY="$key"
|
||||
else
|
||||
# Display warning about using plain text API key
|
||||
echo "⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️"
|
||||
echo "WARNING: Plain text API key detected. Converting to hashed format for security."
|
||||
echo "For better security, use WAHA_API_KEY=sha512:{SHA512_HASH_FOR_YOUR_API_KEY}"
|
||||
echo "⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️"
|
||||
# Hash the key using sha512sum
|
||||
HASHED_KEY=$(echo -n "$key" | sha512sum | awk '{print $1}')
|
||||
export WAHA_API_KEY="sha512:$HASHED_KEY"
|
||||
fi
|
||||
fi
|
||||
|
||||
#
|
||||
# xvfb-run
|
||||
#
|
||||
USE_XVFB=false
|
||||
if [ -z "$WHATSAPP_DEFAULT_ENGINE" ] || [ "$WHATSAPP_DEFAULT_ENGINE" = "WEBJS" ]; then
|
||||
# Try to run xvfb-run with a test command
|
||||
if xvfb-run --auto-servernum echo "xvfb-run is working!"; then
|
||||
USE_XVFB=true
|
||||
else
|
||||
echo "xvfb-run test failed, do not run it"
|
||||
USE_XVFB=false
|
||||
fi
|
||||
fi
|
||||
|
||||
#
|
||||
# Start your application using node with exec to ensure proper signal handling
|
||||
#
|
||||
if [ "$USE_XVFB" = "true" ]; then
|
||||
exec xvfb-run --auto-servernum node dist/main
|
||||
else
|
||||
exec node dist/main
|
||||
fi
|
||||
Binary file not shown.
Binary file not shown.
+22
-20
@@ -31,31 +31,32 @@
|
||||
"@aws-sdk/client-s3": "^3.633.0",
|
||||
"@aws-sdk/s3-request-presigner": "^3.633.0",
|
||||
"@nestjs/axios": "^3.0.2",
|
||||
"@nestjs/common": "^9.0.9",
|
||||
"@nestjs/common": "^11.0.0",
|
||||
"@nestjs/config": "^3.2.3",
|
||||
"@nestjs/core": "^9.0.9",
|
||||
"@nestjs/passport": "^9.0.0",
|
||||
"@nestjs/platform-express": "^9.0.9",
|
||||
"@nestjs/platform-ws": "^9.0.9",
|
||||
"@nestjs/serve-static": "^2.1.3",
|
||||
"@nestjs/core": "^11.0.0",
|
||||
"@nestjs/passport": "^11.0.0",
|
||||
"@nestjs/platform-express": "^11.0.0",
|
||||
"@nestjs/platform-ws": "^11.0.0",
|
||||
"@nestjs/serve-static": "^5.0.0",
|
||||
"@nestjs/swagger": "^7.1.11",
|
||||
"@nestjs/terminus": "^10.2.3",
|
||||
"@nestjs/websockets": "^9.0.9",
|
||||
"@nestjs/websockets": "^11.0.0",
|
||||
"@types/better-sqlite3": "^7.6.10",
|
||||
"@types/lodash": "^4.14.194",
|
||||
"@types/passport": "^1.0.17",
|
||||
"@types/ws": "^8.5.4",
|
||||
"adm-zip": "0.5.10",
|
||||
"agentkeepalive": "^4.5.0",
|
||||
"async-lock": "^1.4.1",
|
||||
"audio-decode": "^2.2.2",
|
||||
"axios": "^1.7.7",
|
||||
"axios": "^1.9.0",
|
||||
"axios-retry": "^4.5.0",
|
||||
"better-sqlite3": "11.3.0",
|
||||
"check-disk-space": "^3.4.0",
|
||||
"chokidar": "^3.6.0",
|
||||
"class-transformer": "^0.5.1",
|
||||
"class-validator": "0.14.0",
|
||||
"del": "^6.0.0",
|
||||
"del": "^8.0.0",
|
||||
"express-basic-auth": "^1.2.1",
|
||||
"file-type": "16.5.4",
|
||||
"fs-extra": "^11.2.0",
|
||||
@@ -71,23 +72,23 @@
|
||||
"mongodb": "6.9.0",
|
||||
"nestjs-pino": "^4.1.0",
|
||||
"node-cache": "5.1.2",
|
||||
"passport": "^0.6.0",
|
||||
"passport": "^0.7.0",
|
||||
"passport-headerapikey": "^1.2.2",
|
||||
"pg": "^8.13.1",
|
||||
"pino-http": "^10.2.0",
|
||||
"pino-pretty": "^11.2.1",
|
||||
"pretty-bytes": "5.6.0",
|
||||
"promise-retry": "^2.0.1",
|
||||
"puppeteer": "^23.6.0",
|
||||
"puppeteer": "^24.10.0",
|
||||
"qrcode": "^1.5.1",
|
||||
"qrcode-terminal": "^0.12.0",
|
||||
"reflect-metadata": "^0.1.13",
|
||||
"rimraf": "^3.0.2",
|
||||
"rimraf": "^4.3.0",
|
||||
"rxjs": "^7.8.1",
|
||||
"sharp": "^0.33.4",
|
||||
"swagger-ui-express": "^4.1.4",
|
||||
"ulid": "^2.3.0",
|
||||
"whatsapp-web.js": "github:devlikeapro/whatsapp-web.js#fork-main-channels",
|
||||
"whatsapp-web.js": "github:devlikeapro/whatsapp-web.js#fork-main-2025-06-09",
|
||||
"write-file-atomic": "^6.0.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
@@ -96,16 +97,17 @@
|
||||
"resolutions": {
|
||||
"bufferutil": "^4.0.8",
|
||||
"ws": "^8.18.0",
|
||||
"puppeteer": "^24.7.2",
|
||||
"puppeteer": "^24.10.0",
|
||||
"whatwg-url": "13.0.0",
|
||||
"libsignal": "github:devlikeapro/libsignal-node#fork-master"
|
||||
"libsignal": "github:devlikeapro/libsignal-node#fork-master",
|
||||
"axios": "^1.9.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@grpc/grpc-js": "^1.12.4",
|
||||
"@grpc/grpc-js": "^1.13.4",
|
||||
"@grpc/proto-loader": "^0.7.13",
|
||||
"@nestjs/cli": "^9.0.0",
|
||||
"@nestjs/schematics": "^9.0.1",
|
||||
"@nestjs/testing": "^9.0.9",
|
||||
"@nestjs/cli": "^11.0.0",
|
||||
"@nestjs/schematics": "^11.0.0",
|
||||
"@nestjs/testing": "^11.0.0",
|
||||
"@types/express": "^4.17.3",
|
||||
"@types/jest": "26.0.10",
|
||||
"@types/node": "^18.14.2",
|
||||
@@ -116,7 +118,7 @@
|
||||
"eslint-config-prettier": "^6.10.0",
|
||||
"eslint-plugin-import": "^2.20.1",
|
||||
"eslint-plugin-simple-import-sort": "^10.0.0",
|
||||
"grpc-tools": "^1.12.4",
|
||||
"grpc-tools": "^1.13.0",
|
||||
"jest": "^29.7.0",
|
||||
"prettier": "^1.19.1",
|
||||
"protoc-gen-ts": "^0.8.7",
|
||||
|
||||
+2
-2
@@ -12,9 +12,9 @@ touch "$OUTPUT_FILE"
|
||||
# Find all .md files in the source directory and combine them
|
||||
find "$SOURCE_DIR" -type f -name "*.ts" | while read -r file; do
|
||||
# Add file name to OUTPUT_FILE
|
||||
echo -e "\n-------" >> "$OUTPUT_FILE"
|
||||
echo "\n-------" >> "$OUTPUT_FILE"
|
||||
echo "File: $file" >> "$OUTPUT_FILE"
|
||||
echo -e "-------\n" >> "$OUTPUT_FILE"
|
||||
echo "-------\n" >> "$OUTPUT_FILE"
|
||||
# Append the content of the file to the output file
|
||||
cat "$file" >> "$OUTPUT_FILE"
|
||||
done
|
||||
|
||||
@@ -36,8 +36,8 @@ class AuthController {
|
||||
summary: 'Get QR code for pairing WhatsApp API.',
|
||||
})
|
||||
@SessionApiParam
|
||||
@ApiFileAcceptHeader(Base64File, QRCodeValue)
|
||||
@UseInterceptors(new BufferResponseInterceptor())
|
||||
@ApiFileAcceptHeader('image/png', Base64File, QRCodeValue)
|
||||
@UseInterceptors(new BufferResponseInterceptor('image/png'))
|
||||
async getQR(
|
||||
@QRCodeSessionParam session: WhatsappSession,
|
||||
@Query() query: QRCodeQuery,
|
||||
|
||||
@@ -30,6 +30,8 @@ import {
|
||||
GetChatMessageQuery,
|
||||
GetChatMessagesFilter,
|
||||
GetChatMessagesQuery,
|
||||
OverviewBodyRequest,
|
||||
OverviewFilter,
|
||||
OverviewPaginationParams,
|
||||
PinMessageRequest,
|
||||
ReadChatMessagesQuery,
|
||||
@@ -65,8 +67,23 @@ class ChatsController {
|
||||
getChatsOverview(
|
||||
@WorkingSessionParam session: WhatsappSession,
|
||||
@Query() pagination: OverviewPaginationParams,
|
||||
@Query() filter: OverviewFilter,
|
||||
): Promise<ChatSummary[]> {
|
||||
return session.getChatsOverview(pagination);
|
||||
return session.getChatsOverview(pagination, filter);
|
||||
}
|
||||
|
||||
@Post('overview')
|
||||
@SessionApiParam
|
||||
@ApiOperation({
|
||||
summary:
|
||||
'Get chats overview. Use POST if you have too many "ids" params - GET can limit it',
|
||||
})
|
||||
@UsePipes(new ValidationPipe({ transform: true, whitelist: true }))
|
||||
postChatsOverview(
|
||||
@WorkingSessionParam session: WhatsappSession,
|
||||
@Body() body: OverviewBodyRequest,
|
||||
): Promise<ChatSummary[]> {
|
||||
return session.getChatsOverview(body.pagination, body.filter);
|
||||
}
|
||||
|
||||
@Delete(':chatId')
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
import {
|
||||
Body,
|
||||
Controller,
|
||||
Param,
|
||||
Post,
|
||||
UsePipes,
|
||||
ValidationPipe,
|
||||
} from '@nestjs/common';
|
||||
import { ApiOperation, ApiSecurity, ApiTags } from '@nestjs/swagger';
|
||||
import { WhatsappSession } from '@waha/core/abc/session.abc';
|
||||
import {
|
||||
SessionApiParam,
|
||||
WorkingSessionParam,
|
||||
} from '@waha/nestjs/params/SessionApiParam';
|
||||
|
||||
import { SessionManager } from '../core/abc/manager.abc';
|
||||
import {
|
||||
EventCancelRequest,
|
||||
EventMessageRequest,
|
||||
} from '../structures/events.dto';
|
||||
import { WAMessage } from '../structures/responses.dto';
|
||||
|
||||
@ApiSecurity('api_key')
|
||||
@Controller('api/:session/events')
|
||||
@ApiTags('📅 Events')
|
||||
export class EventsController {
|
||||
constructor(private manager: SessionManager) {}
|
||||
|
||||
@Post()
|
||||
@ApiOperation({ summary: 'Send an event message' })
|
||||
@UsePipes(new ValidationPipe())
|
||||
@SessionApiParam
|
||||
async sendEvent(
|
||||
@WorkingSessionParam session: WhatsappSession,
|
||||
@Body() request: EventMessageRequest,
|
||||
): Promise<WAMessage> {
|
||||
return session.sendEvent(request);
|
||||
}
|
||||
|
||||
// @Post(':id/cancel')
|
||||
// @ApiOperation({ summary: 'Cancel an event by ID' })
|
||||
// @UsePipes(new ValidationPipe())
|
||||
// @SessionApiParam
|
||||
// async cancelEvent(
|
||||
// @WorkingSessionParam session: WhatsappSession,
|
||||
// @Param('id') id: string,
|
||||
// ): Promise<WAMessage> {
|
||||
// return session.cancelEvent(id);
|
||||
// }
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
import { isLidUser } from '@adiwajshing/baileys/lib/WABinary/jid-utils';
|
||||
import {
|
||||
Controller,
|
||||
Get,
|
||||
Param,
|
||||
Query,
|
||||
UnprocessableEntityException,
|
||||
UsePipes,
|
||||
ValidationPipe,
|
||||
} from '@nestjs/common';
|
||||
import { ApiOperation, ApiSecurity, ApiTags } from '@nestjs/swagger';
|
||||
import { WhatsappSession } from '@waha/core/abc/session.abc';
|
||||
import {
|
||||
SessionApiParam,
|
||||
WorkingSessionParam,
|
||||
} from '@waha/nestjs/params/SessionApiParam';
|
||||
import { CountResponse } from '@waha/structures/base.dto';
|
||||
import {
|
||||
LidsListQueryParams,
|
||||
LidToPhoneNumber,
|
||||
} from '@waha/structures/lids.dto';
|
||||
import { PaginationParams, SortOrder } from '@waha/structures/pagination.dto';
|
||||
|
||||
import { SessionManager } from '../core/abc/manager.abc';
|
||||
|
||||
@ApiSecurity('api_key')
|
||||
@Controller('api/:session/lids')
|
||||
@ApiTags('👤 Contacts')
|
||||
export class LidsController {
|
||||
constructor(private manager: SessionManager) {}
|
||||
|
||||
@Get('/')
|
||||
@SessionApiParam
|
||||
@ApiOperation({ summary: 'Get all known lids to phone number mapping' })
|
||||
@UsePipes(new ValidationPipe({ transform: true, whitelist: true }))
|
||||
async getAll(
|
||||
@WorkingSessionParam session: WhatsappSession,
|
||||
@Query() params: LidsListQueryParams,
|
||||
): Promise<Array<LidToPhoneNumber>> {
|
||||
// Always lid
|
||||
const pagination: PaginationParams = params;
|
||||
pagination.sortBy = 'lid';
|
||||
pagination.sortOrder = SortOrder.ASC;
|
||||
const lids = await session.getAllLids(pagination);
|
||||
return lids;
|
||||
}
|
||||
|
||||
@Get('/count')
|
||||
@SessionApiParam
|
||||
@ApiOperation({ summary: 'Get the number of known lids' })
|
||||
@UsePipes(new ValidationPipe({ transform: true, whitelist: true }))
|
||||
async getLidsCount(
|
||||
@WorkingSessionParam session: WhatsappSession,
|
||||
): Promise<CountResponse> {
|
||||
const count = await session.getLidsCount();
|
||||
return {
|
||||
count: count,
|
||||
};
|
||||
}
|
||||
|
||||
@Get('/:lid')
|
||||
@SessionApiParam
|
||||
@ApiOperation({ summary: 'Get phone number by lid' })
|
||||
@UsePipes(new ValidationPipe({ transform: true, whitelist: true }))
|
||||
async findPNByLid(
|
||||
@WorkingSessionParam session: WhatsappSession,
|
||||
@Param('lid') lid: string,
|
||||
): Promise<LidToPhoneNumber> {
|
||||
if (!lid.includes('@')) {
|
||||
lid = lid + '@lid';
|
||||
}
|
||||
|
||||
if (!isLidUser(lid)) {
|
||||
throw new UnprocessableEntityException(
|
||||
'Invalid LID - it must end with @lid',
|
||||
);
|
||||
}
|
||||
const result = await session.findPNByLid(lid);
|
||||
result.pn = result.pn || null;
|
||||
return result;
|
||||
}
|
||||
|
||||
@Get('/pn/:phoneNumber')
|
||||
@SessionApiParam
|
||||
@ApiOperation({ summary: 'Get lid by phone number (chat id)' })
|
||||
@UsePipes(new ValidationPipe({ transform: true, whitelist: true }))
|
||||
async findLIDByPhoneNumber(
|
||||
@WorkingSessionParam session: WhatsappSession,
|
||||
@Param('phoneNumber') phoneNumber: string,
|
||||
): Promise<LidToPhoneNumber> {
|
||||
if (isLidUser(phoneNumber)) {
|
||||
return {
|
||||
lid: phoneNumber,
|
||||
pn: null,
|
||||
};
|
||||
}
|
||||
|
||||
const result = await session.findLIDByPhoneNumber(phoneNumber);
|
||||
result.lid = result.lid || null;
|
||||
return result;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
import {
|
||||
Body,
|
||||
Controller,
|
||||
Post,
|
||||
UnprocessableEntityException,
|
||||
UseInterceptors,
|
||||
} from '@nestjs/common';
|
||||
import { ApiOperation, ApiSecurity, ApiTags } from '@nestjs/swagger';
|
||||
import { SessionManager } from '@waha/core/abc/manager.abc';
|
||||
import { WAMimeType } from '@waha/core/media/WAMimeType';
|
||||
import { ApiFileAcceptHeader } from '@waha/nestjs/ApiFileAcceptHeader';
|
||||
import {
|
||||
SessionApiParam,
|
||||
WorkingSessionParam,
|
||||
} from '@waha/nestjs/params/SessionApiParam';
|
||||
import {
|
||||
FileDTO,
|
||||
VideoFileDTO,
|
||||
VoiceFileDTO,
|
||||
} from '@waha/structures/media.dto';
|
||||
|
||||
import { WhatsappSession } from '../core/abc/session.abc';
|
||||
import { BufferResponseInterceptor } from '../nestjs/BufferResponseInterceptor';
|
||||
|
||||
@ApiSecurity('api_key')
|
||||
@Controller('api/:session/media')
|
||||
@ApiTags('🖼️ Media')
|
||||
class MediaController {
|
||||
constructor(private manager: SessionManager) {}
|
||||
|
||||
@Post('convert/voice')
|
||||
@ApiOperation({
|
||||
summary: 'Convert voice to WhatsApp format (opus)',
|
||||
})
|
||||
@SessionApiParam
|
||||
@UseInterceptors(
|
||||
new BufferResponseInterceptor(WAMimeType.VOICE, 'output.opus'),
|
||||
)
|
||||
@ApiFileAcceptHeader(WAMimeType.VOICE)
|
||||
async convertVoice(
|
||||
@WorkingSessionParam session: WhatsappSession,
|
||||
@Body() file: VoiceFileDTO,
|
||||
): Promise<Buffer> {
|
||||
const data = await this.buffer(session, file);
|
||||
const content = await session.mediaConverter.voice(data);
|
||||
return content;
|
||||
}
|
||||
|
||||
@Post('convert/video')
|
||||
@ApiOperation({
|
||||
summary: 'Convert video to WhatsApp format (mp4)',
|
||||
})
|
||||
@SessionApiParam
|
||||
@UseInterceptors(
|
||||
new BufferResponseInterceptor(WAMimeType.VIDEO, 'output.mp4'),
|
||||
)
|
||||
@ApiFileAcceptHeader(WAMimeType.VIDEO)
|
||||
async convertVideo(
|
||||
@WorkingSessionParam session: WhatsappSession,
|
||||
@Body() file: VideoFileDTO,
|
||||
): Promise<Buffer> {
|
||||
const data = await this.buffer(session, file);
|
||||
const content = await session.mediaConverter.video(data);
|
||||
return content;
|
||||
}
|
||||
|
||||
private async buffer(
|
||||
session: WhatsappSession,
|
||||
file: FileDTO,
|
||||
): Promise<Buffer> {
|
||||
if ('url' in file) {
|
||||
return session.fetch(file.url);
|
||||
} else if ('data' in file) {
|
||||
return Buffer.from(file.data, 'base64');
|
||||
} else {
|
||||
throw new UnprocessableEntityException(
|
||||
'Either "url" or "data" must be specified.',
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export { MediaController };
|
||||
@@ -21,8 +21,8 @@ export class ScreenshotController {
|
||||
constructor(private manager: SessionManager) {}
|
||||
|
||||
@Get('/screenshot')
|
||||
@UseInterceptors(new BufferResponseInterceptor())
|
||||
@ApiFileAcceptHeader()
|
||||
@UseInterceptors(new BufferResponseInterceptor('image/jpeg'))
|
||||
@ApiFileAcceptHeader('image/jpeg')
|
||||
async screenshot(
|
||||
@Res({ passthrough: true }) res: Response,
|
||||
@Query() sessionQuery: SessionQuery,
|
||||
|
||||
@@ -1,15 +1,12 @@
|
||||
import * as process from 'node:process';
|
||||
import * as v8 from 'node:v8';
|
||||
|
||||
import {
|
||||
Body,
|
||||
Controller,
|
||||
Get,
|
||||
Logger,
|
||||
NotFoundException,
|
||||
Post,
|
||||
Query,
|
||||
StreamableFile,
|
||||
UsePipes,
|
||||
} from '@nestjs/common';
|
||||
import { ApiOperation, ApiSecurity, ApiTags } from '@nestjs/swagger';
|
||||
@@ -112,34 +109,3 @@ export class ServerController {
|
||||
return { stopping: true };
|
||||
}
|
||||
}
|
||||
|
||||
@ApiSecurity('api_key')
|
||||
@Controller('api/server/debug')
|
||||
@ApiTags('🔍 Observability')
|
||||
export class ServerDebugController {
|
||||
private logger: Logger;
|
||||
private readonly enabled: boolean;
|
||||
|
||||
constructor(protected config: WhatsappConfigService) {
|
||||
this.logger = new Logger('ServerDebugController');
|
||||
this.enabled = this.config.debugModeEnabled;
|
||||
}
|
||||
|
||||
@Get('heapsnapshot')
|
||||
@ApiOperation({
|
||||
summary: 'Return a heapsnapshot',
|
||||
description: "Return a heapsnapshot of the server's memory",
|
||||
})
|
||||
async heapsnapshot() {
|
||||
if (!this.enabled) {
|
||||
throw new NotFoundException('WAHA_DEBUG_MODE is disabled');
|
||||
}
|
||||
this.logger.log('Creating a heap snapshot...');
|
||||
const heap = v8.getHeapSnapshot();
|
||||
const fileName = `${Date.now()}.heapsnapshot`;
|
||||
return new StreamableFile(heap, {
|
||||
type: 'application/octet-stream',
|
||||
disposition: `attachment; filename=${fileName}`,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
import * as v8 from 'node:v8';
|
||||
|
||||
import {
|
||||
Controller,
|
||||
Get,
|
||||
Logger,
|
||||
NotFoundException,
|
||||
Query,
|
||||
StreamableFile,
|
||||
UsePipes,
|
||||
} from '@nestjs/common';
|
||||
import { ApiOperation, ApiSecurity, ApiTags } from '@nestjs/swagger';
|
||||
import { WhatsappConfigService } from '@waha/config.service';
|
||||
import { SessionManager } from '@waha/core/abc/manager.abc';
|
||||
import { WhatsappSession } from '@waha/core/abc/session.abc';
|
||||
import {
|
||||
SessionApiParam,
|
||||
WorkingSessionParam,
|
||||
} from '@waha/nestjs/params/SessionApiParam';
|
||||
import { WAHAValidationPipe } from '@waha/nestjs/pipes/WAHAValidationPipe';
|
||||
import { BrowserTraceQuery } from '@waha/structures/server.debug.dto';
|
||||
import { createReadStream } from 'fs';
|
||||
|
||||
@ApiSecurity('api_key')
|
||||
@Controller('api/server/debug')
|
||||
@ApiTags('🔍 Observability')
|
||||
export class ServerDebugController {
|
||||
private logger: Logger;
|
||||
private readonly enabled: boolean;
|
||||
|
||||
constructor(
|
||||
private config: WhatsappConfigService,
|
||||
private manager: SessionManager,
|
||||
) {
|
||||
this.logger = new Logger('ServerDebugController');
|
||||
this.enabled = this.config.debugModeEnabled;
|
||||
}
|
||||
|
||||
@Get('heapsnapshot')
|
||||
@ApiOperation({
|
||||
summary: 'Return a heapsnapshot for the current nodejs process',
|
||||
description: "Return a heapsnapshot of the server's memory",
|
||||
})
|
||||
async heapsnapshot() {
|
||||
if (!this.enabled) {
|
||||
throw new NotFoundException('WAHA_DEBUG_MODE is disabled');
|
||||
}
|
||||
this.logger.log('Creating a heap snapshot...');
|
||||
const heap = v8.getHeapSnapshot();
|
||||
const fileName = `${Date.now()}.heapsnapshot`;
|
||||
return new StreamableFile(heap, {
|
||||
type: 'application/octet-stream',
|
||||
disposition: `attachment; filename=${fileName}`,
|
||||
});
|
||||
}
|
||||
|
||||
@Get('browser/trace/:session')
|
||||
@ApiOperation({
|
||||
summary: 'Collect and get a trace.json for Chrome DevTools ',
|
||||
description: 'Uses https://pptr.dev/api/puppeteer.tracing',
|
||||
})
|
||||
@SessionApiParam
|
||||
@UsePipes(new WAHAValidationPipe())
|
||||
async browserTrace(
|
||||
@WorkingSessionParam session: WhatsappSession,
|
||||
@Query() query: BrowserTraceQuery,
|
||||
) {
|
||||
if (!this.enabled) {
|
||||
throw new NotFoundException('WAHA_DEBUG_MODE is disabled');
|
||||
}
|
||||
const filepath = await session.browserTrace(query);
|
||||
const stream = createReadStream(filepath);
|
||||
const filename = `trace - ${session.name} - ${new Date()}.json`;
|
||||
return new StreamableFile(stream, {
|
||||
type: 'application/octet-stream',
|
||||
disposition: `attachment; filename=${filename}`,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -233,6 +233,9 @@ class SessionsController {
|
||||
@Body() request: SessionStartDeprecatedRequest,
|
||||
): Promise<SessionDTO> {
|
||||
const name = request.name;
|
||||
if (!request.name) {
|
||||
throw new UnprocessableEntityException('Session name is required');
|
||||
}
|
||||
if (this.manager.isRunning(name)) {
|
||||
const msg = `Session '${name}' is already started.`;
|
||||
throw new UnprocessableEntityException(msg);
|
||||
@@ -255,6 +258,9 @@ class SessionsController {
|
||||
async DEPRECATED_stop(
|
||||
@Body() request: SessionStopDeprecatedRequest,
|
||||
): Promise<void> {
|
||||
if (!request.name) {
|
||||
throw new UnprocessableEntityException('Session name is required');
|
||||
}
|
||||
const name = request.name;
|
||||
if (request.logout) {
|
||||
// Old API did remove the session complete
|
||||
@@ -283,6 +289,9 @@ class SessionsController {
|
||||
async DEPRECATED_logout(
|
||||
@Body() request: SessionLogoutDeprecatedRequest,
|
||||
): Promise<void> {
|
||||
if (!request.name) {
|
||||
throw new UnprocessableEntityException('Session name is required');
|
||||
}
|
||||
const name = request.name;
|
||||
await this.withLock(name, async () => {
|
||||
await this.manager.unassign(name);
|
||||
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
WebSocketServer,
|
||||
} from '@nestjs/websockets';
|
||||
import { SessionManager } from '@waha/core/abc/manager.abc';
|
||||
import { WebSocketAuth } from '@waha/core/auth/WebSocketAuth';
|
||||
import { WebsocketHeartbeatJob } from '@waha/nestjs/ws/WebsocketHeartbeatJob';
|
||||
import { WebSocket } from '@waha/nestjs/ws/ws';
|
||||
import { WAHAEvents, WAHAEventsWild } from '@waha/structures/enums.dto';
|
||||
@@ -21,6 +22,15 @@ import { IncomingMessage } from 'http';
|
||||
import * as url from 'url';
|
||||
import { Server } from 'ws';
|
||||
|
||||
export enum WebSocketCloseCode {
|
||||
NORMAL = 1000,
|
||||
GOING_AWAY = 1001,
|
||||
PROTOCOL_ERROR = 1002,
|
||||
UNSUPPORTED_DATA = 1003,
|
||||
POLICY_VIOLATION = 1008,
|
||||
INTERNAL_ERROR = 1011,
|
||||
}
|
||||
|
||||
@WebSocketGateway({
|
||||
path: '/ws',
|
||||
cors: true,
|
||||
@@ -41,7 +51,10 @@ export class WebsocketGatewayCore
|
||||
private heartbeat: WebsocketHeartbeatJob;
|
||||
private eventUnmask = new EventWildUnmask(WAHAEvents, WAHAEventsWild);
|
||||
|
||||
constructor(private manager: SessionManager) {
|
||||
constructor(
|
||||
private manager: SessionManager,
|
||||
private auth: WebSocketAuth,
|
||||
) {
|
||||
this.logger = new Logger('WebsocketGateway');
|
||||
this.heartbeat = new WebsocketHeartbeatJob(
|
||||
this.logger,
|
||||
@@ -51,14 +64,23 @@ export class WebsocketGatewayCore
|
||||
|
||||
handleConnection(socket: WebSocket, request: IncomingMessage, ...args): any {
|
||||
// wsc - websocket client
|
||||
const id = generatePrefixedId('wsc');
|
||||
socket.id = id;
|
||||
this.logger.debug(`New client connected: ${request.url}`);
|
||||
socket.id = generatePrefixedId('wsc');
|
||||
|
||||
if (!this.auth.validateRequest(request)) {
|
||||
// Not authorized - close connection
|
||||
socket.close(WebSocketCloseCode.POLICY_VIOLATION, 'Unauthorized');
|
||||
this.logger.debug(
|
||||
`Unauthorized websocket connection attempt: ${request.url} - ${socket.id}`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
this.logger.debug(`New client connected: ${request.url} - ${socket.id}`);
|
||||
const params = this.getParams(request);
|
||||
const session: string = params.session;
|
||||
const events: WAHAEvents[] = params.events;
|
||||
this.logger.debug(
|
||||
`Client connected to session: '${session}', events: ${events}, ${id}`,
|
||||
`Client connected to session: '${session}', events: ${events}, ${socket.id}`,
|
||||
);
|
||||
|
||||
const sub = this.manager
|
||||
@@ -97,26 +119,11 @@ export class WebsocketGatewayCore
|
||||
async beforeApplicationShutdown(signal?: string) {
|
||||
this.logger.log('Shutting down websocket server');
|
||||
this.heartbeat?.stop();
|
||||
|
||||
// Allow pending messages to be sent, it can be even 1ms, just to release the event loop
|
||||
await sleep(100);
|
||||
// Close clients and server
|
||||
await this.close(this.server);
|
||||
this.logger.log('Websocket server is down');
|
||||
}
|
||||
|
||||
// Cherry-pick from nestjs new version
|
||||
// https://github.com/nestjs/nest/pull/13531/files
|
||||
private async close(server: any) {
|
||||
const closeEventSignal = new Promise((resolve, reject) =>
|
||||
server.close((err) => (err ? reject(err) : resolve(undefined))),
|
||||
);
|
||||
for (const ws of server.clients) {
|
||||
ws.terminate();
|
||||
}
|
||||
await closeEventSignal;
|
||||
}
|
||||
|
||||
afterInit(server: Server) {
|
||||
this.logger.debug('Websocket server initialized');
|
||||
|
||||
@@ -138,7 +138,10 @@ export class WhatsappConfigService implements OnApplicationBootstrap {
|
||||
}
|
||||
|
||||
getApiKey(): string | undefined {
|
||||
return this.configService.get('WHATSAPP_API_KEY', '');
|
||||
return (
|
||||
this.configService.get('WHATSAPP_API_KEY', '') ||
|
||||
this.configService.get('WAHA_API_KEY', '')
|
||||
);
|
||||
}
|
||||
|
||||
getExcludedPaths(): string[] {
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { INestApplication } from '@nestjs/common';
|
||||
import { DocumentBuilder, OpenAPIObject, SwaggerModule } from '@nestjs/swagger';
|
||||
import { DECORATORS } from '@nestjs/swagger/dist/constants';
|
||||
import { BasicAuthFunction } from '@waha/core/auth/basicAuth';
|
||||
import { DashboardConfigServiceCore } from '@waha/core/config/DashboardConfigServiceCore';
|
||||
import { Logger } from 'nestjs-pino';
|
||||
|
||||
import { WhatsappConfigService } from '../config.service';
|
||||
@@ -9,16 +11,22 @@ import { SwaggerConfigServiceCore } from './config/SwaggerConfigServiceCore';
|
||||
|
||||
export class SwaggerConfiguratorCore {
|
||||
protected logger: any;
|
||||
private config: SwaggerConfigServiceCore;
|
||||
|
||||
constructor(protected app: INestApplication) {
|
||||
this.logger = app.get(Logger);
|
||||
this.config = app.get(SwaggerConfigServiceCore);
|
||||
}
|
||||
|
||||
get title() {
|
||||
return 'WAHA - WhatsApp HTTP API';
|
||||
return this.config.title || 'WAHA - WhatsApp HTTP API';
|
||||
}
|
||||
|
||||
get description() {
|
||||
if (this.config.description) {
|
||||
return this.config.description;
|
||||
}
|
||||
|
||||
return (
|
||||
'<b>WhatsApp HTTP API</b> that you can run in a click!<br/>' +
|
||||
'<a href="/dashboard"><b>📊 Dashboard</b></a><br/>' +
|
||||
@@ -43,10 +51,19 @@ export class SwaggerConfiguratorCore {
|
||||
}
|
||||
|
||||
get externalDocUrl() {
|
||||
return 'https://waha.devlike.pro/';
|
||||
return this.config.externalDocUrl || 'https://waha.devlike.pro/';
|
||||
}
|
||||
|
||||
configure(webhooks: any[]) {
|
||||
if (!this.config.enabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
const credentials = this.config.credentials;
|
||||
if (credentials) {
|
||||
this.setUpAuth(credentials);
|
||||
}
|
||||
|
||||
const app = this.app;
|
||||
const builder = new DocumentBuilder();
|
||||
|
||||
@@ -71,10 +88,12 @@ export class SwaggerConfiguratorCore {
|
||||
)
|
||||
.addTag('👥 Groups', `Groups methods.<br>`)
|
||||
.addTag('✅ Presence', `Presence information`)
|
||||
.addTag('📅 Events', `Event Message`)
|
||||
.addTag(
|
||||
'🏷️ Labels',
|
||||
'Labels - available only for WhatsApp Business accounts',
|
||||
)
|
||||
.addTag('🖼️ Media', 'Media methods')
|
||||
.addTag('🔍 Observability', 'Other methods')
|
||||
.addTag('🗄️ Storage', 'Storage methods')
|
||||
.addApiKey({
|
||||
@@ -161,4 +180,12 @@ export class SwaggerConfiguratorCore {
|
||||
document.webhooks = webhooks;
|
||||
return document;
|
||||
}
|
||||
|
||||
setUpAuth(credentials: [string, string]): void {
|
||||
const [username, password] = credentials;
|
||||
const dashboardConfig = this.app.get(DashboardConfigServiceCore);
|
||||
const exclude = ['/api/', dashboardConfig.dashboardUri, '/health', '/ws'];
|
||||
const authFunction = BasicAuthFunction(username, password, exclude);
|
||||
this.app.use(authFunction);
|
||||
}
|
||||
}
|
||||
@@ -50,7 +50,6 @@ export abstract class SessionManager
|
||||
WAIT_SESSION_RUNNING_TIMEOUT = 5_000;
|
||||
WAIT_STATUS_INTERVAL = 500;
|
||||
WAIT_STATUS_TIMEOUT = 10_000;
|
||||
LOCK_TIMEOUT = 10_000;
|
||||
|
||||
protected constructor(
|
||||
protected log: PinoLogger,
|
||||
@@ -58,8 +57,9 @@ export abstract class SessionManager
|
||||
protected gowsConfigService: GowsEngineConfigService,
|
||||
) {
|
||||
this.lock = new AsyncLock({
|
||||
timeout: 5_000,
|
||||
maxPending: Infinity,
|
||||
timeout: this.LOCK_TIMEOUT,
|
||||
maxExecutionTime: 30_000,
|
||||
});
|
||||
this.log.setContext(SessionManager.name);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
import { isJidBroadcast } from '@adiwajshing/baileys/lib/WABinary/jid-utils';
|
||||
import {
|
||||
CoreMediaConverter,
|
||||
IMediaConverter,
|
||||
} from '@waha/core/media/IConverter';
|
||||
import { MessagesForRead } from '@waha/core/utils/convertors';
|
||||
import { isJidNewsletter } from '@waha/core/utils/jids';
|
||||
import {
|
||||
@@ -16,19 +20,24 @@ import {
|
||||
GetChatMessageQuery,
|
||||
GetChatMessagesFilter,
|
||||
GetChatMessagesQuery,
|
||||
OverviewFilter,
|
||||
ReadChatMessagesQuery,
|
||||
ReadChatMessagesResponse,
|
||||
} from '@waha/structures/chats.dto';
|
||||
import { SendButtonsRequest } from '@waha/structures/chatting.buttons.dto';
|
||||
import { BinaryFile, RemoteFile } from '@waha/structures/files.dto';
|
||||
import { Label, LabelDTO, LabelID } from '@waha/structures/labels.dto';
|
||||
import { LidToPhoneNumber } from '@waha/structures/lids.dto';
|
||||
import { PaginationParams } from '@waha/structures/pagination.dto';
|
||||
import { MessageSource, WAMessage } from '@waha/structures/responses.dto';
|
||||
import { BrowserTraceQuery } from '@waha/structures/server.debug.dto';
|
||||
import { DefaultMap } from '@waha/utils/DefaultMap';
|
||||
import { generatePrefixedId } from '@waha/utils/ids';
|
||||
import { LoggerBuilder } from '@waha/utils/logging';
|
||||
import { complete } from '@waha/utils/reactive/complete';
|
||||
import { SwitchObservable } from '@waha/utils/reactive/SwitchObservable';
|
||||
import axios from 'axios';
|
||||
import axiosRetry from 'axios-retry';
|
||||
import * as fs from 'fs';
|
||||
import * as lodash from 'lodash';
|
||||
import * as NodeCache from 'node-cache';
|
||||
@@ -75,6 +84,10 @@ import {
|
||||
WAHAPresenceStatus,
|
||||
WAHASessionStatus,
|
||||
} from '../../structures/enums.dto';
|
||||
import {
|
||||
EventCancelRequest,
|
||||
EventMessageRequest,
|
||||
} from '../../structures/events.dto';
|
||||
import {
|
||||
CreateGroupRequest,
|
||||
GroupField,
|
||||
@@ -100,9 +113,12 @@ import { NotImplementedByEngineError } from '../exceptions';
|
||||
import { IMediaManager } from '../media/IMediaManager';
|
||||
import { QR } from '../QR';
|
||||
import { DataStore } from './DataStore';
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
const qrcode = require('qrcode-terminal');
|
||||
|
||||
axiosRetry(axios, { retries: 3 });
|
||||
|
||||
const CHROME_PATH = '/usr/bin/google-chrome-stable';
|
||||
const CHROMIUM_PATH = '/usr/bin/chromium';
|
||||
|
||||
@@ -159,6 +175,8 @@ export abstract class WhatsappSession {
|
||||
stdTTL: 10 * 60, // 10 minutes
|
||||
});
|
||||
|
||||
public mediaConverter: IMediaConverter = new CoreMediaConverter();
|
||||
|
||||
public constructor({
|
||||
name,
|
||||
printQR,
|
||||
@@ -268,7 +286,6 @@ export abstract class WhatsappSession {
|
||||
'--aggressive-cache-discard',
|
||||
'--disable-accelerated-2d-canvas',
|
||||
'--disable-application-cache',
|
||||
// '--disable-background-networking', // COMMENTED to test WEBJS stability
|
||||
// DO NOT disable software rasterizer, it will break the video
|
||||
// https://github.com/devlikeapro/waha/issues/629
|
||||
// '--disable-software-rasterizer',
|
||||
@@ -303,9 +320,8 @@ export abstract class WhatsappSession {
|
||||
'--no-sandbox',
|
||||
'--no-zygote',
|
||||
'--password-store=basic',
|
||||
// '--renderer-process-limit=2', // COMMENTED to test WEBJS stability
|
||||
'--renderer-process-limit=1',
|
||||
'--safebrowsing-disable-auto-update',
|
||||
// '--single-process',
|
||||
'--use-mock-keychain',
|
||||
'--window-size=1280,720',
|
||||
];
|
||||
@@ -334,6 +350,10 @@ export abstract class WhatsappSession {
|
||||
* START - Methods for API
|
||||
*/
|
||||
|
||||
public browserTrace(query: BrowserTraceQuery): Promise<string> {
|
||||
throw new NotImplementedByEngineError();
|
||||
}
|
||||
|
||||
/**
|
||||
* Auth methods
|
||||
*/
|
||||
@@ -466,6 +486,14 @@ export abstract class WhatsappSession {
|
||||
throw new NotImplementedByEngineError();
|
||||
}
|
||||
|
||||
sendEvent(request: EventMessageRequest): Promise<WAMessage> {
|
||||
throw new NotImplementedByEngineError();
|
||||
}
|
||||
|
||||
cancelEvent(eventId: string): Promise<WAMessage> {
|
||||
throw new NotImplementedByEngineError();
|
||||
}
|
||||
|
||||
/**
|
||||
* Chats methods
|
||||
*/
|
||||
@@ -475,6 +503,7 @@ export abstract class WhatsappSession {
|
||||
|
||||
public getChatsOverview(
|
||||
pagination: PaginationParams,
|
||||
filter?: OverviewFilter,
|
||||
): Promise<ChatSummary[]> {
|
||||
throw new NotImplementedByEngineError();
|
||||
}
|
||||
@@ -615,6 +644,29 @@ export abstract class WhatsappSession {
|
||||
throw new NotImplementedByEngineError();
|
||||
}
|
||||
|
||||
/**
|
||||
* Lid to Phone Number methods
|
||||
*/
|
||||
public async getAllLids(
|
||||
pagination: PaginationParams,
|
||||
): Promise<Array<LidToPhoneNumber>> {
|
||||
throw new NotImplementedByEngineError();
|
||||
}
|
||||
|
||||
public async getLidsCount(): Promise<number> {
|
||||
throw new NotImplementedByEngineError();
|
||||
}
|
||||
|
||||
public async findPNByLid(lid: string): Promise<LidToPhoneNumber> {
|
||||
throw new NotImplementedByEngineError();
|
||||
}
|
||||
|
||||
public async findLIDByPhoneNumber(
|
||||
phoneNumber: string,
|
||||
): Promise<LidToPhoneNumber> {
|
||||
throw new NotImplementedByEngineError();
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch the latest profile picture of the contact (group, newsletter, etc.)
|
||||
* @param id
|
||||
@@ -633,10 +685,17 @@ export abstract class WhatsappSession {
|
||||
}
|
||||
|
||||
protected async refreshProfilePicture(id: string) {
|
||||
let fn: Promise<string>;
|
||||
if (isJidBroadcast(id)) {
|
||||
this.logger.debug(`Refreshing profile picture for id "${id}"...`);
|
||||
// Have no pictures
|
||||
if (id === '0@c.us' || id === '0@s.whatsapp.net') {
|
||||
return null;
|
||||
} else if (isJidNewsletter(id)) {
|
||||
} else if (isJidBroadcast(id)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Find the right method
|
||||
let fn: Promise<string>;
|
||||
if (isJidNewsletter(id)) {
|
||||
fn = this.channelsGetChannel(id).then(
|
||||
(channel: Channel) => channel.picture || channel.preview,
|
||||
);
|
||||
@@ -952,6 +1011,15 @@ export abstract class WhatsappSession {
|
||||
const api = this.sentMessageIds.has(id);
|
||||
return api ? MessageSource.API : MessageSource.APP;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches the content from the specified URL and returns it as a Buffer.
|
||||
*/
|
||||
public async fetch(url: string): Promise<Buffer> {
|
||||
return axios.get(url, { responseType: 'arraybuffer' }).then((res) => {
|
||||
return Buffer.from(res.data);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export function getGroupInviteLink(code: string) {
|
||||
|
||||
+79
-21
@@ -1,29 +1,40 @@
|
||||
import { INestApplication, Module } from '@nestjs/common';
|
||||
import * as process from 'node:process';
|
||||
|
||||
import { INestApplication, MiddlewareConsumer, Module } from '@nestjs/common';
|
||||
import { Provider } from '@nestjs/common/interfaces/modules/provider.interface';
|
||||
import { ConfigModule } from '@nestjs/config';
|
||||
import { APP_INTERCEPTOR } from '@nestjs/core';
|
||||
import { PassportModule } from '@nestjs/passport';
|
||||
import { ServeStaticModule } from '@nestjs/serve-static';
|
||||
import { TerminusModule } from '@nestjs/terminus';
|
||||
import { ChannelsController } from '@waha/api/channels.controller';
|
||||
import { LidsController } from '@waha/api/lids.controller';
|
||||
import { ProfileController } from '@waha/api/profile.controller';
|
||||
import {
|
||||
ServerController,
|
||||
ServerDebugController,
|
||||
} from '@waha/api/server.controller';
|
||||
import { WebsocketGatewayCore } from '@waha/core/api/websocket.gateway.core';
|
||||
import { ServerController } from '@waha/api/server.controller';
|
||||
import { ServerDebugController } from '@waha/api/server.debug.controller';
|
||||
import { WebsocketGatewayCore } from '@waha/api/websocket.gateway.core';
|
||||
import { ApiKeyStrategy } from '@waha/core/auth/apiKey.strategy';
|
||||
import { IApiKeyAuth } from '@waha/core/auth/auth';
|
||||
import { AuthMiddleware } from '@waha/core/auth/auth.middleware';
|
||||
import { BasicAuthFunction } from '@waha/core/auth/basicAuth';
|
||||
import { WebSocketAuth } from '@waha/core/auth/WebSocketAuth';
|
||||
import { GowsEngineConfigService } from '@waha/core/config/GowsEngineConfigService';
|
||||
import { WebJSEngineConfigService } from '@waha/core/config/WebJSEngineConfigService';
|
||||
import { MediaLocalStorageModule } from '@waha/core/media/local/media.local.storage.module';
|
||||
import { MediaLocalStorageConfig } from '@waha/core/media/local/MediaLocalStorageConfig';
|
||||
import { ChannelsInfoServiceCore } from '@waha/core/services/ChannelsInfoServiceCore';
|
||||
import { parseBool } from '@waha/helpers';
|
||||
import { BufferJsonReplacerInterceptor } from '@waha/nestjs/BufferJsonReplacerInterceptor';
|
||||
import { HttpsExpress } from '@waha/nestjs/HttpsExpress';
|
||||
import {
|
||||
getPinoHttpUseLevel,
|
||||
getPinoLogLevel,
|
||||
getPinoTransport,
|
||||
} from '@waha/utils/logging';
|
||||
import { noSlashAtTheEnd } from '@waha/utils/string';
|
||||
import * as Joi from 'joi';
|
||||
import { LoggerModule } from 'nestjs-pino';
|
||||
import { Logger as NestJSPinoLogger } from 'nestjs-pino';
|
||||
import { join } from 'path';
|
||||
import { Logger } from 'pino';
|
||||
|
||||
@@ -31,9 +42,11 @@ import { AuthController } from '../api/auth.controller';
|
||||
import { ChatsController } from '../api/chats.controller';
|
||||
import { ChattingController } from '../api/chatting.controller';
|
||||
import { ContactsController } from '../api/contacts.controller';
|
||||
import { EventsController } from '../api/events.controller';
|
||||
import { GroupsController } from '../api/groups.controller';
|
||||
import { HealthController } from '../api/health.controller';
|
||||
import { LabelsController } from '../api/labels.controller';
|
||||
import { MediaController } from '../api/media.controller';
|
||||
import { PingController } from '../api/ping.controller';
|
||||
import { PresenceController } from '../api/presence.controller';
|
||||
import { ScreenshotController } from '../api/screenshot.controller';
|
||||
@@ -43,6 +56,7 @@ import { VersionController } from '../api/version.controller';
|
||||
import { WhatsappConfigService } from '../config.service';
|
||||
import { SessionManager } from './abc/manager.abc';
|
||||
import { WAHAHealthCheckService } from './abc/WAHAHealthCheckService';
|
||||
import { ApiKeyAuthFactory } from './auth/ApiKeyAuthFactory';
|
||||
import { DashboardConfigServiceCore } from './config/DashboardConfigServiceCore';
|
||||
import { EngineConfigService } from './config/EngineConfigService';
|
||||
import { SwaggerConfigServiceCore } from './config/SwaggerConfigServiceCore';
|
||||
@@ -60,6 +74,7 @@ export const IMPORTS_CORE = [
|
||||
autoLogging: {
|
||||
ignore: (req) => {
|
||||
return (
|
||||
req.url.startsWith('/ping') ||
|
||||
req.url.startsWith('/dashboard/') ||
|
||||
req.url.startsWith('/api/files/') ||
|
||||
req.url.startsWith('/api/s3/')
|
||||
@@ -129,15 +144,40 @@ export const CONTROLLERS = [
|
||||
StatusController,
|
||||
LabelsController,
|
||||
ContactsController,
|
||||
LidsController,
|
||||
GroupsController,
|
||||
PresenceController,
|
||||
ScreenshotController,
|
||||
EventsController,
|
||||
PingController,
|
||||
HealthController,
|
||||
ServerController,
|
||||
ServerDebugController,
|
||||
VersionController,
|
||||
MediaController,
|
||||
];
|
||||
export const PROVIDERS_BASE: Provider[] = [
|
||||
{
|
||||
provide: APP_INTERCEPTOR,
|
||||
useClass: BufferJsonReplacerInterceptor,
|
||||
},
|
||||
DashboardConfigServiceCore,
|
||||
SwaggerConfigServiceCore,
|
||||
WebJSEngineConfigService,
|
||||
GowsEngineConfigService,
|
||||
WhatsappConfigService,
|
||||
EngineConfigService,
|
||||
WebsocketGatewayCore,
|
||||
MediaLocalStorageConfig,
|
||||
WebSocketAuth,
|
||||
ApiKeyStrategy,
|
||||
{
|
||||
provide: IApiKeyAuth,
|
||||
useFactory: ApiKeyAuthFactory,
|
||||
inject: [WhatsappConfigService, NestJSPinoLogger],
|
||||
},
|
||||
];
|
||||
|
||||
const PROVIDERS = [
|
||||
{
|
||||
provide: SessionManager,
|
||||
@@ -147,19 +187,8 @@ const PROVIDERS = [
|
||||
provide: WAHAHealthCheckService,
|
||||
useClass: WAHAHealthCheckServiceCore,
|
||||
},
|
||||
{
|
||||
provide: APP_INTERCEPTOR,
|
||||
useClass: BufferJsonReplacerInterceptor,
|
||||
},
|
||||
ChannelsInfoServiceCore,
|
||||
DashboardConfigServiceCore,
|
||||
SwaggerConfigServiceCore,
|
||||
WebJSEngineConfigService,
|
||||
GowsEngineConfigService,
|
||||
WhatsappConfigService,
|
||||
EngineConfigService,
|
||||
WebsocketGatewayCore,
|
||||
MediaLocalStorageConfig,
|
||||
...PROVIDERS_BASE,
|
||||
];
|
||||
|
||||
@Module({
|
||||
@@ -170,15 +199,44 @@ const PROVIDERS = [
|
||||
export class AppModuleCore {
|
||||
public startTimestamp: number;
|
||||
|
||||
constructor(protected config: WhatsappConfigService) {
|
||||
constructor(
|
||||
protected config: WhatsappConfigService,
|
||||
private dashboardConfig: DashboardConfigServiceCore,
|
||||
) {
|
||||
this.startTimestamp = Date.now();
|
||||
}
|
||||
|
||||
static getHttpsOptions(logger: Logger) {
|
||||
return undefined;
|
||||
const httpsEnabled = parseBool(process.env.WAHA_HTTPS_ENABLED);
|
||||
if (!httpsEnabled) {
|
||||
return undefined;
|
||||
}
|
||||
const httpsExpress = new HttpsExpress(logger);
|
||||
return httpsExpress.readSync();
|
||||
}
|
||||
|
||||
static appReady(app: INestApplication, logger: Logger) {
|
||||
return;
|
||||
const httpsEnabled = parseBool(process.env.WAHA_HTTPS_ENABLED);
|
||||
if (!httpsEnabled) {
|
||||
return;
|
||||
}
|
||||
const httpd = app.getHttpServer();
|
||||
const httpsExpress = new HttpsExpress(logger);
|
||||
httpsExpress.watchCertChanges(httpd);
|
||||
}
|
||||
|
||||
configure(consumer: MiddlewareConsumer) {
|
||||
const exclude = this.config.getExcludedPaths();
|
||||
consumer
|
||||
.apply(AuthMiddleware)
|
||||
.exclude(...exclude)
|
||||
.forRoutes('api', 'health', 'ws');
|
||||
const dashboardCredentials = this.dashboardConfig.credentials;
|
||||
if (dashboardCredentials) {
|
||||
const username = dashboardCredentials[0];
|
||||
const password = dashboardCredentials[1];
|
||||
const route = noSlashAtTheEnd(this.dashboardConfig.dashboardUri);
|
||||
consumer.apply(BasicAuthFunction(username, password)).forRoutes(route);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
import { LoggerService } from '@nestjs/common';
|
||||
import { WhatsappConfigService } from '@waha/config.service';
|
||||
import {
|
||||
HashAuth,
|
||||
IApiKeyAuth,
|
||||
NoAuth,
|
||||
PlainApiKeyAuth,
|
||||
} from '@waha/core/auth/auth';
|
||||
|
||||
export function ApiKeyAuthFactory(
|
||||
config: WhatsappConfigService,
|
||||
logger: LoggerService,
|
||||
): IApiKeyAuth {
|
||||
const apiKey = config.getApiKey();
|
||||
if (!apiKey) {
|
||||
setTimeout(() => {
|
||||
logger.warn('🚫🚫🚫🚫🚫🚫🚫🚫🚫🚫🚫🚫🚫🚫🚫🚫🚫🚫🚫🚫🚫🚫🚫🚫🚫');
|
||||
logger.warn('WARNING: No API key detected. This is a security risk.');
|
||||
logger.warn(
|
||||
'Your API is publicly accessible without any authentication.',
|
||||
);
|
||||
logger.warn(
|
||||
'To secure your API, set environment variable: WAHA_API_KEY=your_api_key',
|
||||
);
|
||||
logger.warn(
|
||||
'For better security, use WAHA_API_KEY=sha512:{SHA512_HASH_FOR_YOUR_API_KEY}',
|
||||
);
|
||||
logger.warn('🚫🚫🚫🚫🚫🚫🚫🚫🚫🚫🚫🚫🚫🚫🚫🚫🚫🚫🚫🚫🚫🚫🚫🚫🚫');
|
||||
}, 3000);
|
||||
return new NoAuth();
|
||||
}
|
||||
|
||||
if (apiKey.startsWith('sha512:')) {
|
||||
const hash = apiKey.slice(7);
|
||||
return new HashAuth(hash, 'sha512');
|
||||
}
|
||||
|
||||
// Fallback to plain text
|
||||
setTimeout(() => {
|
||||
logger.warn('⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️');
|
||||
logger.warn(
|
||||
'WARNING: Plain text API key detected. This is a security risk.',
|
||||
);
|
||||
logger.warn(
|
||||
'Your API key can be exposed in environment variables or process lists.',
|
||||
);
|
||||
logger.warn(
|
||||
'For better security, use WAHA_API_KEY=sha512:{SHA512_HASH_FOR_YOUR_API_KEY}',
|
||||
);
|
||||
logger.warn('⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️');
|
||||
}, 2000);
|
||||
return new PlainApiKeyAuth(apiKey);
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { IncomingMessage } from 'http';
|
||||
import * as url from 'url';
|
||||
|
||||
import { IApiKeyAuth } from './auth';
|
||||
|
||||
@Injectable()
|
||||
export class WebSocketAuth {
|
||||
constructor(private auth: IApiKeyAuth) {}
|
||||
|
||||
validateRequest(request: IncomingMessage) {
|
||||
if (this.auth.skipAuth()) {
|
||||
return true;
|
||||
}
|
||||
const provided = this.getKeyFromQueryParams(request);
|
||||
return this.auth.isValid(provided);
|
||||
}
|
||||
|
||||
private getKeyFromQueryParams(request: IncomingMessage) {
|
||||
let query = url.parse(request.url, true).query;
|
||||
// case-insensitive query params
|
||||
query = Object.keys(query).reduce((acc, key) => {
|
||||
acc[key.toLowerCase()] = query[key];
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
const provided = query['x-api-key'];
|
||||
// Check if it's array - return first
|
||||
if (Array.isArray(provided)) {
|
||||
return provided[0];
|
||||
}
|
||||
return provided;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { PassportStrategy } from '@nestjs/passport';
|
||||
import { IApiKeyAuth } from '@waha/core/auth/auth';
|
||||
import { HeaderAPIKeyStrategy } from 'passport-headerapikey';
|
||||
|
||||
@Injectable()
|
||||
export class ApiKeyStrategy extends PassportStrategy(HeaderAPIKeyStrategy) {
|
||||
constructor(private auth: IApiKeyAuth) {
|
||||
super({ header: 'X-Api-Key', prefix: '' }, true, (apikey, done) => {
|
||||
const isValid = this.auth.isValid(apikey);
|
||||
return done(isValid);
|
||||
});
|
||||
}
|
||||
|
||||
validate(apikey: string, done: (result: boolean) => void): void {
|
||||
const isValid = this.auth.isValid(apikey);
|
||||
return done(isValid);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
import {
|
||||
Injectable,
|
||||
NestMiddleware,
|
||||
UnauthorizedException,
|
||||
} from '@nestjs/common';
|
||||
import * as passport from 'passport';
|
||||
|
||||
import { IApiKeyAuth } from './auth';
|
||||
|
||||
@Injectable()
|
||||
export class AuthMiddleware implements NestMiddleware {
|
||||
constructor(private auth: IApiKeyAuth) {}
|
||||
|
||||
use(req: any, res: any, next: () => void) {
|
||||
// Skip authentication if auth says so
|
||||
if (this.auth.skipAuth()) {
|
||||
next();
|
||||
return;
|
||||
}
|
||||
|
||||
passport.authenticate('headerapikey', { session: false }, (value) => {
|
||||
if (!value) {
|
||||
throw new UnauthorizedException();
|
||||
}
|
||||
next();
|
||||
})(req, res, next);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
import * as crypto from 'crypto';
|
||||
|
||||
export abstract class IApiKeyAuth {
|
||||
abstract isValid(plain: string): boolean;
|
||||
|
||||
abstract skipAuth(): boolean;
|
||||
}
|
||||
|
||||
export class NoAuth implements IApiKeyAuth {
|
||||
isValid(plain: string): boolean {
|
||||
return true;
|
||||
}
|
||||
|
||||
skipAuth(): boolean {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
export class PlainApiKeyAuth implements IApiKeyAuth {
|
||||
constructor(private key: string) {}
|
||||
|
||||
isValid(plain: string): boolean {
|
||||
return compare(plain, this.key);
|
||||
}
|
||||
|
||||
skipAuth(): boolean {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export class HashAuth implements IApiKeyAuth {
|
||||
constructor(
|
||||
private hash: string,
|
||||
private algorithm: string,
|
||||
) {
|
||||
this.algorithm = algorithm;
|
||||
}
|
||||
|
||||
isValid(plain: string): boolean {
|
||||
if (!plain) {
|
||||
return false;
|
||||
}
|
||||
const hash = crypto.createHash(this.algorithm).update(plain).digest('hex');
|
||||
return compare(hash, this.hash);
|
||||
}
|
||||
|
||||
skipAuth(): boolean {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Securely compare 2 strings
|
||||
*/
|
||||
export function compare(provided: string, stored: string | undefined): boolean {
|
||||
if (!stored || !provided) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
// Convert strings to buffers for constant-time comparison
|
||||
const providedBuffer = Buffer.from(provided);
|
||||
const storedBuffer = Buffer.from(stored);
|
||||
|
||||
// If lengths are different, return false but use a dummy comparison to prevent timing attacks
|
||||
if (providedBuffer.length !== storedBuffer.length) {
|
||||
// Create a dummy buffer of the same length as the provided key
|
||||
const dummyBuffer = Buffer.alloc(providedBuffer.length);
|
||||
// Perform comparison with dummy buffer to maintain constant time
|
||||
crypto.timingSafeEqual(providedBuffer, dummyBuffer);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Perform constant-time comparison
|
||||
return crypto.timingSafeEqual(providedBuffer, storedBuffer);
|
||||
} catch (error) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
import * as basicAuth from 'express-basic-auth';
|
||||
|
||||
export function BasicAuthFunction(username, password, exclude: string[] = []) {
|
||||
function authFunction(req, res, next) {
|
||||
const ignore = exclude.filter((url) => req.url.startsWith(url)).length > 0;
|
||||
if (ignore) {
|
||||
next();
|
||||
return;
|
||||
}
|
||||
|
||||
const auth = basicAuth({
|
||||
challenge: true,
|
||||
users: {
|
||||
[username]: password,
|
||||
},
|
||||
});
|
||||
auth(req, res, next);
|
||||
}
|
||||
|
||||
return authFunction;
|
||||
}
|
||||
@@ -18,4 +18,20 @@ export class DashboardConfigServiceCore {
|
||||
const value = this.configService.get('WAHA_DASHBOARD_ENABLED', 'true');
|
||||
return parseBool(value);
|
||||
}
|
||||
|
||||
get credentials(): [string, string] | null {
|
||||
const user = this.configService.get('WAHA_DASHBOARD_USERNAME', '');
|
||||
const password = this.configService.get('WAHA_DASHBOARD_PASSWORD', '');
|
||||
if (!user && !password) {
|
||||
return null;
|
||||
}
|
||||
if ((user && !password) || (!user && password)) {
|
||||
this.logger.warn(
|
||||
'Set up both WAHA_DASHBOARD_USERNAME and WAHA_DASHBOARD_PASSWORD ' +
|
||||
'to enable dashboard authentication.',
|
||||
);
|
||||
return null;
|
||||
}
|
||||
return [user, password];
|
||||
}
|
||||
}
|
||||
@@ -1,16 +1,21 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { WhatsappConfigService } from '@waha/config.service';
|
||||
import { BootstrapConfig } from '@waha/core/engines/gows/GowsBootstrap';
|
||||
import { GowsConfig } from '@waha/core/engines/gows/session.gows.core';
|
||||
|
||||
@Injectable()
|
||||
export class GowsEngineConfigService {
|
||||
constructor(protected configService: ConfigService) {}
|
||||
constructor(
|
||||
protected configService: ConfigService,
|
||||
protected whatsappConfigService: WhatsappConfigService,
|
||||
) {}
|
||||
|
||||
getBootstrapConfig(): BootstrapConfig {
|
||||
return {
|
||||
path: this.configService.get('WAHA_GOWS_PATH'),
|
||||
socket: this.getSocket(),
|
||||
pprof: this.whatsappConfigService.debugModeEnabled,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -19,4 +19,43 @@ export class SwaggerConfigServiceCore {
|
||||
);
|
||||
return parseBool(value);
|
||||
}
|
||||
|
||||
get enabled(): boolean {
|
||||
const value = this.configService.get('WHATSAPP_SWAGGER_ENABLED', 'true');
|
||||
return parseBool(value);
|
||||
}
|
||||
|
||||
get credentials(): [string, string] | undefined {
|
||||
const user = this.configService.get<string>(
|
||||
'WHATSAPP_SWAGGER_USERNAME',
|
||||
undefined,
|
||||
);
|
||||
const password = this.configService.get<string>(
|
||||
'WHATSAPP_SWAGGER_PASSWORD',
|
||||
undefined,
|
||||
);
|
||||
if (!user && !password) {
|
||||
return null;
|
||||
}
|
||||
if ((user && !password) || (!user && password)) {
|
||||
this.logger.warn(
|
||||
'Set up both WHATSAPP_SWAGGER_USERNAME and WHATSAPP_SWAGGER_PASSWORD ' +
|
||||
'to enable swagger authentication.',
|
||||
);
|
||||
return null;
|
||||
}
|
||||
return [user, password];
|
||||
}
|
||||
|
||||
get title() {
|
||||
return this.configService.get('WHATSAPP_SWAGGER_TITLE', '');
|
||||
}
|
||||
|
||||
get description() {
|
||||
return this.configService.get('WHATSAPP_SWAGGER_DESCRIPTION', '');
|
||||
}
|
||||
|
||||
get externalDocUrl() {
|
||||
return this.configService.get('WHATSAPP_SWAGGER_EXTERNAL_DOC_URL', '');
|
||||
}
|
||||
}
|
||||
@@ -22,6 +22,7 @@ export async function isUnixSocket(socketPath: string): Promise<boolean> {
|
||||
export interface BootstrapConfig {
|
||||
path: string;
|
||||
socket: string;
|
||||
pprof?: boolean;
|
||||
}
|
||||
|
||||
export class GowsBootstrap implements EngineBootstrap {
|
||||
@@ -44,6 +45,7 @@ export class GowsBootstrap implements EngineBootstrap {
|
||||
this.logger,
|
||||
this.config.path,
|
||||
this.config.socket,
|
||||
this.config.pprof,
|
||||
);
|
||||
this.gows.start(() => {
|
||||
this.logger.info(`GOWS stopped, exiting...`);
|
||||
|
||||
@@ -14,13 +14,22 @@ export class GowsSubprocess {
|
||||
private logger: Logger,
|
||||
readonly path: string,
|
||||
readonly socket: string,
|
||||
readonly pprof: boolean = false,
|
||||
) {}
|
||||
|
||||
start(onExit: (code: number) => void) {
|
||||
this.logger.info('Starting GOWS subprocess...');
|
||||
this.logger.debug(`GOWS path '${this.path}', socket: '${this.socket}'...`);
|
||||
|
||||
this.child = spawn(this.path, ['--socket', this.socket], {
|
||||
const args = ['--socket', this.socket];
|
||||
if (this.pprof) {
|
||||
this.logger.info('Debug mode enabled, adding pprof flags');
|
||||
args.push('--pprof');
|
||||
args.push('--pprof-port=6060');
|
||||
args.push('--pprof-host=0.0.0.0');
|
||||
}
|
||||
|
||||
this.child = spawn(this.path, args, {
|
||||
detached: true,
|
||||
});
|
||||
this.logger.debug(`GOWS started with PID: ${this.child.pid}`);
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
export function isFromFullSync(event: any): boolean {
|
||||
return event.FromFullSync;
|
||||
}
|
||||
File diff suppressed because it is too large.
Load diff
@@ -15,6 +15,28 @@ function deserialize_messages_ButtonReplyRequest(buffer_arg) {
|
||||
return gows_pb.ButtonReplyRequest.deserializeBinary(new Uint8Array(buffer_arg));
|
||||
}
|
||||
|
||||
function serialize_messages_CancelEventMessageRequest(arg) {
|
||||
if (!(arg instanceof gows_pb.CancelEventMessageRequest)) {
|
||||
throw new Error('Expected argument of type messages.CancelEventMessageRequest');
|
||||
}
|
||||
return Buffer.from(arg.serializeBinary());
|
||||
}
|
||||
|
||||
function deserialize_messages_CancelEventMessageRequest(buffer_arg) {
|
||||
return gows_pb.CancelEventMessageRequest.deserializeBinary(new Uint8Array(buffer_arg));
|
||||
}
|
||||
|
||||
function serialize_messages_ChatLabelRequest(arg) {
|
||||
if (!(arg instanceof gows_pb.ChatLabelRequest)) {
|
||||
throw new Error('Expected argument of type messages.ChatLabelRequest');
|
||||
}
|
||||
return Buffer.from(arg.serializeBinary());
|
||||
}
|
||||
|
||||
function deserialize_messages_ChatLabelRequest(buffer_arg) {
|
||||
return gows_pb.ChatLabelRequest.deserializeBinary(new Uint8Array(buffer_arg));
|
||||
}
|
||||
|
||||
function serialize_messages_ChatPresenceRequest(arg) {
|
||||
if (!(arg instanceof gows_pb.ChatPresenceRequest)) {
|
||||
throw new Error('Expected argument of type messages.ChatPresenceRequest');
|
||||
@@ -70,6 +92,17 @@ function deserialize_messages_CreateNewsletterRequest(buffer_arg) {
|
||||
return gows_pb.CreateNewsletterRequest.deserializeBinary(new Uint8Array(buffer_arg));
|
||||
}
|
||||
|
||||
function serialize_messages_DeleteLabelRequest(arg) {
|
||||
if (!(arg instanceof gows_pb.DeleteLabelRequest)) {
|
||||
throw new Error('Expected argument of type messages.DeleteLabelRequest');
|
||||
}
|
||||
return Buffer.from(arg.serializeBinary());
|
||||
}
|
||||
|
||||
function deserialize_messages_DeleteLabelRequest(buffer_arg) {
|
||||
return gows_pb.DeleteLabelRequest.deserializeBinary(new Uint8Array(buffer_arg));
|
||||
}
|
||||
|
||||
function serialize_messages_DownloadMediaRequest(arg) {
|
||||
if (!(arg instanceof gows_pb.DownloadMediaRequest)) {
|
||||
throw new Error('Expected argument of type messages.DownloadMediaRequest');
|
||||
@@ -158,6 +191,28 @@ function deserialize_messages_GetContactsRequest(buffer_arg) {
|
||||
return gows_pb.GetContactsRequest.deserializeBinary(new Uint8Array(buffer_arg));
|
||||
}
|
||||
|
||||
function serialize_messages_GetLabelsRequest(arg) {
|
||||
if (!(arg instanceof gows_pb.GetLabelsRequest)) {
|
||||
throw new Error('Expected argument of type messages.GetLabelsRequest');
|
||||
}
|
||||
return Buffer.from(arg.serializeBinary());
|
||||
}
|
||||
|
||||
function deserialize_messages_GetLabelsRequest(buffer_arg) {
|
||||
return gows_pb.GetLabelsRequest.deserializeBinary(new Uint8Array(buffer_arg));
|
||||
}
|
||||
|
||||
function serialize_messages_GetLidsRequest(arg) {
|
||||
if (!(arg instanceof gows_pb.GetLidsRequest)) {
|
||||
throw new Error('Expected argument of type messages.GetLidsRequest');
|
||||
}
|
||||
return Buffer.from(arg.serializeBinary());
|
||||
}
|
||||
|
||||
function deserialize_messages_GetLidsRequest(buffer_arg) {
|
||||
return gows_pb.GetLidsRequest.deserializeBinary(new Uint8Array(buffer_arg));
|
||||
}
|
||||
|
||||
function serialize_messages_GetMessagesRequest(arg) {
|
||||
if (!(arg instanceof gows_pb.GetMessagesRequest)) {
|
||||
throw new Error('Expected argument of type messages.GetMessagesRequest');
|
||||
@@ -389,6 +444,17 @@ function deserialize_messages_OptionalString(buffer_arg) {
|
||||
return gows_pb.OptionalString.deserializeBinary(new Uint8Array(buffer_arg));
|
||||
}
|
||||
|
||||
function serialize_messages_OptionalUInt64(arg) {
|
||||
if (!(arg instanceof gows_pb.OptionalUInt64)) {
|
||||
throw new Error('Expected argument of type messages.OptionalUInt64');
|
||||
}
|
||||
return Buffer.from(arg.serializeBinary());
|
||||
}
|
||||
|
||||
function deserialize_messages_OptionalUInt64(buffer_arg) {
|
||||
return gows_pb.OptionalUInt64.deserializeBinary(new Uint8Array(buffer_arg));
|
||||
}
|
||||
|
||||
function serialize_messages_PairCodeRequest(arg) {
|
||||
if (!(arg instanceof gows_pb.PairCodeRequest)) {
|
||||
throw new Error('Expected argument of type messages.PairCodeRequest');
|
||||
@@ -576,6 +642,17 @@ function deserialize_messages_UpdateParticipantsRequest(buffer_arg) {
|
||||
return gows_pb.UpdateParticipantsRequest.deserializeBinary(new Uint8Array(buffer_arg));
|
||||
}
|
||||
|
||||
function serialize_messages_UpsertLabelRequest(arg) {
|
||||
if (!(arg instanceof gows_pb.UpsertLabelRequest)) {
|
||||
throw new Error('Expected argument of type messages.UpsertLabelRequest');
|
||||
}
|
||||
return Buffer.from(arg.serializeBinary());
|
||||
}
|
||||
|
||||
function deserialize_messages_UpsertLabelRequest(buffer_arg) {
|
||||
return gows_pb.UpsertLabelRequest.deserializeBinary(new Uint8Array(buffer_arg));
|
||||
}
|
||||
|
||||
|
||||
//
|
||||
// Events
|
||||
@@ -594,7 +671,7 @@ var EventStreamService = exports.EventStreamService = {
|
||||
},
|
||||
};
|
||||
|
||||
exports.EventStreamClient = grpc.makeGenericClientConstructor(EventStreamService);
|
||||
exports.EventStreamClient = grpc.makeGenericClientConstructor(EventStreamService, 'EventStream');
|
||||
var MessageServiceService = exports.MessageServiceService = {
|
||||
//
|
||||
// Session management
|
||||
@@ -691,6 +768,53 @@ setProfileName: {
|
||||
responseDeserialize: deserialize_messages_Empty,
|
||||
},
|
||||
//
|
||||
// Lids
|
||||
//
|
||||
getAllLids: {
|
||||
path: '/messages.MessageService/GetAllLids',
|
||||
requestStream: false,
|
||||
responseStream: false,
|
||||
requestType: gows_pb.GetLidsRequest,
|
||||
responseType: gows_pb.JsonList,
|
||||
requestSerialize: serialize_messages_GetLidsRequest,
|
||||
requestDeserialize: deserialize_messages_GetLidsRequest,
|
||||
responseSerialize: serialize_messages_JsonList,
|
||||
responseDeserialize: deserialize_messages_JsonList,
|
||||
},
|
||||
getLidsCount: {
|
||||
path: '/messages.MessageService/GetLidsCount',
|
||||
requestStream: false,
|
||||
responseStream: false,
|
||||
requestType: gows_pb.Session,
|
||||
responseType: gows_pb.OptionalUInt64,
|
||||
requestSerialize: serialize_messages_Session,
|
||||
requestDeserialize: deserialize_messages_Session,
|
||||
responseSerialize: serialize_messages_OptionalUInt64,
|
||||
responseDeserialize: deserialize_messages_OptionalUInt64,
|
||||
},
|
||||
findPNByLid: {
|
||||
path: '/messages.MessageService/FindPNByLid',
|
||||
requestStream: false,
|
||||
responseStream: false,
|
||||
requestType: gows_pb.EntityByIdRequest,
|
||||
responseType: gows_pb.OptionalString,
|
||||
requestSerialize: serialize_messages_EntityByIdRequest,
|
||||
requestDeserialize: deserialize_messages_EntityByIdRequest,
|
||||
responseSerialize: serialize_messages_OptionalString,
|
||||
responseDeserialize: deserialize_messages_OptionalString,
|
||||
},
|
||||
findLIDByPhoneNumber: {
|
||||
path: '/messages.MessageService/FindLIDByPhoneNumber',
|
||||
requestStream: false,
|
||||
responseStream: false,
|
||||
requestType: gows_pb.EntityByIdRequest,
|
||||
responseType: gows_pb.OptionalString,
|
||||
requestSerialize: serialize_messages_EntityByIdRequest,
|
||||
requestDeserialize: deserialize_messages_EntityByIdRequest,
|
||||
responseSerialize: serialize_messages_OptionalString,
|
||||
responseDeserialize: deserialize_messages_OptionalString,
|
||||
},
|
||||
//
|
||||
// Groups
|
||||
//
|
||||
fetchGroups: {
|
||||
@@ -1090,6 +1214,100 @@ getSubscribedNewsletters: {
|
||||
responseDeserialize: deserialize_messages_Empty,
|
||||
},
|
||||
//
|
||||
// Labels
|
||||
//
|
||||
getLabels: {
|
||||
path: '/messages.MessageService/GetLabels',
|
||||
requestStream: false,
|
||||
responseStream: false,
|
||||
requestType: gows_pb.GetLabelsRequest,
|
||||
responseType: gows_pb.JsonList,
|
||||
requestSerialize: serialize_messages_GetLabelsRequest,
|
||||
requestDeserialize: deserialize_messages_GetLabelsRequest,
|
||||
responseSerialize: serialize_messages_JsonList,
|
||||
responseDeserialize: deserialize_messages_JsonList,
|
||||
},
|
||||
upsertLabel: {
|
||||
path: '/messages.MessageService/UpsertLabel',
|
||||
requestStream: false,
|
||||
responseStream: false,
|
||||
requestType: gows_pb.UpsertLabelRequest,
|
||||
responseType: gows_pb.Empty,
|
||||
requestSerialize: serialize_messages_UpsertLabelRequest,
|
||||
requestDeserialize: deserialize_messages_UpsertLabelRequest,
|
||||
responseSerialize: serialize_messages_Empty,
|
||||
responseDeserialize: deserialize_messages_Empty,
|
||||
},
|
||||
deleteLabel: {
|
||||
path: '/messages.MessageService/DeleteLabel',
|
||||
requestStream: false,
|
||||
responseStream: false,
|
||||
requestType: gows_pb.DeleteLabelRequest,
|
||||
responseType: gows_pb.Empty,
|
||||
requestSerialize: serialize_messages_DeleteLabelRequest,
|
||||
requestDeserialize: deserialize_messages_DeleteLabelRequest,
|
||||
responseSerialize: serialize_messages_Empty,
|
||||
responseDeserialize: deserialize_messages_Empty,
|
||||
},
|
||||
addChatLabel: {
|
||||
path: '/messages.MessageService/AddChatLabel',
|
||||
requestStream: false,
|
||||
responseStream: false,
|
||||
requestType: gows_pb.ChatLabelRequest,
|
||||
responseType: gows_pb.Empty,
|
||||
requestSerialize: serialize_messages_ChatLabelRequest,
|
||||
requestDeserialize: deserialize_messages_ChatLabelRequest,
|
||||
responseSerialize: serialize_messages_Empty,
|
||||
responseDeserialize: deserialize_messages_Empty,
|
||||
},
|
||||
removeChatLabel: {
|
||||
path: '/messages.MessageService/RemoveChatLabel',
|
||||
requestStream: false,
|
||||
responseStream: false,
|
||||
requestType: gows_pb.ChatLabelRequest,
|
||||
responseType: gows_pb.Empty,
|
||||
requestSerialize: serialize_messages_ChatLabelRequest,
|
||||
requestDeserialize: deserialize_messages_ChatLabelRequest,
|
||||
responseSerialize: serialize_messages_Empty,
|
||||
responseDeserialize: deserialize_messages_Empty,
|
||||
},
|
||||
getLabelsByJid: {
|
||||
path: '/messages.MessageService/GetLabelsByJid',
|
||||
requestStream: false,
|
||||
responseStream: false,
|
||||
requestType: gows_pb.EntityByIdRequest,
|
||||
responseType: gows_pb.JsonList,
|
||||
requestSerialize: serialize_messages_EntityByIdRequest,
|
||||
requestDeserialize: deserialize_messages_EntityByIdRequest,
|
||||
responseSerialize: serialize_messages_JsonList,
|
||||
responseDeserialize: deserialize_messages_JsonList,
|
||||
},
|
||||
getChatsByLabelId: {
|
||||
path: '/messages.MessageService/GetChatsByLabelId',
|
||||
requestStream: false,
|
||||
responseStream: false,
|
||||
requestType: gows_pb.EntityByIdRequest,
|
||||
responseType: gows_pb.JsonList,
|
||||
requestSerialize: serialize_messages_EntityByIdRequest,
|
||||
requestDeserialize: deserialize_messages_EntityByIdRequest,
|
||||
responseSerialize: serialize_messages_JsonList,
|
||||
responseDeserialize: deserialize_messages_JsonList,
|
||||
},
|
||||
//
|
||||
// Events
|
||||
//
|
||||
cancelEventMessage: {
|
||||
path: '/messages.MessageService/CancelEventMessage',
|
||||
requestStream: false,
|
||||
responseStream: false,
|
||||
requestType: gows_pb.CancelEventMessageRequest,
|
||||
responseType: gows_pb.MessageResponse,
|
||||
requestSerialize: serialize_messages_CancelEventMessageRequest,
|
||||
requestDeserialize: deserialize_messages_CancelEventMessageRequest,
|
||||
responseSerialize: serialize_messages_MessageResponse,
|
||||
responseDeserialize: deserialize_messages_MessageResponse,
|
||||
},
|
||||
//
|
||||
// Media
|
||||
//
|
||||
downloadMedia: {
|
||||
@@ -1163,4 +1381,4 @@ getMessageById: {
|
||||
},
|
||||
};
|
||||
|
||||
exports.MessageServiceClient = grpc.makeGenericClientConstructor(MessageServiceService);
|
||||
exports.MessageServiceClient = grpc.makeGenericClientConstructor(MessageServiceService, 'MessageService');
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,45 @@
|
||||
import { toCusFormat } from '@waha/core/engines/noweb/session.noweb.core';
|
||||
import { Label, LabelChatAssociation } from '@waha/structures/labels.dto';
|
||||
|
||||
import * as gows from './types';
|
||||
|
||||
/**
|
||||
* Converts a GOWS LabelEdit event to a Label DTO
|
||||
*/
|
||||
export function eventToLabelDTO(labelEdit: gows.LabelEdit): Label {
|
||||
return {
|
||||
id: labelEdit.LabelID,
|
||||
name: labelEdit.Action.name,
|
||||
color: labelEdit.Action.color,
|
||||
colorHex: Label.toHex(labelEdit.Action.color),
|
||||
} as Label;
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a GOWS LabelAssociationChat event to a LabelChatAssociation DTO
|
||||
*/
|
||||
export function eventToLabelChatAssociationDTO(
|
||||
labelAssoc: gows.LabelAssociationChat,
|
||||
): LabelChatAssociation {
|
||||
return {
|
||||
labelId: labelAssoc.LabelID,
|
||||
label: null, // We don't have the label info here
|
||||
chatId: toCusFormat(labelAssoc.JID),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a LabelEdit event is for a label creation or update (not deletion)
|
||||
*/
|
||||
export function isLabelUpsertEvent(labelEdit: gows.LabelEdit): boolean {
|
||||
return !labelEdit.Action.deleted;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a LabelAssociationChat event is for adding a label to a chat
|
||||
*/
|
||||
export function isLabelChatAddedEvent(
|
||||
labelAssoc: gows.LabelAssociationChat,
|
||||
): boolean {
|
||||
return labelAssoc.Action.labeled === true;
|
||||
}
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
isJidGroup,
|
||||
jidNormalizedUser,
|
||||
normalizeMessageContent,
|
||||
WAMessageKey,
|
||||
} from '@adiwajshing/baileys';
|
||||
import { isJidBroadcast } from '@adiwajshing/baileys/lib/WABinary/jid-utils';
|
||||
import * as grpc from '@grpc/grpc-js';
|
||||
@@ -32,7 +33,10 @@ import {
|
||||
statusToAck,
|
||||
} from '@waha/core/engines/gows/helpers';
|
||||
import { GowsAuthFactoryCore } from '@waha/core/engines/gows/store/GowsAuthFactoryCore';
|
||||
import { toCusFormat } from '@waha/core/engines/noweb/session.noweb.core';
|
||||
import {
|
||||
getDestination,
|
||||
toCusFormat,
|
||||
} from '@waha/core/engines/noweb/session.noweb.core';
|
||||
import { extractMediaContent } from '@waha/core/engines/noweb/utils';
|
||||
import {
|
||||
AvailableInPlusVersion,
|
||||
@@ -60,6 +64,7 @@ import {
|
||||
GetChatMessageQuery,
|
||||
GetChatMessagesFilter,
|
||||
GetChatMessagesQuery,
|
||||
OverviewFilter,
|
||||
ReadChatMessagesQuery,
|
||||
ReadChatMessagesResponse,
|
||||
} from '@waha/structures/chats.dto';
|
||||
@@ -67,11 +72,13 @@ import {
|
||||
ChatRequest,
|
||||
CheckNumberStatusQuery,
|
||||
EditMessageRequest,
|
||||
MessageContactVcardRequest,
|
||||
MessageFileRequest,
|
||||
MessageForwardRequest,
|
||||
MessageImageRequest,
|
||||
MessageLinkCustomPreviewRequest,
|
||||
MessageLocationRequest,
|
||||
MessagePollRequest,
|
||||
MessageReactionRequest,
|
||||
MessageReplyRequest,
|
||||
MessageTextRequest,
|
||||
@@ -87,6 +94,12 @@ import {
|
||||
WAHASessionStatus,
|
||||
WAMessageAck,
|
||||
} from '@waha/structures/enums.dto';
|
||||
import {
|
||||
EventCancelRequest,
|
||||
EventMessageRequest,
|
||||
EventResponse,
|
||||
EventResponsePayload,
|
||||
} from '@waha/structures/events.dto';
|
||||
import { BinaryFile, RemoteFile } from '@waha/structures/files.dto';
|
||||
import {
|
||||
CreateGroupRequest,
|
||||
@@ -112,7 +125,13 @@ import {
|
||||
DeleteStatusRequest,
|
||||
TextStatus,
|
||||
} from '@waha/structures/status.dto';
|
||||
import { EnginePayload, WAMessageAckBody } from '@waha/structures/webhooks.dto';
|
||||
import {
|
||||
EnginePayload,
|
||||
PollVotePayload,
|
||||
WAMessageAckBody,
|
||||
WAMessageEditedBody,
|
||||
WAMessageRevokedBody,
|
||||
} from '@waha/structures/webhooks.dto';
|
||||
import { PaginatorInMemory } from '@waha/utils/Paginator';
|
||||
import { sleep, waitUntil } from '@waha/utils/promiseTimeout';
|
||||
import { onlyEvent } from '@waha/utils/reactive/ops/onlyEvent';
|
||||
@@ -132,8 +151,22 @@ import { promisify } from 'util';
|
||||
import * as gows from './types';
|
||||
import { MessageStatus } from './types';
|
||||
import MessageServiceClient = messages.MessageServiceClient;
|
||||
import { isFromFullSync } from '@waha/core/engines/gows/appstate';
|
||||
import { toVcard } from '@waha/core/helpers';
|
||||
import { AckToStatus } from '@waha/core/utils/acks';
|
||||
import { ParseEventResponseType } from '@waha/core/utils/events';
|
||||
import { DistinctAck } from '@waha/core/utils/reactive';
|
||||
import { Label, LabelDTO, LabelID } from '@waha/structures/labels.dto';
|
||||
import { LidToPhoneNumber } from '@waha/structures/lids.dto';
|
||||
import { exclude } from '@waha/utils/reactive/ops/exclude';
|
||||
import * as lodash from 'lodash';
|
||||
|
||||
import {
|
||||
eventToLabelChatAssociationDTO,
|
||||
eventToLabelDTO,
|
||||
isLabelChatAddedEvent,
|
||||
isLabelUpsertEvent,
|
||||
} from './labels.gows';
|
||||
|
||||
enum WhatsMeowEvent {
|
||||
CONNECTED = 'gows.ConnectedEventData',
|
||||
@@ -150,6 +183,13 @@ enum WhatsMeowEvent {
|
||||
// Groups
|
||||
GROUP_INFO = 'events.GroupInfo',
|
||||
JOINED_GROUP = 'events.JoinedGroup',
|
||||
// Labels
|
||||
LABEL_EDIT = 'events.LabelEdit',
|
||||
LABEL_ASSOCIATION_CHAT = 'events.LabelAssociationChat',
|
||||
// Events
|
||||
EVENT_MESSAGE_RESPONSE = 'gows.EventMessageResponse',
|
||||
// Polls
|
||||
POLL_VOTE_EVENT = 'gows.PollVoteEvent',
|
||||
}
|
||||
|
||||
const gRPCClientConfig = {
|
||||
@@ -369,6 +409,53 @@ export class WhatsappSessionGoWSCore extends WhatsappSession {
|
||||
this.events2.get(WAHAEvents.MESSAGE).switch(messagesFromOthers$);
|
||||
this.events2.get(WAHAEvents.MESSAGE_ANY).switch(messagesFromAll$);
|
||||
|
||||
// Handle revoked messages
|
||||
const messagesRevoked$ = messages$.pipe(
|
||||
filter((msg) => {
|
||||
return (
|
||||
msg?.Message?.protocolMessage?.type === 0 &&
|
||||
msg?.Message?.protocolMessage?.key !== undefined
|
||||
);
|
||||
}),
|
||||
mergeMap(async (message): Promise<WAMessageRevokedBody> => {
|
||||
const afterMessage = await this.toWAMessage(message);
|
||||
// Extract the revoked message ID from protocolMessage.key
|
||||
const revokedMessageId = message.Message.protocolMessage.key?.ID;
|
||||
return {
|
||||
after: afterMessage,
|
||||
before: null,
|
||||
revokedMessageId: revokedMessageId,
|
||||
_data: message,
|
||||
};
|
||||
}),
|
||||
);
|
||||
this.events2.get(WAHAEvents.MESSAGE_REVOKED).switch(messagesRevoked$);
|
||||
|
||||
// Handle edited messages
|
||||
const messagesEdited$ = messages$.pipe(
|
||||
filter((msg) => {
|
||||
return (
|
||||
msg?.Message?.protocolMessage?.type === 14 &&
|
||||
msg?.Message?.protocolMessage?.editedMessage !== undefined
|
||||
);
|
||||
}),
|
||||
mergeMap(async (message): Promise<WAMessageEditedBody> => {
|
||||
const waMessage = await this.toWAMessage(message);
|
||||
// Extract the body from editedMessage using extractBody function
|
||||
const body =
|
||||
this.extractBody(message.Message.protocolMessage.editedMessage) || '';
|
||||
// Extract the original message ID from protocolMessage.key
|
||||
const editedMessageId = message.Message.protocolMessage.key?.ID;
|
||||
return {
|
||||
...waMessage,
|
||||
body: body,
|
||||
editedMessageId: editedMessageId,
|
||||
_data: message,
|
||||
};
|
||||
}),
|
||||
);
|
||||
this.events2.get(WAHAEvents.MESSAGE_EDITED).switch(messagesEdited$);
|
||||
|
||||
const receipt$ = all$.pipe(onlyEvent(WhatsMeowEvent.RECEIPT));
|
||||
const messageAck$ = receipt$.pipe(
|
||||
mergeMap(this.receiptToMessageAck.bind(this)),
|
||||
@@ -419,6 +506,92 @@ export class WhatsappSessionGoWSCore extends WhatsappSession {
|
||||
filter(Boolean),
|
||||
);
|
||||
this.events2.get(WAHAEvents.GROUP_V2_UPDATE).switch(groupV2Update$);
|
||||
|
||||
// Label Events
|
||||
// First, create streams for the raw label events
|
||||
const labelEditEvents$ = all$.pipe(
|
||||
onlyEvent(WhatsMeowEvent.LABEL_EDIT),
|
||||
exclude(isFromFullSync),
|
||||
);
|
||||
|
||||
// Split the raw label edit events into upsert and deleted events
|
||||
const [labelUpsertEvents$, labelDeletedEvents$] = partition(
|
||||
labelEditEvents$,
|
||||
isLabelUpsertEvent,
|
||||
);
|
||||
|
||||
// Convert the events to DTOs
|
||||
const labelUpsert$ = labelUpsertEvents$.pipe(map(eventToLabelDTO));
|
||||
|
||||
const labelDeleted$ = labelDeletedEvents$.pipe(map(eventToLabelDTO));
|
||||
|
||||
this.events2.get(WAHAEvents.LABEL_UPSERT).switch(labelUpsert$);
|
||||
this.events2.get(WAHAEvents.LABEL_DELETED).switch(labelDeleted$);
|
||||
|
||||
//
|
||||
// Polls
|
||||
//
|
||||
const pollVoteEvent$ = all$.pipe(
|
||||
onlyEvent(WhatsMeowEvent.POLL_VOTE_EVENT),
|
||||
map(this.toPollVotePayload.bind(this)),
|
||||
filter(Boolean),
|
||||
);
|
||||
|
||||
// Split into successful and failed responses
|
||||
const [pollVoteSuccess$, pollVoteFailed$] = partition(
|
||||
pollVoteEvent$,
|
||||
(payload: PollVotePayload) => !!payload.vote.selectedOptions,
|
||||
);
|
||||
|
||||
this.events2.get(WAHAEvents.POLL_VOTE).switch(pollVoteSuccess$);
|
||||
this.events2.get(WAHAEvents.POLL_VOTE_FAILED).switch(pollVoteFailed$);
|
||||
|
||||
//
|
||||
// Event Message
|
||||
//
|
||||
const eventMessageResponse$ = all$.pipe(
|
||||
onlyEvent(WhatsMeowEvent.EVENT_MESSAGE_RESPONSE),
|
||||
map(this.toEventResponsePayload.bind(this)),
|
||||
filter(Boolean),
|
||||
);
|
||||
|
||||
// Split into successful and failed responses
|
||||
const [eventResponseSuccess$, eventResponseFailed$] = partition(
|
||||
eventMessageResponse$,
|
||||
(payload: EventResponsePayload) => !!payload.eventResponse,
|
||||
);
|
||||
|
||||
this.events2.get(WAHAEvents.EVENT_RESPONSE).switch(eventResponseSuccess$);
|
||||
this.events2
|
||||
.get(WAHAEvents.EVENT_RESPONSE_FAILED)
|
||||
.switch(eventResponseFailed$);
|
||||
|
||||
//
|
||||
// Labels
|
||||
//
|
||||
// Handle label association events
|
||||
const labelAssociationEvents$ = all$.pipe(
|
||||
onlyEvent(WhatsMeowEvent.LABEL_ASSOCIATION_CHAT),
|
||||
exclude(isFromFullSync),
|
||||
);
|
||||
|
||||
// Split the raw label association events into added and deleted events
|
||||
const [labelChatAddedEvents$, labelChatDeletedEvents$] = partition(
|
||||
labelAssociationEvents$,
|
||||
isLabelChatAddedEvent,
|
||||
);
|
||||
|
||||
// Convert the events to DTOs
|
||||
const labelChatAdded$ = labelChatAddedEvents$.pipe(
|
||||
map(eventToLabelChatAssociationDTO),
|
||||
);
|
||||
|
||||
const labelChatDeleted$ = labelChatDeletedEvents$.pipe(
|
||||
map(eventToLabelChatAssociationDTO),
|
||||
);
|
||||
|
||||
this.events2.get(WAHAEvents.LABEL_CHAT_ADDED).switch(labelChatAdded$);
|
||||
this.events2.get(WAHAEvents.LABEL_CHAT_DELETED).switch(labelChatDeleted$);
|
||||
}
|
||||
|
||||
async fetchContactProfilePicture(id: string): Promise<string> {
|
||||
@@ -587,6 +760,37 @@ export class WhatsappSessionGoWSCore extends WhatsappSession {
|
||||
return this.messageResponse(jid, data);
|
||||
}
|
||||
|
||||
async sendContactVCard(request: MessageContactVcardRequest) {
|
||||
const jid = toJID(this.ensureSuffix(request.chatId));
|
||||
const contacts = request.contacts.map((el) => ({ vcard: toVcard(el) }));
|
||||
const message = new messages.MessageRequest({
|
||||
jid: jid,
|
||||
session: this.session,
|
||||
replyTo: getMessageIdFromSerialized(request.reply_to),
|
||||
contacts: contacts.map((contact) => new messages.vCardContact(contact)),
|
||||
});
|
||||
const response = await promisify(this.client.SendMessage)(message);
|
||||
const data = response.toObject();
|
||||
return this.messageResponse(jid, data);
|
||||
}
|
||||
|
||||
async sendPoll(request: MessagePollRequest) {
|
||||
const jid = toJID(request.chatId);
|
||||
const message = new messages.MessageRequest({
|
||||
jid: jid,
|
||||
session: this.session,
|
||||
replyTo: getMessageIdFromSerialized(request.reply_to),
|
||||
poll: new messages.PollMessage({
|
||||
name: request.poll.name,
|
||||
options: request.poll.options,
|
||||
multipleAnswers: request.poll.multipleAnswers,
|
||||
}),
|
||||
});
|
||||
const response = await promisify(this.client.SendMessage)(message);
|
||||
const data = response.toObject();
|
||||
return this.messageResponse(jid, data);
|
||||
}
|
||||
|
||||
public async deleteMessage(chatId: string, messageId: string) {
|
||||
const jid = toJID(this.ensureSuffix(chatId));
|
||||
const key = parseMessageIdSerialized(messageId);
|
||||
@@ -946,6 +1150,59 @@ export class WhatsappSessionGoWSCore extends WhatsappSession {
|
||||
return this.messageResponse(key.remoteJid, data);
|
||||
}
|
||||
|
||||
async sendEvent(request: EventMessageRequest): Promise<WAMessage> {
|
||||
const jid = toJID(this.ensureSuffix(request.chatId));
|
||||
const event = request.event;
|
||||
|
||||
// Create EventLocation if provided
|
||||
let location = null;
|
||||
if (event.location) {
|
||||
location = new messages.EventLocation({
|
||||
name: event.location.name,
|
||||
// Doesn't work right now
|
||||
degreesLatitude: 0,
|
||||
degreesLongitude: 0,
|
||||
});
|
||||
}
|
||||
|
||||
// Create event payload
|
||||
const eventMessage = new messages.EventMessage({
|
||||
name: event.name,
|
||||
description: event.description,
|
||||
startTime: event.startTime,
|
||||
endTime: event.endTime,
|
||||
location: location,
|
||||
extraGuestsAllowed: event.extraGuestsAllowed,
|
||||
});
|
||||
|
||||
// Create message
|
||||
const message = new messages.MessageRequest({
|
||||
jid: jid,
|
||||
session: this.session,
|
||||
event: eventMessage,
|
||||
replyTo: getMessageIdFromSerialized(request.reply_to),
|
||||
});
|
||||
|
||||
const response = await promisify(this.client.SendMessage)(message);
|
||||
const data = response.toObject();
|
||||
return this.messageResponse(jid, data) as any;
|
||||
}
|
||||
|
||||
async cancelEvent(eventId: string): Promise<WAMessage> {
|
||||
throw new Error('Method not implemented.');
|
||||
|
||||
const key = parseMessageIdSerialized(eventId, false);
|
||||
const jid = key.remoteJid;
|
||||
const request = new messages.CancelEventMessageRequest({
|
||||
session: this.session,
|
||||
jid: jid,
|
||||
messageId: key.id,
|
||||
});
|
||||
const response = await promisify(this.client.CancelEventMessage)(request);
|
||||
const data = response.toObject();
|
||||
return this.messageResponse(jid, data) as any;
|
||||
}
|
||||
|
||||
public async setPresence(presence: WAHAPresenceStatus, chatId?: string) {
|
||||
let request: any;
|
||||
let method: any;
|
||||
@@ -1006,13 +1263,13 @@ export class WhatsappSessionGoWSCore extends WhatsappSession {
|
||||
}
|
||||
|
||||
public async getPresence(chatId: string): Promise<WAHAChatPresences> {
|
||||
const remoteJid = toJID(chatId);
|
||||
if (!(remoteJid in this.presences.keys())) {
|
||||
await this.subscribePresence(remoteJid);
|
||||
const jid = toJID(chatId);
|
||||
await this.subscribePresence(jid);
|
||||
if (!(jid in this.presences.keys())) {
|
||||
await sleep(1000);
|
||||
}
|
||||
const result = this.presences.get(remoteJid) || [];
|
||||
return this.toWahaPresences(remoteJid, result);
|
||||
const result = this.presences.get(jid) || [];
|
||||
return this.toWahaPresences(jid, result);
|
||||
}
|
||||
|
||||
async subscribePresence(chatId: string) {
|
||||
@@ -1246,11 +1503,68 @@ export class WhatsappSessionGoWSCore extends WhatsappSession {
|
||||
return data.map(this.toWAContact.bind(this));
|
||||
}
|
||||
|
||||
/**
|
||||
* Lid to Phone Number methods
|
||||
*/
|
||||
public async getAllLids(
|
||||
pagination: PaginationParams,
|
||||
): Promise<Array<LidToPhoneNumber>> {
|
||||
const request = new messages.GetLidsRequest({
|
||||
session: this.session,
|
||||
});
|
||||
const response = await promisify(this.client.GetAllLids)(request);
|
||||
const data = parseJsonList(response);
|
||||
|
||||
const lids = data.map((item) => ({
|
||||
lid: item.lid,
|
||||
pn: toCusFormat(item.pn),
|
||||
}));
|
||||
|
||||
// Use in-memory pagination
|
||||
const paginator = new PaginatorInMemory(pagination);
|
||||
return paginator.apply(lids);
|
||||
}
|
||||
|
||||
public async getLidsCount(): Promise<number> {
|
||||
const response = await promisify(this.client.GetLidsCount)(this.session);
|
||||
return response?.value;
|
||||
}
|
||||
|
||||
public async findPNByLid(lid: string): Promise<LidToPhoneNumber> {
|
||||
const request = new messages.EntityByIdRequest({
|
||||
session: this.session,
|
||||
id: lid,
|
||||
});
|
||||
const response = await promisify(this.client.FindPNByLid)(request);
|
||||
const phoneNumber = response?.value;
|
||||
return {
|
||||
lid: lid,
|
||||
pn: phoneNumber ? toCusFormat(phoneNumber) : null,
|
||||
};
|
||||
}
|
||||
|
||||
public async findLIDByPhoneNumber(
|
||||
phoneNumber: string,
|
||||
): Promise<LidToPhoneNumber> {
|
||||
const pn = toJID(phoneNumber);
|
||||
const request = new messages.EntityByIdRequest({
|
||||
session: this.session,
|
||||
id: pn,
|
||||
});
|
||||
const response = await promisify(this.client.FindLIDByPhoneNumber)(request);
|
||||
const lid = response.value;
|
||||
return {
|
||||
lid: lid || null,
|
||||
pn: toCusFormat(pn),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Chats methods
|
||||
*/
|
||||
public async getChatsOverview(
|
||||
pagination: PaginationParams,
|
||||
filter?: OverviewFilter,
|
||||
): Promise<ChatSummary[]> {
|
||||
if (!pagination.sortBy) {
|
||||
pagination.sortBy = 'timestamp';
|
||||
@@ -1258,7 +1572,7 @@ export class WhatsappSessionGoWSCore extends WhatsappSession {
|
||||
if (!pagination.sortOrder) {
|
||||
pagination.sortOrder = SortOrder.DESC;
|
||||
}
|
||||
const chats = await this.getChats(pagination);
|
||||
const chats = await this.getChats(pagination, filter);
|
||||
|
||||
const promises = [];
|
||||
for (const chat of chats) {
|
||||
@@ -1297,10 +1611,17 @@ export class WhatsappSessionGoWSCore extends WhatsappSession {
|
||||
};
|
||||
}
|
||||
|
||||
public async getChats(pagination: PaginationParams) {
|
||||
public async getChats(
|
||||
pagination: PaginationParams,
|
||||
filter: OverviewFilter | null = null,
|
||||
) {
|
||||
if (pagination.sortBy === ChatSortField.CONVERSATION_TIMESTAMP) {
|
||||
pagination.sortBy = 'timestamp';
|
||||
}
|
||||
let jids = [];
|
||||
if (filter?.ids && filter.ids.length > 0) {
|
||||
jids = filter.ids.map((id) => toJID(id));
|
||||
}
|
||||
const request = new messages.GetChatsRequest({
|
||||
session: this.session,
|
||||
pagination: new messages.Pagination({
|
||||
@@ -1314,6 +1635,9 @@ export class WhatsappSessionGoWSCore extends WhatsappSession {
|
||||
? messages.SortBy.Order.DESC
|
||||
: messages.SortBy.Order.ASC,
|
||||
}),
|
||||
filter: new messages.ChatFilter({
|
||||
jids: jids,
|
||||
}),
|
||||
});
|
||||
const response = await promisify(this.client.GetChats)(request);
|
||||
const data = parseJsonList(response);
|
||||
@@ -1390,6 +1714,119 @@ export class WhatsappSessionGoWSCore extends WhatsappSession {
|
||||
return this.processIncomingMessage(msg, query.downloadMedia);
|
||||
}
|
||||
|
||||
/**
|
||||
* Labels methods
|
||||
*/
|
||||
|
||||
public async getLabels(): Promise<Label[]> {
|
||||
const request = new messages.GetLabelsRequest({
|
||||
session: this.session,
|
||||
});
|
||||
const response = await promisify(this.client.GetLabels)(request);
|
||||
const labels = parseJsonList(response);
|
||||
return labels.map(this.toLabel);
|
||||
}
|
||||
|
||||
public async createLabel(labelDto: LabelDTO): Promise<Label> {
|
||||
const labels = await this.getLabels();
|
||||
const highestLabelId = lodash.max(
|
||||
labels.map((label) => parseInt(label.id)),
|
||||
);
|
||||
const labelId = highestLabelId ? highestLabelId + 1 : 1;
|
||||
const label: Label = {
|
||||
id: labelId.toString(),
|
||||
name: labelDto.name,
|
||||
color: labelDto.color,
|
||||
colorHex: Label.toHex(labelDto.color),
|
||||
};
|
||||
return this.updateLabel(label);
|
||||
}
|
||||
|
||||
protected toLabel(label: any): Label {
|
||||
const color = label.color;
|
||||
return {
|
||||
id: label.id,
|
||||
name: label.name,
|
||||
color: color,
|
||||
colorHex: Label.toHex(color),
|
||||
};
|
||||
}
|
||||
|
||||
public async updateLabel(label: Label): Promise<Label> {
|
||||
const request = new messages.UpsertLabelRequest({
|
||||
session: this.session,
|
||||
label: new messages.Label({
|
||||
id: label.id,
|
||||
name: label.name,
|
||||
color: label.color,
|
||||
}),
|
||||
});
|
||||
await promisify(this.client.UpsertLabel)(request);
|
||||
return label;
|
||||
}
|
||||
|
||||
public async deleteLabel(label: Label): Promise<void> {
|
||||
const request = new messages.DeleteLabelRequest({
|
||||
session: this.session,
|
||||
label: new messages.Label({
|
||||
id: label.id,
|
||||
name: label.name,
|
||||
color: label.color,
|
||||
}),
|
||||
});
|
||||
await promisify(this.client.DeleteLabel)(request);
|
||||
}
|
||||
|
||||
public async getChatsByLabelId(labelId: string) {
|
||||
const request = new messages.EntityByIdRequest({
|
||||
session: this.session,
|
||||
id: labelId,
|
||||
});
|
||||
const response = await promisify(this.client.GetChatsByLabelId)(request);
|
||||
const ids = parseJsonList(response);
|
||||
return ids.map((jid) => {
|
||||
return {
|
||||
id: toCusFormat(jid),
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
public async getChatLabels(chatId: string): Promise<Label[]> {
|
||||
const jid = toJID(chatId);
|
||||
const request = new messages.EntityByIdRequest({
|
||||
session: this.session,
|
||||
id: jid,
|
||||
});
|
||||
const response = await promisify(this.client.GetLabelsByJid)(request);
|
||||
const labels = parseJsonList(response);
|
||||
return labels.map(this.toLabel);
|
||||
}
|
||||
|
||||
public async putLabelsToChat(chatId: string, labels: LabelID[]) {
|
||||
const jid = toJID(chatId);
|
||||
const labelsIds = labels.map((label) => label.id);
|
||||
const currentLabels = await this.getChatLabels(jid);
|
||||
const currentLabelsIds = currentLabels.map((label) => label.id);
|
||||
const addLabelsIds = lodash.difference(labelsIds, currentLabelsIds);
|
||||
const removeLabelsIds = lodash.difference(currentLabelsIds, labelsIds);
|
||||
for (const labelId of addLabelsIds) {
|
||||
const request = new messages.ChatLabelRequest({
|
||||
session: this.session,
|
||||
labelId: labelId,
|
||||
chatId: jid,
|
||||
});
|
||||
await promisify(this.client.AddChatLabel)(request);
|
||||
}
|
||||
for (const labelId of removeLabelsIds) {
|
||||
const request = new messages.ChatLabelRequest({
|
||||
session: this.session,
|
||||
labelId: labelId,
|
||||
chatId: jid,
|
||||
});
|
||||
await promisify(this.client.RemoveChatLabel)(request);
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// END - Methods for API
|
||||
//
|
||||
@@ -1398,10 +1835,12 @@ export class WhatsappSessionGoWSCore extends WhatsappSession {
|
||||
// if there is no text or media message
|
||||
if (!message) return;
|
||||
if (!message.Message) return;
|
||||
// Ignore reactions, we have dedicated handler for that
|
||||
// Ignore reactions, we have a dedicated handler for that
|
||||
if (message.Message.reactionMessage) return;
|
||||
// Ignore poll votes, we have dedicated handler for that
|
||||
// Ignore poll votes, we have a dedicated handler for that
|
||||
if (message.Message.pollUpdateMessage) return;
|
||||
// Ignore event response, we have a dedicated handler for that
|
||||
if (message.Message.encEventResponseMessage) return;
|
||||
// Ignore protocol messages
|
||||
if (message.Message.protocolMessage) return;
|
||||
|
||||
@@ -1464,6 +1903,63 @@ export class WhatsappSessionGoWSCore extends WhatsappSession {
|
||||
};
|
||||
}
|
||||
|
||||
private toPollVotePayload(event: any): PollVotePayload {
|
||||
// Extract event creation message key from the message
|
||||
const creationKey = event.Message?.pollUpdateMessage.pollCreationMessageKey;
|
||||
const key: WAMessageKey = {
|
||||
remoteJid: creationKey.remoteJID,
|
||||
fromMe: creationKey.fromMe,
|
||||
id: creationKey.ID,
|
||||
participant: creationKey.participant,
|
||||
};
|
||||
const fromToParticipant = getFromToParticipant(event);
|
||||
const pollCreationKey = getDestination(key);
|
||||
return {
|
||||
poll: pollCreationKey,
|
||||
vote: {
|
||||
id: buildMessageId(event),
|
||||
from: toCusFormat(fromToParticipant.from),
|
||||
fromMe: event.Info.IsFromMe,
|
||||
to: toCusFormat(fromToParticipant.to),
|
||||
participant: toCusFormat(fromToParticipant.participant),
|
||||
selectedOptions: event.Votes,
|
||||
timestamp: event.Message.pollUpdateMessage.senderTimestampMS,
|
||||
},
|
||||
_data: event,
|
||||
};
|
||||
}
|
||||
|
||||
private toEventResponsePayload(event: any): EventResponsePayload {
|
||||
const msg = this.toWAMessage(event);
|
||||
let response: EventResponse | null = null;
|
||||
if (event.EventResponse) {
|
||||
response = {
|
||||
response: ParseEventResponseType(event.EventResponse.response),
|
||||
timestampMs: event.EventResponse.timestampMS,
|
||||
extraGuestCount: event.EventResponse.extraGuestCount || 0,
|
||||
};
|
||||
}
|
||||
|
||||
// Extract event creation message key from the message
|
||||
const message = event.Message || event.message;
|
||||
const eventCreationMessageKey =
|
||||
message?.encEventResponseMessage?.eventCreationMessageKey;
|
||||
const key: WAMessageKey = {
|
||||
remoteJid: eventCreationMessageKey.remoteJID,
|
||||
fromMe: eventCreationMessageKey.fromMe,
|
||||
id: eventCreationMessageKey.ID,
|
||||
participant: eventCreationMessageKey.participant,
|
||||
};
|
||||
const eventCreationKey = getDestination(key);
|
||||
|
||||
return {
|
||||
...msg,
|
||||
eventCreationKey: eventCreationKey,
|
||||
eventResponse: response,
|
||||
_data: event,
|
||||
};
|
||||
}
|
||||
|
||||
private getSourceDeviceByMsg(message): MessageSource {
|
||||
if (!message.Info.IsFromMe) {
|
||||
return MessageSource.APP;
|
||||
|
||||
@@ -39,3 +39,31 @@ export enum MessageStatus {
|
||||
Read = 4,
|
||||
Played = 5,
|
||||
}
|
||||
|
||||
export interface LabelEditAction {
|
||||
name: string;
|
||||
color: number;
|
||||
deleted: boolean;
|
||||
orderIndex: number;
|
||||
isActive: boolean;
|
||||
type: number;
|
||||
}
|
||||
|
||||
export interface LabelEdit {
|
||||
Timestamp: string;
|
||||
LabelID: string;
|
||||
Action: LabelEditAction;
|
||||
FromFullSync: boolean;
|
||||
}
|
||||
|
||||
export interface LabelAssociationChatAction {
|
||||
labeled: boolean;
|
||||
}
|
||||
|
||||
export interface LabelAssociationChat {
|
||||
JID: string;
|
||||
Timestamp: string;
|
||||
LabelID: string;
|
||||
Action: LabelAssociationChatAction;
|
||||
FromFullSync: boolean;
|
||||
}
|
||||
@@ -90,6 +90,7 @@ import {
|
||||
GetChatMessageQuery,
|
||||
GetChatMessagesFilter,
|
||||
GetChatMessagesQuery,
|
||||
OverviewFilter,
|
||||
PinDuration,
|
||||
ReadChatMessagesQuery,
|
||||
ReadChatMessagesResponse,
|
||||
@@ -138,6 +139,7 @@ import {
|
||||
LabelDTO,
|
||||
LabelID,
|
||||
} from '@waha/structures/labels.dto';
|
||||
import { LidToPhoneNumber } from '@waha/structures/lids.dto';
|
||||
import { ReplyToMessage } from '@waha/structures/message.dto';
|
||||
import { PaginationParams } from '@waha/structures/pagination.dto';
|
||||
import {
|
||||
@@ -157,6 +159,7 @@ import {
|
||||
PollVote,
|
||||
PollVotePayload,
|
||||
WAMessageAckBody,
|
||||
WAMessageEditedBody,
|
||||
WAMessageRevokedBody,
|
||||
} from '@waha/structures/webhooks.dto';
|
||||
import { LoggerBuilder } from '@waha/utils/logging';
|
||||
@@ -385,7 +388,7 @@ export class WhatsappSessionNoWebCore extends WhatsappSession {
|
||||
|
||||
resubscribeToKnownPresences() {
|
||||
for (const jid in this.store.presences) {
|
||||
this.sock.presenceSubscribe(jid);
|
||||
this.subscribePresence(jid);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -467,7 +470,10 @@ export class WhatsappSessionNoWebCore extends WhatsappSession {
|
||||
} else if (connection === 'open') {
|
||||
this.qr.save('');
|
||||
this.status = WAHASessionStatus.WORKING;
|
||||
this.resubscribeToKnownPresences();
|
||||
// Do we need to resubscribe?
|
||||
// Ideally not, we need to explicitly call interesting
|
||||
// jids every 1 minute
|
||||
// this.resubscribeToKnownPresences();
|
||||
return;
|
||||
} else if (connection === 'close') {
|
||||
this.qr.save('');
|
||||
@@ -590,7 +596,8 @@ export class WhatsappSessionNoWebCore extends WhatsappSession {
|
||||
if (
|
||||
protocolMsg !== null &&
|
||||
protocolMsg !== undefined &&
|
||||
protocolMsg.editedMessage
|
||||
protocolMsg.editedMessage &&
|
||||
protocolMsg.key
|
||||
) {
|
||||
this.sock?.ev.emit('messages.update', [
|
||||
{
|
||||
@@ -615,7 +622,7 @@ export class WhatsappSessionNoWebCore extends WhatsappSession {
|
||||
}
|
||||
for (const message of messages) {
|
||||
const content = normalizeMessageContent(message.message);
|
||||
if (!content.pollUpdateMessage) {
|
||||
if (!content?.pollUpdateMessage) {
|
||||
continue;
|
||||
}
|
||||
const creationMsgKey = content.pollUpdateMessage.pollCreationMessageKey;
|
||||
@@ -1126,8 +1133,17 @@ export class WhatsappSessionNoWebCore extends WhatsappSession {
|
||||
|
||||
public async getChatsOverview(
|
||||
pagination: PaginationParams,
|
||||
filter?: OverviewFilter,
|
||||
): Promise<ChatSummary[]> {
|
||||
const chats = await this.store.getChats(pagination, false);
|
||||
// Convert customer format IDs to JID format if filter is provided
|
||||
let jidFilter;
|
||||
if (filter?.ids && filter.ids.length > 0) {
|
||||
jidFilter = {
|
||||
ids: filter.ids.map((id) => toJID(id)),
|
||||
};
|
||||
}
|
||||
|
||||
const chats = await this.store.getChats(pagination, false, jidFilter);
|
||||
// Remove unreadCount, it's not ready yet
|
||||
chats.forEach((chat) => delete chat.unreadCount);
|
||||
|
||||
@@ -1339,6 +1355,44 @@ export class WhatsappSessionNoWebCore extends WhatsappSession {
|
||||
throw new NotImplementedByEngineError();
|
||||
}
|
||||
|
||||
/**
|
||||
* Lid to Phone Number methods
|
||||
*/
|
||||
public async getAllLids(
|
||||
pagination: PaginationParams,
|
||||
): Promise<Array<LidToPhoneNumber>> {
|
||||
const lids = await this.store.getAllLids(pagination);
|
||||
return lids.map((value) => {
|
||||
return {
|
||||
lid: value.lid,
|
||||
pn: toCusFormat(value.pn),
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
public async getLidsCount(): Promise<number> {
|
||||
return this.store.getLidsCount();
|
||||
}
|
||||
|
||||
public async findPNByLid(lid: string): Promise<LidToPhoneNumber> {
|
||||
const pn = await this.store.findPNByLid(lid);
|
||||
return {
|
||||
lid: lid,
|
||||
pn: pn ? toCusFormat(pn) : null,
|
||||
};
|
||||
}
|
||||
|
||||
public async findLIDByPhoneNumber(
|
||||
phoneNumber: string,
|
||||
): Promise<LidToPhoneNumber> {
|
||||
const pn = toJID(phoneNumber);
|
||||
const lid = await this.store.findLidByPN(pn);
|
||||
return {
|
||||
lid: lid || null,
|
||||
pn: toCusFormat(pn),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Group methods
|
||||
*/
|
||||
@@ -1473,14 +1527,14 @@ export class WhatsappSessionNoWebCore extends WhatsappSession {
|
||||
}
|
||||
|
||||
public async getPresence(chatId: string): Promise<WAHAChatPresences> {
|
||||
const remoteJid = toJID(chatId);
|
||||
if (!(remoteJid in this.store.presences)) {
|
||||
this.store.presences[remoteJid] = {};
|
||||
await this.sock.presenceSubscribe(remoteJid);
|
||||
const jid = toJID(chatId);
|
||||
await this.subscribePresence(jid);
|
||||
if (!(jid in this.store.presences)) {
|
||||
this.store.presences[jid] = {};
|
||||
await sleep(1000);
|
||||
}
|
||||
const result = this.store.presences[remoteJid];
|
||||
return this.toWahaPresences(remoteJid, result);
|
||||
const result = this.store.presences[jid];
|
||||
return this.toWahaPresences(jid, result);
|
||||
}
|
||||
|
||||
public subscribePresence(id: string): Promise<void> {
|
||||
@@ -1751,6 +1805,7 @@ export class WhatsappSessionNoWebCore extends WhatsappSession {
|
||||
const messagesUpsert$ = fromEvent(this.sock.ev, 'messages.upsert').pipe(
|
||||
map((event: BaileysEventMap['messages.upsert']) => event.messages),
|
||||
mergeAll(),
|
||||
share(),
|
||||
);
|
||||
let [messagesFromMe$, messagesFromOthers$] = partition(
|
||||
messagesUpsert$,
|
||||
@@ -1777,15 +1832,45 @@ export class WhatsappSessionNoWebCore extends WhatsappSession {
|
||||
),
|
||||
mergeMap(async (message): Promise<WAMessageRevokedBody> => {
|
||||
const afterMessage = await this.toWAMessage(message);
|
||||
// Extract the revoked message ID from protocolMessage.key
|
||||
const revokedMessageId = message.message.protocolMessage.key?.id;
|
||||
return {
|
||||
after: afterMessage,
|
||||
before: null,
|
||||
revokedMessageId: revokedMessageId,
|
||||
_data: message,
|
||||
};
|
||||
}),
|
||||
);
|
||||
this.events2.get(WAHAEvents.MESSAGE_REVOKED).switch(messagesRevoked$);
|
||||
|
||||
// Handle edited messages
|
||||
// @ts-ignore
|
||||
const messagesEdited$ = messagesUpsert$.pipe(
|
||||
filter(
|
||||
(message) =>
|
||||
// @ts-ignore
|
||||
message.message?.protocolMessage?.type ===
|
||||
proto.Message.ProtocolMessage.Type.MESSAGE_EDIT &&
|
||||
message.message?.protocolMessage?.editedMessage !== undefined,
|
||||
),
|
||||
mergeMap(async (message): Promise<WAMessageEditedBody> => {
|
||||
const waMessage = await this.toWAMessage(message);
|
||||
// Extract the body from editedMessage using extractBody function
|
||||
const body =
|
||||
this.extractBody(message.message.protocolMessage.editedMessage) || '';
|
||||
// Extract the original message ID from protocolMessage.key
|
||||
const editedMessageId = message.message.protocolMessage.key?.id;
|
||||
return {
|
||||
...waMessage,
|
||||
body: body,
|
||||
editedMessageId: editedMessageId,
|
||||
_data: message,
|
||||
};
|
||||
}),
|
||||
);
|
||||
this.events2.get(WAHAEvents.MESSAGE_EDITED).switch(messagesEdited$);
|
||||
|
||||
//
|
||||
// Message Reactions
|
||||
//
|
||||
@@ -1804,6 +1889,7 @@ export class WhatsappSessionNoWebCore extends WhatsappSession {
|
||||
).pipe(
|
||||
// @ts-ignore
|
||||
mergeAll(),
|
||||
share(),
|
||||
);
|
||||
const messageAckDirect$ = messageUpdates$.pipe(
|
||||
filter(isMine), // ack comes only for MY messages
|
||||
@@ -1814,6 +1900,7 @@ export class WhatsappSessionNoWebCore extends WhatsappSession {
|
||||
fromEvent(this.sock.ev, 'message-receipt.update').pipe(
|
||||
// @ts-ignore
|
||||
mergeAll(),
|
||||
share(),
|
||||
);
|
||||
|
||||
const messageAckGroups$ = messageReceiptUpdate$.pipe(
|
||||
@@ -1830,7 +1917,7 @@ export class WhatsappSessionNoWebCore extends WhatsappSession {
|
||||
//
|
||||
this.events2
|
||||
.get(WAHAEvents.STATE_CHANGE)
|
||||
.switch(fromEvent(this.sock.ev, 'connection.update'));
|
||||
.switch(fromEvent(this.sock.ev, 'connection.update').pipe(share()));
|
||||
|
||||
const groupsUpsert$: Observable<GroupMetadata> = fromEvent(
|
||||
this.sock.ev,
|
||||
@@ -1838,6 +1925,7 @@ export class WhatsappSessionNoWebCore extends WhatsappSession {
|
||||
).pipe(
|
||||
// @ts-ignore
|
||||
mergeAll(),
|
||||
share(),
|
||||
);
|
||||
const groupsUpdate$: Observable<Partial<GroupMetadata>> = fromEvent(
|
||||
this.sock.ev,
|
||||
@@ -1845,11 +1933,12 @@ export class WhatsappSessionNoWebCore extends WhatsappSession {
|
||||
).pipe(
|
||||
// @ts-ignore
|
||||
mergeAll(),
|
||||
share(),
|
||||
);
|
||||
const groupsParticipantsUpdate$: Observable<any> = fromEvent(
|
||||
this.sock.ev,
|
||||
'group-participants.update',
|
||||
);
|
||||
).pipe(share());
|
||||
|
||||
this.events2.get(WAHAEvents.GROUP_JOIN).switch(groupsUpsert$);
|
||||
|
||||
@@ -1876,13 +1965,12 @@ export class WhatsappSessionNoWebCore extends WhatsappSession {
|
||||
);
|
||||
this.events2.get(WAHAEvents.GROUP_V2_LEAVE).switch(groupV2Leave$);
|
||||
|
||||
this.events2
|
||||
.get(WAHAEvents.PRESENCE_UPDATE)
|
||||
.switch(
|
||||
fromEvent(this.sock.ev, 'presence.update').pipe(
|
||||
map((data: any) => this.toWahaPresences(data.id, data.presences)),
|
||||
),
|
||||
);
|
||||
this.events2.get(WAHAEvents.PRESENCE_UPDATE).switch(
|
||||
fromEvent(this.sock.ev, 'presence.update').pipe(
|
||||
map((data: any) => this.toWahaPresences(data.id, data.presences)),
|
||||
share(),
|
||||
),
|
||||
);
|
||||
|
||||
//
|
||||
// Poll votes
|
||||
@@ -1909,7 +1997,7 @@ export class WhatsappSessionNoWebCore extends WhatsappSession {
|
||||
//
|
||||
// @ts-ignore
|
||||
const calls$: Observable<WACallEvent[]> = fromEvent(this.sock.ev, 'call');
|
||||
const call$ = calls$.pipe(mergeMap(identity));
|
||||
const call$ = calls$.pipe(mergeMap(identity), share());
|
||||
this.events2.get(WAHAEvents.CALL_RECEIVED).switch(
|
||||
call$.pipe(
|
||||
filter((call: WACallEvent) => call.status === 'offer'),
|
||||
@@ -1942,7 +2030,7 @@ export class WhatsappSessionNoWebCore extends WhatsappSession {
|
||||
const labelsEdit$: Observable<NOWEBLabel> = fromEvent(
|
||||
this.sock.ev,
|
||||
'labels.edit',
|
||||
);
|
||||
).pipe(share());
|
||||
this.events2.get(WAHAEvents.LABEL_UPSERT).switch(
|
||||
labelsEdit$.pipe(
|
||||
exclude((data: NOWEBLabel) => data.deleted),
|
||||
@@ -1955,7 +2043,10 @@ export class WhatsappSessionNoWebCore extends WhatsappSession {
|
||||
map(this.toLabel.bind(this)),
|
||||
),
|
||||
);
|
||||
const labelsAssociation$ = fromEvent(this.sock.ev, 'labels.association');
|
||||
const labelsAssociation$ = fromEvent(
|
||||
this.sock.ev,
|
||||
'labels.association',
|
||||
).pipe(share());
|
||||
const labelsAssociationAdd$: Observable<ChatLabelAssociation> =
|
||||
labelsAssociation$.pipe(
|
||||
filter(({ type }: any) => type === 'add'),
|
||||
@@ -2049,12 +2140,18 @@ export class WhatsappSessionNoWebCore extends WhatsappSession {
|
||||
if (message.message.pollUpdateMessage) return;
|
||||
// Ignore calls, we have dedicated handler for that
|
||||
if (message.message.call?.callKey) return;
|
||||
// Ignore revoke, we have a dedicated handler for that
|
||||
// Ignore revoke, we have a dedicated event for that
|
||||
if (
|
||||
message.message?.protocolMessage?.type ===
|
||||
proto.Message.ProtocolMessage.Type.REVOKE
|
||||
)
|
||||
return;
|
||||
// Ignore edit, we have a dedicated event for that
|
||||
if (
|
||||
message.message?.protocolMessage?.type ===
|
||||
proto.Message.ProtocolMessage.Type.MESSAGE_EDIT
|
||||
)
|
||||
return;
|
||||
if (
|
||||
message.message?.protocolMessage?.type ===
|
||||
proto.Message.ProtocolMessage.Type.EPHEMERAL_SYNC_RESPONSE
|
||||
@@ -2561,7 +2658,7 @@ function getFrom(key, meId) {
|
||||
return key.remoteJid;
|
||||
}
|
||||
|
||||
function getDestination(key, meId = undefined): MessageDestination {
|
||||
export function getDestination(key, meId = undefined): MessageDestination {
|
||||
return {
|
||||
id: buildMessageId(key),
|
||||
to: toCusFormat(getTo(key, meId)),
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Chat } from '@adiwajshing/baileys';
|
||||
import { OverviewFilter } from '@waha/structures/chats.dto';
|
||||
import { PaginationParams } from '@waha/structures/pagination.dto';
|
||||
|
||||
export interface IChatRepository {
|
||||
@@ -9,6 +10,7 @@ export interface IChatRepository {
|
||||
getAllWithMessages(
|
||||
pagination: PaginationParams,
|
||||
broadcast: boolean,
|
||||
filter?: OverviewFilter,
|
||||
): Promise<Chat[]>;
|
||||
|
||||
getById(id: string): Promise<Chat | null>;
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
import { LimitOffsetParams } from '@waha/structures/pagination.dto';
|
||||
|
||||
export class LidToPN {
|
||||
id: string;
|
||||
pn: string;
|
||||
}
|
||||
|
||||
export interface INowebLidPNRepository {
|
||||
/**
|
||||
* Save lid to phone number mapping
|
||||
*/
|
||||
saveLids(lids: LidToPN[]): Promise<void>;
|
||||
|
||||
/**
|
||||
* Get all lids with pagination
|
||||
*/
|
||||
getAllLids(pagination?: LimitOffsetParams): Promise<LidToPN[]>;
|
||||
|
||||
/**
|
||||
* Get a total count of lids
|
||||
*/
|
||||
getLidsCount(): Promise<number>;
|
||||
|
||||
/**
|
||||
* Find a phone number by lid
|
||||
*/
|
||||
findPNByLid(lid: string): Promise<string | null>;
|
||||
|
||||
/**
|
||||
* Find lid by phone number
|
||||
*/
|
||||
findLidByPN(pn: string): Promise<string | null>;
|
||||
}
|
||||
@@ -5,6 +5,7 @@ import { ILabelsRepository } from '@waha/core/engines/noweb/store/ILabelsReposit
|
||||
import { IChatRepository } from './IChatRepository';
|
||||
import { IContactRepository } from './IContactRepository';
|
||||
import { IMessagesRepository } from './IMessagesRepository';
|
||||
import { INowebLidPNRepository } from './INowebLidPNRepository';
|
||||
|
||||
export abstract class INowebStorage {
|
||||
abstract init(): Promise<void>;
|
||||
@@ -22,4 +23,6 @@ export abstract class INowebStorage {
|
||||
abstract getLabelsRepository(): ILabelsRepository;
|
||||
|
||||
abstract getLabelAssociationRepository(): ILabelAssociationRepository;
|
||||
|
||||
abstract getLidPNRepository(): INowebLidPNRepository;
|
||||
}
|
||||
@@ -6,8 +6,15 @@ import {
|
||||
} from '@adiwajshing/baileys';
|
||||
import { GroupMetadata } from '@adiwajshing/baileys/lib/Types/GroupMetadata';
|
||||
import { Label } from '@adiwajshing/baileys/lib/Types/Label';
|
||||
import { GetChatMessagesFilter } from '@waha/structures/chats.dto';
|
||||
import { PaginationParams } from '@waha/structures/pagination.dto';
|
||||
import {
|
||||
GetChatMessagesFilter,
|
||||
OverviewFilter,
|
||||
} from '@waha/structures/chats.dto';
|
||||
import { LidToPhoneNumber } from '@waha/structures/lids.dto';
|
||||
import {
|
||||
LimitOffsetParams,
|
||||
PaginationParams,
|
||||
} from '@waha/structures/pagination.dto';
|
||||
|
||||
export interface INowebStore {
|
||||
presences: any;
|
||||
@@ -28,7 +35,11 @@ export interface INowebStore {
|
||||
|
||||
getMessageById(chatId: string, messageId: string): Promise<any>;
|
||||
|
||||
getChats(pagination: PaginationParams, broadcast: boolean): Promise<Chat[]>;
|
||||
getChats(
|
||||
pagination: PaginationParams,
|
||||
broadcast: boolean,
|
||||
filter?: OverviewFilter,
|
||||
): Promise<Chat[]>;
|
||||
|
||||
getChat(jid: string): Promise<Chat | null>;
|
||||
|
||||
@@ -47,4 +58,13 @@ export interface INowebStore {
|
||||
getGroups(pagination: PaginationParams): Promise<GroupMetadata[]>;
|
||||
|
||||
resetGroupsCache(): void;
|
||||
|
||||
// Lid Repository methods
|
||||
getAllLids(pagination?: LimitOffsetParams): Promise<LidToPhoneNumber[]>;
|
||||
|
||||
getLidsCount(): Promise<number>;
|
||||
|
||||
findPNByLid(lid: string): Promise<string | null>;
|
||||
|
||||
findLidByPN(pn: string): Promise<string | null>;
|
||||
}
|
||||
@@ -7,7 +7,11 @@ import makeWASocket, {
|
||||
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 { LidToPhoneNumber } from '@waha/structures/lids.dto';
|
||||
import {
|
||||
LimitOffsetParams,
|
||||
PaginationParams,
|
||||
} from '@waha/structures/pagination.dto';
|
||||
import { PaginatorInMemory } from '@waha/utils/Paginator';
|
||||
|
||||
import { INowebStore } from './INowebStore';
|
||||
@@ -107,4 +111,23 @@ export class NowebInMemoryStore implements INowebStore {
|
||||
resetGroupsCache() {
|
||||
return;
|
||||
}
|
||||
|
||||
//
|
||||
// Lids methods
|
||||
//
|
||||
getAllLids(pagination?: LimitOffsetParams): Promise<LidToPhoneNumber[]> {
|
||||
throw new BadRequestException(this.errorMessage);
|
||||
}
|
||||
|
||||
findLidByPN(pn: string): Promise<string | null> {
|
||||
throw new BadRequestException(this.errorMessage);
|
||||
}
|
||||
|
||||
findPNByLid(lid: string): Promise<string | null> {
|
||||
throw new BadRequestException(this.errorMessage);
|
||||
}
|
||||
|
||||
getLidsCount(): Promise<number> {
|
||||
throw new BadRequestException(this.errorMessage);
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,7 @@ import makeWASocket, {
|
||||
ChatUpdate,
|
||||
Contact,
|
||||
GroupParticipant,
|
||||
isJidUser,
|
||||
isRealMessage,
|
||||
jidNormalizedUser,
|
||||
ParticipantAction,
|
||||
@@ -18,13 +19,22 @@ import {
|
||||
LabelAssociation,
|
||||
LabelAssociationType,
|
||||
} from '@adiwajshing/baileys/lib/Types/LabelAssociation';
|
||||
import { isLidUser } from '@adiwajshing/baileys/lib/WABinary/jid-utils';
|
||||
import { IGroupRepository } from '@waha/core/engines/noweb/store/IGroupRepository';
|
||||
import { ILabelAssociationRepository } from '@waha/core/engines/noweb/store/ILabelAssociationsRepository';
|
||||
import { ILabelsRepository } from '@waha/core/engines/noweb/store/ILabelsRepository';
|
||||
import { GetChatMessagesFilter } from '@waha/structures/chats.dto';
|
||||
import { PaginationParams, SortOrder } from '@waha/structures/pagination.dto';
|
||||
import {
|
||||
GetChatMessagesFilter,
|
||||
OverviewFilter,
|
||||
} from '@waha/structures/chats.dto';
|
||||
import { LidToPhoneNumber } from '@waha/structures/lids.dto';
|
||||
import {
|
||||
LimitOffsetParams,
|
||||
PaginationParams,
|
||||
SortOrder,
|
||||
} from '@waha/structures/pagination.dto';
|
||||
import { DefaultMap } from '@waha/utils/DefaultMap';
|
||||
import { sleep, waitUntil } from '@waha/utils/promiseTimeout';
|
||||
import { waitUntil } from '@waha/utils/promiseTimeout';
|
||||
import * as lodash from 'lodash';
|
||||
import { toNumber } from 'lodash';
|
||||
import { Logger } from 'pino';
|
||||
@@ -32,6 +42,7 @@ import { Logger } from 'pino';
|
||||
import { IChatRepository } from './IChatRepository';
|
||||
import { IContactRepository } from './IContactRepository';
|
||||
import { IMessagesRepository } from './IMessagesRepository';
|
||||
import { INowebLidPNRepository, LidToPN } from './INowebLidPNRepository';
|
||||
import { INowebStorage } from './INowebStorage';
|
||||
import { INowebStore } from './INowebStore';
|
||||
|
||||
@@ -49,9 +60,17 @@ export class NowebPersistentStore implements INowebStore {
|
||||
private messagesRepo: IMessagesRepository;
|
||||
private labelsRepo: ILabelsRepository;
|
||||
private labelAssociationsRepo: ILabelAssociationRepository;
|
||||
private lidRepo: INowebLidPNRepository;
|
||||
public presences: any;
|
||||
private lock: any;
|
||||
|
||||
private lock: any = new AsyncLock({
|
||||
timeout: 5_000,
|
||||
maxPending: Infinity,
|
||||
maxExecutionTime: 60_000,
|
||||
});
|
||||
|
||||
private groupsFetchLock: any = new AsyncLock({
|
||||
timeout: 5_000,
|
||||
maxPending: Infinity,
|
||||
maxExecutionTime: 60_000,
|
||||
});
|
||||
@@ -71,8 +90,8 @@ export class NowebPersistentStore implements INowebStore {
|
||||
this.messagesRepo = storage.getMessagesRepository();
|
||||
this.labelsRepo = storage.getLabelsRepository();
|
||||
this.labelAssociationsRepo = storage.getLabelAssociationRepository();
|
||||
this.lidRepo = storage.getLidPNRepository();
|
||||
this.presences = {};
|
||||
this.lock = new AsyncLock({ maxPending: Infinity });
|
||||
}
|
||||
|
||||
init(): Promise<void> {
|
||||
@@ -112,9 +131,16 @@ export class NowebPersistentStore implements INowebStore {
|
||||
ev.on('groups.upsert', (data) =>
|
||||
this.withLock('groups', () => this.onGroupUpsert(data)),
|
||||
);
|
||||
ev.on('groups.update', (data) =>
|
||||
this.withLock('groups', () => this.onGroupUpdate(data)),
|
||||
);
|
||||
ev.on('groups.update', (data) => {
|
||||
this.withLock('groups', () => this.onGroupUpdate(data));
|
||||
this.withLock('lids', async () => {
|
||||
const participants = lodash.flatMap(data, (g) => g?.participants || []);
|
||||
const lids = await this.handleLidPNUpdates(participants);
|
||||
this.logger.debug(
|
||||
`groups.update - '${lids.length}' synced lid to pn mapping`,
|
||||
);
|
||||
});
|
||||
});
|
||||
ev.on('group-participants.update', (data) =>
|
||||
this.withLock(`group-${data.id}`, () =>
|
||||
this.onGroupParticipantsUpdate(data),
|
||||
@@ -122,12 +148,24 @@ export class NowebPersistentStore implements INowebStore {
|
||||
);
|
||||
|
||||
// Contacts
|
||||
ev.on('contacts.upsert', (data) =>
|
||||
this.withLock('contacts', () => this.onContactsUpsert(data)),
|
||||
);
|
||||
ev.on('contacts.update', (data) =>
|
||||
this.withLock('contacts', () => this.onContactUpdate(data)),
|
||||
);
|
||||
ev.on('contacts.upsert', (data) => {
|
||||
this.withLock('contacts', () => this.onContactsUpsert(data));
|
||||
this.withLock('lids', async () => {
|
||||
const lids = await this.handleLidPNUpdates(data);
|
||||
this.logger.debug(
|
||||
`contacts.upsert - '${lids.length}' synced lid to pn mapping`,
|
||||
);
|
||||
});
|
||||
});
|
||||
ev.on('contacts.update', (data) => {
|
||||
this.withLock('contacts', () => this.onContactUpdate(data));
|
||||
this.withLock('lids', async () => {
|
||||
const lids = await this.handleLidPNUpdates(data);
|
||||
this.logger.debug(
|
||||
`contacts.update - '${lids.length}' synced lid to pn mapping`,
|
||||
);
|
||||
});
|
||||
});
|
||||
ev.on('labels.edit', (data) => this.onLabelsEdit(data));
|
||||
ev.on('labels.association', ({ association, type }) =>
|
||||
this.onLabelsAssociation(association, type),
|
||||
@@ -162,6 +200,12 @@ export class NowebPersistentStore implements INowebStore {
|
||||
await this.onContactsUpsert(contacts);
|
||||
this.logger.info(`history sync - '${contacts.length}' synced contacts`);
|
||||
}),
|
||||
this.withLock('lids', async () => {
|
||||
const lids = await this.handleLidPNUpdates(contacts);
|
||||
this.logger.info(
|
||||
`history sync - '${lids.length}' synced lid to pn mapping`,
|
||||
);
|
||||
}),
|
||||
this.withLock('chats', () => this.onChatUpsert(chats)),
|
||||
this.withLock('messages', () => this.syncMessagesHistory(messages)),
|
||||
]);
|
||||
@@ -192,6 +236,9 @@ export class NowebPersistentStore implements INowebStore {
|
||||
for (const update of updates) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
const jid = jidNormalizedUser(update.key.remoteJid!);
|
||||
if (!update.key.id) {
|
||||
continue;
|
||||
}
|
||||
const message = await this.messagesRepo.getByJidById(jid, update.key.id);
|
||||
if (!message) {
|
||||
this.logger.warn(
|
||||
@@ -482,10 +529,14 @@ export class NowebPersistentStore implements INowebStore {
|
||||
return this.messagesRepo.getByJidById(chatId, messageId);
|
||||
}
|
||||
|
||||
getChats(pagination: PaginationParams, broadcast: boolean): Promise<Chat[]> {
|
||||
getChats(
|
||||
pagination: PaginationParams,
|
||||
broadcast: boolean,
|
||||
filter?: OverviewFilter,
|
||||
): Promise<Chat[]> {
|
||||
pagination.sortBy ||= 'conversationTimestamp';
|
||||
pagination.sortOrder ||= SortOrder.DESC;
|
||||
return this.chatRepo.getAllWithMessages(pagination, broadcast);
|
||||
return this.chatRepo.getAllWithMessages(pagination, broadcast, filter);
|
||||
}
|
||||
|
||||
async getChat(jid: string): Promise<Chat | null> {
|
||||
@@ -559,4 +610,64 @@ export class NowebPersistentStore implements INowebStore {
|
||||
const ids = associations.map((association) => association.labelId);
|
||||
return await this.labelsRepo.getAllByIds(ids);
|
||||
}
|
||||
|
||||
//
|
||||
// Lid methods
|
||||
//
|
||||
private async handleLidPNUpdates(contacts: Array<Partial<Contact>>) {
|
||||
let lids: LidToPN[] = [];
|
||||
for (const contact of contacts) {
|
||||
// contact.id = pn, contact.lid = lid
|
||||
if (isJidUser(contact.id) && isLidUser(contact.lid)) {
|
||||
lids.push({
|
||||
pn: contact.id,
|
||||
id: contact.lid,
|
||||
});
|
||||
}
|
||||
// contact.pn = pn, contact.lid = lid
|
||||
else if (isJidUser(contact.pn) && isLidUser(contact.lid)) {
|
||||
lids.push({
|
||||
pn: contact.pn,
|
||||
id: contact.lid,
|
||||
});
|
||||
}
|
||||
// contact.pn = pn, contact.id = lid
|
||||
else if (isJidUser(contact.pn) && isLidUser(contact.id)) {
|
||||
lids.push({
|
||||
pn: contact.pn,
|
||||
id: contact.id,
|
||||
});
|
||||
}
|
||||
}
|
||||
// make lids unique by id
|
||||
lids = lodash.uniqBy(lids, 'id');
|
||||
if (lids.length > 0) {
|
||||
await this.lidRepo.saveLids(lids);
|
||||
}
|
||||
return lids;
|
||||
}
|
||||
|
||||
async getAllLids(
|
||||
pagination?: LimitOffsetParams,
|
||||
): Promise<LidToPhoneNumber[]> {
|
||||
const lids = await this.lidRepo.getAllLids(pagination);
|
||||
return lids.map((value) => {
|
||||
return {
|
||||
lid: value.id,
|
||||
pn: value.pn,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
getLidsCount(): Promise<number> {
|
||||
return this.lidRepo.getLidsCount();
|
||||
}
|
||||
|
||||
findPNByLid(lid: string): Promise<string | null> {
|
||||
return this.lidRepo.findPNByLid(lid);
|
||||
}
|
||||
|
||||
findLidByPN(pn: string): Promise<string | null> {
|
||||
return this.lidRepo.findLidByPN(pn);
|
||||
}
|
||||
}
|
||||
@@ -65,6 +65,15 @@ export const NowebLabelAssociationsSchema = new Schema(
|
||||
],
|
||||
);
|
||||
|
||||
export const NowebLidMapSchema = new Schema(
|
||||
'lid_map',
|
||||
[new Field('id', 'TEXT'), new Field('pn', 'TEXT'), new Field('data', 'TEXT')],
|
||||
[
|
||||
new Index('lid_map_id_index', ['id']),
|
||||
new Index('lid_map_pn_index', ['pn']),
|
||||
],
|
||||
);
|
||||
|
||||
export const NOWEB_STORE_SCHEMA = [
|
||||
NowebContactSchema,
|
||||
NowebChatSchema,
|
||||
@@ -72,6 +81,7 @@ export const NOWEB_STORE_SCHEMA = [
|
||||
NowebMessagesSchema,
|
||||
NowebLabelsSchema,
|
||||
NowebLabelAssociationsSchema,
|
||||
NowebLidMapSchema,
|
||||
];
|
||||
|
||||
export const Migrations: string[] = [
|
||||
@@ -100,4 +110,8 @@ export const Migrations: string[] = [
|
||||
'CREATE INDEX IF NOT EXISTS label_assoc_type_label_index ON "labelAssociations" (type, "labelId")',
|
||||
'CREATE INDEX IF NOT EXISTS label_assoc_type_chat_index ON "labelAssociations" (type, "chatId")',
|
||||
'CREATE INDEX IF NOT EXISTS label_assoc_type_message_index ON "labelAssociations" (type, "messageId")',
|
||||
// Lid Map
|
||||
'CREATE TABLE IF NOT EXISTS lid_map (id TEXT PRIMARY KEY, pn TEXT, data TEXT)',
|
||||
'CREATE UNIQUE INDEX IF NOT EXISTS lid_map_id_index ON lid_map (id)',
|
||||
'CREATE INDEX IF NOT EXISTS lid_map_pn_index ON lid_map (pn)',
|
||||
];
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Chat } from '@adiwajshing/baileys';
|
||||
import { SqlKVRepository } from '@waha/core/storage/sql/SqlKVRepository';
|
||||
import { OverviewFilter } from '@waha/structures/chats.dto';
|
||||
import { PaginationParams } from '@waha/structures/pagination.dto';
|
||||
|
||||
export class SqlChatMethods {
|
||||
@@ -8,15 +9,23 @@ export class SqlChatMethods {
|
||||
async getAllWithMessages(
|
||||
pagination: PaginationParams,
|
||||
broadcast: boolean,
|
||||
filter?: OverviewFilter,
|
||||
): Promise<Chat[]> {
|
||||
// Get chats with conversationTimestamp is not Null
|
||||
let query = this.repository.select().whereNotNull('conversationTimestamp');
|
||||
|
||||
if (!broadcast) {
|
||||
// filter out chat by id if it ends at @newsletter or @broadcast
|
||||
query = query
|
||||
.andWhereNot('id', 'like', '%@broadcast')
|
||||
.andWhereNot('id', 'like', '%@newsletter');
|
||||
}
|
||||
|
||||
// Filter by IDs if provided
|
||||
if (filter?.ids && filter.ids.length > 0) {
|
||||
query = query.whereIn('id', filter.ids);
|
||||
}
|
||||
|
||||
query = this.repository.pagination(query, pagination);
|
||||
return await this.repository.all(query);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Chat } from '@adiwajshing/baileys';
|
||||
import { NowebChatSchema } from '@waha/core/engines/noweb/store/schemas';
|
||||
import { SqlChatMethods } from '@waha/core/engines/noweb/store/sql/SqlChatMethods';
|
||||
import { OverviewFilter } from '@waha/structures/chats.dto';
|
||||
import { PaginationParams } from '@waha/structures/pagination.dto';
|
||||
import { KnexPaginator } from '@waha/utils/Paginator';
|
||||
|
||||
@@ -28,7 +29,8 @@ export class Sqlite3ChatRepository
|
||||
getAllWithMessages(
|
||||
pagination: PaginationParams,
|
||||
broadcast: boolean,
|
||||
filter?: OverviewFilter,
|
||||
): Promise<Chat[]> {
|
||||
return this.methods.getAllWithMessages(pagination, broadcast);
|
||||
return this.methods.getAllWithMessages(pagination, broadcast, filter);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
import { NowebLidMapSchema } from '@waha/core/engines/noweb/store/schemas';
|
||||
import { LimitOffsetParams } from '@waha/structures/pagination.dto';
|
||||
import { KnexPaginator } from '@waha/utils/Paginator';
|
||||
|
||||
import { INowebLidPNRepository, LidToPN } from '../INowebLidPNRepository';
|
||||
import { NOWEBSqlite3KVRepository } from './NOWEBSqlite3KVRepository';
|
||||
|
||||
export class LidPaginator extends KnexPaginator {
|
||||
indexes = ['id', 'pn'];
|
||||
}
|
||||
|
||||
export class Sqlite3LidPNRepository
|
||||
extends NOWEBSqlite3KVRepository<LidToPN>
|
||||
implements INowebLidPNRepository
|
||||
{
|
||||
get schema() {
|
||||
return NowebLidMapSchema;
|
||||
}
|
||||
|
||||
saveLids(lids: LidToPN[]): Promise<void> {
|
||||
return this.upsertMany(lids);
|
||||
}
|
||||
|
||||
getAllLids(pagination?: LimitOffsetParams): Promise<LidToPN[]> {
|
||||
return this.getAll(pagination);
|
||||
}
|
||||
|
||||
getLidsCount(): Promise<number> {
|
||||
return this.getCount();
|
||||
}
|
||||
|
||||
async findLidByPN(pn: string): Promise<string | null> {
|
||||
const value = await this.getBy({ pn: pn });
|
||||
return value?.id || null;
|
||||
}
|
||||
|
||||
async findPNByLid(lid: string): Promise<string | null> {
|
||||
const value = await this.getBy({ id: lid });
|
||||
return value?.pn || null;
|
||||
}
|
||||
}
|
||||
@@ -1,20 +1,18 @@
|
||||
import { Schema } from '@waha/core/storage/Schema';
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
const Database = require('better-sqlite3');
|
||||
import Knex from 'knex';
|
||||
|
||||
export class Sqlite3SchemaValidation {
|
||||
constructor(
|
||||
private table: Schema,
|
||||
private db,
|
||||
private knex: Knex.Knex,
|
||||
) {}
|
||||
|
||||
validate() {
|
||||
async validate() {
|
||||
const table = this.table;
|
||||
|
||||
// Check table has the columns
|
||||
const columns = this.db.prepare(`PRAGMA table_info(${table.name})`).all();
|
||||
// Check exact number of columns
|
||||
const columns = await this.knex.raw(`PRAGMA table_info(${table.name})`);
|
||||
// Check the exact number of columns
|
||||
if (columns.length !== table.columns.length) {
|
||||
throw new Error(
|
||||
`Table '${table.name}' does not have expected number of columns. Expected ${table.columns.length}, got ${columns.length}`,
|
||||
@@ -37,7 +35,7 @@ export class Sqlite3SchemaValidation {
|
||||
}
|
||||
|
||||
// Check table has expected indexes
|
||||
const indexes = this.db.prepare(`PRAGMA index_list(${table.name})`).all();
|
||||
const indexes = await this.knex.raw(`PRAGMA index_list(${table.name})`);
|
||||
const indexNames = indexes.map((index) => index.name);
|
||||
for (const index of table.indexes) {
|
||||
if (!indexNames.includes(index.name)) {
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
import { ILabelAssociationRepository } from '@waha/core/engines/noweb/store/ILabelAssociationsRepository';
|
||||
import { ILabelsRepository } from '@waha/core/engines/noweb/store/ILabelsRepository';
|
||||
import { INowebLidPNRepository } from '@waha/core/engines/noweb/store/INowebLidPNRepository';
|
||||
import { Sqlite3GroupRepository } from '@waha/core/engines/noweb/store/sqlite3/Sqlite3GroupRepository';
|
||||
import { Sqlite3LabelAssociationsRepository } from '@waha/core/engines/noweb/store/sqlite3/Sqlite3LabelAssociationsRepository';
|
||||
import { Sqlite3LabelsRepository } from '@waha/core/engines/noweb/store/sqlite3/Sqlite3LabelsRepository';
|
||||
import { Sqlite3LidPNRepository } from '@waha/core/engines/noweb/store/sqlite3/Sqlite3LidPNRepository';
|
||||
import { Schema } from '@waha/core/storage/Schema';
|
||||
import Knex from 'knex';
|
||||
|
||||
import { INowebStorage } from '../INowebStorage';
|
||||
import { Migrations, NOWEB_STORE_SCHEMA } from '../schemas';
|
||||
@@ -12,66 +15,71 @@ import { Sqlite3ContactRepository } from './Sqlite3ContactRepository';
|
||||
import { Sqlite3MessagesRepository } from './Sqlite3MessagesRepository';
|
||||
import { Sqlite3SchemaValidation } from './Sqlite3SchemaValidation';
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
const Database = require('better-sqlite3');
|
||||
|
||||
export class Sqlite3Storage extends INowebStorage {
|
||||
private readonly db: any;
|
||||
private readonly tables: Schema[];
|
||||
private readonly knex: Knex.Knex;
|
||||
|
||||
constructor(filePath: string) {
|
||||
super();
|
||||
this.db = new Database(filePath);
|
||||
this.knex = Knex({
|
||||
client: 'better-sqlite3',
|
||||
connection: { filename: filePath },
|
||||
useNullAsDefault: true,
|
||||
});
|
||||
this.tables = NOWEB_STORE_SCHEMA;
|
||||
}
|
||||
|
||||
async init() {
|
||||
this.db.pragma('journal_mode = WAL;');
|
||||
this.migrate();
|
||||
this.validateSchema();
|
||||
await this.knex.raw('PRAGMA journal_mode = WAL;');
|
||||
await this.migrate();
|
||||
await this.validateSchema();
|
||||
}
|
||||
|
||||
private migrate() {
|
||||
this.migration0001init();
|
||||
private async migrate() {
|
||||
await this.migration0001init();
|
||||
}
|
||||
|
||||
private validateSchema() {
|
||||
private async validateSchema() {
|
||||
for (const table of this.tables) {
|
||||
new Sqlite3SchemaValidation(table, this.db).validate();
|
||||
await new Sqlite3SchemaValidation(table, this.knex).validate();
|
||||
}
|
||||
}
|
||||
|
||||
private migration0001init() {
|
||||
private async migration0001init() {
|
||||
for (const migration of Migrations) {
|
||||
this.db.exec(migration);
|
||||
await this.knex.raw(migration);
|
||||
}
|
||||
}
|
||||
|
||||
async close() {
|
||||
return this.db.close();
|
||||
return this.knex.destroy();
|
||||
}
|
||||
|
||||
getContactsRepository() {
|
||||
return new Sqlite3ContactRepository(this.db);
|
||||
return new Sqlite3ContactRepository(this.knex);
|
||||
}
|
||||
|
||||
getChatRepository() {
|
||||
return new Sqlite3ChatRepository(this.db);
|
||||
return new Sqlite3ChatRepository(this.knex);
|
||||
}
|
||||
|
||||
getGroupRepository() {
|
||||
return new Sqlite3GroupRepository(this.db);
|
||||
return new Sqlite3GroupRepository(this.knex);
|
||||
}
|
||||
|
||||
getLabelsRepository(): ILabelsRepository {
|
||||
return new Sqlite3LabelsRepository(this.db);
|
||||
return new Sqlite3LabelsRepository(this.knex);
|
||||
}
|
||||
|
||||
getLabelAssociationRepository(): ILabelAssociationRepository {
|
||||
return new Sqlite3LabelAssociationsRepository(this.db);
|
||||
return new Sqlite3LabelAssociationsRepository(this.knex);
|
||||
}
|
||||
|
||||
getMessagesRepository() {
|
||||
return new Sqlite3MessagesRepository(this.db);
|
||||
return new Sqlite3MessagesRepository(this.knex);
|
||||
}
|
||||
|
||||
getLidPNRepository(): INowebLidPNRepository {
|
||||
return new Sqlite3LidPNRepository(this.knex);
|
||||
}
|
||||
}
|
||||
@@ -18,7 +18,11 @@ const AsyncLock = require('async-lock');
|
||||
// https://github.com/nodejs/node/issues/26338
|
||||
// Default pending is 1000, set it to infinity
|
||||
// https://github.com/rogierschouten/async-lock/issues/63
|
||||
const fileLock = new AsyncLock({ maxPending: Infinity });
|
||||
const fileLock = new AsyncLock({
|
||||
timeout: 5_000,
|
||||
maxPending: Infinity,
|
||||
maxExecutionTime: 30_000,
|
||||
});
|
||||
|
||||
/**
|
||||
* stores the full authentication state in a single folder.
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
/**
|
||||
* Expose a function to the page if it does not exist
|
||||
*
|
||||
* NOTE:
|
||||
* Rewrite it to 'upsertFunction' after updating Puppeteer to 20.6 or higher
|
||||
* using page.removeExposedFunction
|
||||
* https://pptr.dev/api/puppeteer.page.removeExposedFunction
|
||||
*
|
||||
* @param {import(puppeteer).Page} page
|
||||
* @param {string} name
|
||||
* @param {Function} fn
|
||||
*/
|
||||
export async function exposeFunctionIfAbsent(page, name, fn) {
|
||||
const exist = await page.evaluate((name) => {
|
||||
return !!window[name];
|
||||
}, name);
|
||||
if (exist) {
|
||||
return;
|
||||
}
|
||||
await page.exposeFunction(name, fn);
|
||||
}
|
||||
@@ -99,6 +99,18 @@ export class WPage extends EventEmitter {
|
||||
}
|
||||
}
|
||||
|
||||
async reload(options?: any): Promise<any> {
|
||||
try {
|
||||
return await this.page.reload(options);
|
||||
} catch (err) {
|
||||
this.emit(PAGE_CALL_ERROR_EVENT, {
|
||||
method: 'reload',
|
||||
error: err,
|
||||
});
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
on(event: string | symbol, listener: (...args: any[]) => void): this {
|
||||
if (event === PAGE_CALL_ERROR_EVENT) {
|
||||
super.on(event, listener);
|
||||
@@ -107,4 +119,8 @@ export class WPage extends EventEmitter {
|
||||
this.page.on(event, listener);
|
||||
return this.page as any;
|
||||
}
|
||||
|
||||
get tracing() {
|
||||
return this.page.tracing;
|
||||
}
|
||||
}
|
||||
@@ -1,13 +1,16 @@
|
||||
import { exposeFunctionIfAbsent } from '@waha/core/engines/webjs/Puppeteer';
|
||||
import { WebJSPresence } from '@waha/core/engines/webjs/types';
|
||||
import { GetChatMessagesFilter } from '@waha/structures/chats.dto';
|
||||
import { Label } from '@waha/structures/labels.dto';
|
||||
import { LidToPhoneNumber } from '@waha/structures/lids.dto';
|
||||
import { PaginationParams } from '@waha/structures/pagination.dto';
|
||||
import { TextStatus } from '@waha/structures/status.dto';
|
||||
import { sleep } from '@waha/utils/promiseTimeout';
|
||||
import { EventEmitter } from 'events';
|
||||
import * as lodash from 'lodash';
|
||||
import { Page } from 'puppeteer';
|
||||
import { Client, Events } from 'whatsapp-web.js';
|
||||
import { Message } from 'whatsapp-web.js/src/structures';
|
||||
import { exposeFunctionIfAbsent } from 'whatsapp-web.js/src/util/Puppeter';
|
||||
|
||||
import { CallErrorEvent, PAGE_CALL_ERROR_EVENT, WPage } from './WPage';
|
||||
|
||||
@@ -54,6 +57,26 @@ export class WebjsClientCore extends Client {
|
||||
await this.pupPage.evaluate(LoadWAHA);
|
||||
}
|
||||
|
||||
/**
|
||||
* @result indicating whether the UX fresh look was successfully hidden.
|
||||
*/
|
||||
hideUXFreshLook(): Promise<boolean> {
|
||||
return this.pupPage.evaluate(() => {
|
||||
const WAWebUserPrefsUiRefresh = window.require('WAWebUserPrefsUiRefresh');
|
||||
if (!WAWebUserPrefsUiRefresh) {
|
||||
return false;
|
||||
}
|
||||
if (WAWebUserPrefsUiRefresh.getUiRefreshNuxAcked()) {
|
||||
return false;
|
||||
}
|
||||
WAWebUserPrefsUiRefresh.incrementNuxViewCount();
|
||||
WAWebUserPrefsUiRefresh.setUiRefreshNuxAcked(true);
|
||||
const WAWebModalManager = window.require('WAWebModalManager');
|
||||
WAWebModalManager.ModalManager.close();
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
async attachCustomEventListeners() {
|
||||
await exposeFunctionIfAbsent(
|
||||
this.pupPage,
|
||||
@@ -99,7 +122,7 @@ export class WebjsClientCore extends Client {
|
||||
}
|
||||
|
||||
async createLabel(name: string, color: number): Promise<number> {
|
||||
const labelId: number = await this.pupPage.evaluate(
|
||||
const labelId: number = (await this.pupPage.evaluate(
|
||||
async (name, color) => {
|
||||
// @ts-ignore
|
||||
return await window.WAHA.WAWebBizLabelEditingAction.labelAddAction(
|
||||
@@ -109,7 +132,7 @@ export class WebjsClientCore extends Client {
|
||||
},
|
||||
name,
|
||||
color,
|
||||
);
|
||||
)) as any;
|
||||
return labelId;
|
||||
}
|
||||
|
||||
@@ -136,7 +159,7 @@ export class WebjsClientCore extends Client {
|
||||
}, label);
|
||||
}
|
||||
|
||||
async getChats(pagination?: PaginationParams) {
|
||||
async getChats(pagination?: PaginationParams, filter?: { ids?: string[] }) {
|
||||
if (lodash.isEmpty(pagination)) {
|
||||
return await super.getChats();
|
||||
}
|
||||
@@ -145,10 +168,14 @@ export class WebjsClientCore extends Client {
|
||||
pagination.limit ||= Infinity;
|
||||
pagination.offset ||= 0;
|
||||
|
||||
const chats = await this.pupPage.evaluate(async (pagination) => {
|
||||
// @ts-ignore
|
||||
return await window.WAHA.getChats(pagination);
|
||||
}, pagination);
|
||||
const chats = await this.pupPage.evaluate(
|
||||
async (pagination, filter) => {
|
||||
// @ts-ignore
|
||||
return await window.WAHA.getChats(pagination, filter);
|
||||
},
|
||||
pagination,
|
||||
filter,
|
||||
);
|
||||
|
||||
return chats.map((chat) => ChatFactory.create(this, chat));
|
||||
}
|
||||
@@ -262,4 +289,128 @@ export class WebjsClientCore extends Client {
|
||||
|
||||
return messages.map((m) => new Message(this, m));
|
||||
}
|
||||
|
||||
public async getAllLids(
|
||||
pagination: PaginationParams,
|
||||
): Promise<Array<LidToPhoneNumber>> {
|
||||
const lids: Array<LidToPhoneNumber> = (await this.pupPage.evaluate(
|
||||
async (pagination) => {
|
||||
pagination.limit ||= Infinity;
|
||||
pagination.offset ||= 0;
|
||||
pagination.sortBy ||= 'lid';
|
||||
|
||||
// @ts-ignore
|
||||
const WAWebApiContact = window.Store.LidUtils;
|
||||
|
||||
await WAWebApiContact.warmUpAllLidPnMappings();
|
||||
const lidMap = WAWebApiContact.lidPnCache['$1'];
|
||||
const values = Array.from(lidMap.values());
|
||||
const result = values.map((map) => {
|
||||
return {
|
||||
// @ts-ignore
|
||||
lid: map.lid._serialized,
|
||||
// @ts-ignore
|
||||
pn: map.phoneNumber._serialized,
|
||||
};
|
||||
});
|
||||
// @ts-ignore
|
||||
const paginator = new window.Paginator(pagination);
|
||||
const page = paginator.apply(result);
|
||||
return page;
|
||||
},
|
||||
pagination,
|
||||
)) as any;
|
||||
return lids;
|
||||
}
|
||||
|
||||
public async getLidsCount(): Promise<number> {
|
||||
const count: number = (await this.pupPage.evaluate(async () => {
|
||||
// @ts-ignore
|
||||
const WAWebApiContact = window.Store.LidUtils;
|
||||
|
||||
await WAWebApiContact.warmUpAllLidPnMappings();
|
||||
const lidMap = WAWebApiContact.lidPnCache['$1'];
|
||||
return lidMap.size;
|
||||
})) as any;
|
||||
return count;
|
||||
}
|
||||
|
||||
public async findPNByLid(lid: string): Promise<string> {
|
||||
const pn = await this.pupPage.evaluate(async (lid) => {
|
||||
// @ts-ignore
|
||||
const WAWebApiContact = window.Store.LidUtils;
|
||||
// @ts-ignore
|
||||
const WAWebWidFactory = window.Store.WidFactory;
|
||||
|
||||
const wid = WAWebWidFactory.createWid(lid);
|
||||
const result = WAWebApiContact.getPhoneNumber(wid);
|
||||
return result ? result._serialized : null;
|
||||
}, lid);
|
||||
return pn;
|
||||
}
|
||||
|
||||
public async findLIDByPhoneNumber(phoneNumber: string): Promise<string> {
|
||||
const lid: string = (await this.pupPage.evaluate(async (pn) => {
|
||||
// @ts-ignore
|
||||
const WAWebApiContact = window.Store.LidUtils;
|
||||
// @ts-ignore
|
||||
const WAWebWidFactory = window.Store.WidFactory;
|
||||
|
||||
const wid = WAWebWidFactory.createWid(pn);
|
||||
const result = WAWebApiContact.getCurrentLid(wid);
|
||||
return result ? result._serialized : null;
|
||||
}, phoneNumber)) as any;
|
||||
return lid;
|
||||
}
|
||||
|
||||
/**
|
||||
* Presences methods
|
||||
*/
|
||||
public async subscribePresence(chatId: string): Promise<void> {
|
||||
await this.pupPage.evaluate(async (chatId) => {
|
||||
const d = require;
|
||||
const WidFactory = d('WAWebWidFactory');
|
||||
|
||||
const wid = WidFactory.createWidFromWidLike(chatId);
|
||||
const chat = d('WAWebChatCollection').ChatCollection.get(wid);
|
||||
const tc = chat == null ? void 0 : chat.getTcToken();
|
||||
await d('WAWebContactPresenceBridge').subscribePresence(wid, tc);
|
||||
}, chatId);
|
||||
}
|
||||
|
||||
private async getCurrentPresence(chatId: string): Promise<WebJSPresence[]> {
|
||||
const result = await this.pupPage.evaluate(async (chatId) => {
|
||||
const d = require;
|
||||
const WidFactory = d('WAWebWidFactory');
|
||||
const PresenceCollection = d(
|
||||
'WAWebPresenceCollection',
|
||||
).PresenceCollection;
|
||||
const wid = WidFactory.createWidFromWidLike(chatId);
|
||||
const presence = PresenceCollection.get(wid);
|
||||
if (!presence) {
|
||||
return [];
|
||||
}
|
||||
let chatstates = [];
|
||||
if (chatId.endsWith('@c.us')) {
|
||||
chatstates = [presence.chatstate];
|
||||
} else {
|
||||
chatstates = presence.chatstates.getModelsArray();
|
||||
}
|
||||
return chatstates.map((chatstate) => {
|
||||
return {
|
||||
participant: chatstate.id._serialized,
|
||||
lastSeen: chatstate.t,
|
||||
state: chatstate.type,
|
||||
};
|
||||
});
|
||||
}, chatId);
|
||||
return result;
|
||||
}
|
||||
|
||||
public async getPresence(chatId: string): Promise<WebJSPresence[]> {
|
||||
await this.sendPresenceAvailable();
|
||||
await this.subscribePresence(chatId);
|
||||
await sleep(3_000);
|
||||
return await this.getCurrentPresence(chatId);
|
||||
}
|
||||
}
|
||||
@@ -4,8 +4,14 @@ exports.LoadWAHA = () => {
|
||||
'WAWebBizLabelEditingAction',
|
||||
);
|
||||
|
||||
window.WAHA.getChats = async (pagination) => {
|
||||
window.WAHA.getChats = async (pagination, filter) => {
|
||||
let chats = window.Store.Chat.getModelsArray().slice();
|
||||
|
||||
// Filter chats by IDs if filter is provided
|
||||
if (filter && filter.ids && filter.ids.length > 0) {
|
||||
chats = chats.filter((chat) => filter.ids.includes(chat.id._serialized));
|
||||
}
|
||||
|
||||
const paginator = new window.Paginator(pagination);
|
||||
chats = paginator.apply(chats);
|
||||
const chatPromises = chats.map((chat) => window.WWebJS.getChatModel(chat));
|
||||
|
||||
@@ -23,7 +23,7 @@ interface Me {
|
||||
lid?: string;
|
||||
}
|
||||
|
||||
function jid(field: any) {
|
||||
export function jid(field: any) {
|
||||
if (!field) {
|
||||
return field;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
import { BinaryNode, WAPresence } from '@adiwajshing/baileys';
|
||||
import { toCusFormat } from '@waha/core/engines/noweb/session.noweb.core';
|
||||
import { WAHAPresenceStatus } from '@waha/structures/enums.dto';
|
||||
import { WAHAChatPresences } from '@waha/structures/presence.dto';
|
||||
|
||||
import { jid } from './ack.webjs';
|
||||
|
||||
export function TagPresenceToPresence(node: BinaryNode): WAHAChatPresences {
|
||||
const { attrs } = node;
|
||||
const id = jid(attrs.from);
|
||||
const state =
|
||||
attrs.type === 'unavailable'
|
||||
? WAHAPresenceStatus.OFFLINE
|
||||
: WAHAPresenceStatus.ONLINE;
|
||||
const lastSeen = attrs.last && attrs.last !== 'deny' ? +attrs.last : null;
|
||||
return {
|
||||
id: toCusFormat(id),
|
||||
presences: [
|
||||
{
|
||||
participant: toCusFormat(id),
|
||||
lastKnownPresence: state,
|
||||
lastSeen: lastSeen,
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
export function TagChatstateToPresence(node: BinaryNode): WAHAChatPresences {
|
||||
const { attrs, content } = node;
|
||||
const id = jid(attrs.from);
|
||||
const participant = jid(attrs.participant) || jid(attrs.from);
|
||||
|
||||
const firstChild = content[0] as BinaryNode;
|
||||
|
||||
const type = firstChild.tag as WAPresence;
|
||||
let status = WAHAPresenceStatus.OFFLINE;
|
||||
|
||||
switch (type) {
|
||||
case 'unavailable':
|
||||
status = WAHAPresenceStatus.OFFLINE;
|
||||
break;
|
||||
case 'available':
|
||||
status = WAHAPresenceStatus.ONLINE;
|
||||
break;
|
||||
case 'paused':
|
||||
status = WAHAPresenceStatus.PAUSED;
|
||||
break;
|
||||
case 'composing':
|
||||
status = WAHAPresenceStatus.TYPING;
|
||||
break;
|
||||
}
|
||||
|
||||
if (firstChild.attrs?.media === 'audio') {
|
||||
status = WAHAPresenceStatus.RECORDING;
|
||||
}
|
||||
|
||||
return {
|
||||
id: toCusFormat(id),
|
||||
presences: [
|
||||
{
|
||||
participant: toCusFormat(participant),
|
||||
lastKnownPresence: status,
|
||||
lastSeen: null,
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
@@ -19,6 +19,10 @@ import {
|
||||
ToGroupV2UpdateEvent,
|
||||
} from '@waha/core/engines/webjs/groups.webjs';
|
||||
import { LocalAuth } from '@waha/core/engines/webjs/LocalAuth';
|
||||
import {
|
||||
TagChatstateToPresence,
|
||||
TagPresenceToPresence,
|
||||
} from '@waha/core/engines/webjs/presence';
|
||||
import { WebjsClientCore } from '@waha/core/engines/webjs/WebjsClientCore';
|
||||
import {
|
||||
CallErrorEvent,
|
||||
@@ -55,6 +59,7 @@ import {
|
||||
GetChatMessageQuery,
|
||||
GetChatMessagesFilter,
|
||||
GetChatMessagesQuery,
|
||||
OverviewFilter,
|
||||
ReadChatMessagesQuery,
|
||||
ReadChatMessagesResponse,
|
||||
} from '@waha/structures/chats.dto';
|
||||
@@ -93,26 +98,31 @@ import {
|
||||
SettingsSecurityChangeInfo,
|
||||
} from '@waha/structures/groups.dto';
|
||||
import { Label, LabelDTO, LabelID } from '@waha/structures/labels.dto';
|
||||
import { LidToPhoneNumber } from '@waha/structures/lids.dto';
|
||||
import { ReplyToMessage } from '@waha/structures/message.dto';
|
||||
import { PaginationParams, SortOrder } from '@waha/structures/pagination.dto';
|
||||
import {
|
||||
MessageSource,
|
||||
WAMessage,
|
||||
WAMessageReaction,
|
||||
} from '@waha/structures/responses.dto';
|
||||
WAHAChatPresences,
|
||||
WAHAPresenceData,
|
||||
} from '@waha/structures/presence.dto';
|
||||
import { WAMessage, WAMessageReaction } from '@waha/structures/responses.dto';
|
||||
import { BrowserTraceQuery } from '@waha/structures/server.debug.dto';
|
||||
import { MeInfo } from '@waha/structures/sessions.dto';
|
||||
import { StatusRequest, TextStatus } from '@waha/structures/status.dto';
|
||||
import {
|
||||
EnginePayload,
|
||||
WAMessageAckBody,
|
||||
WAMessageEditedBody,
|
||||
WAMessageRevokedBody,
|
||||
} from '@waha/structures/webhooks.dto';
|
||||
import { PaginatorInMemory } from '@waha/utils/Paginator';
|
||||
import { sleep, waitUntil } from '@waha/utils/promiseTimeout';
|
||||
import { SingleDelayedJobRunner } from '@waha/utils/SingleDelayedJobRunner';
|
||||
import { TmpDir } from '@waha/utils/tmpdir';
|
||||
import * as lodash from 'lodash';
|
||||
import * as path from 'path';
|
||||
import { ProtocolError } from 'puppeteer';
|
||||
import { filter, fromEvent, merge, mergeMap, Observable } from 'rxjs';
|
||||
import { filter, fromEvent, merge, mergeMap, Observable, share } from 'rxjs';
|
||||
import { map } from 'rxjs/operators';
|
||||
import {
|
||||
AuthStrategy,
|
||||
@@ -127,13 +137,14 @@ import {
|
||||
Label as WEBJSLabel,
|
||||
Location,
|
||||
Message,
|
||||
MessageAck,
|
||||
MessageMedia,
|
||||
Reaction,
|
||||
WAState,
|
||||
} from 'whatsapp-web.js';
|
||||
import { Message as MessageInstance } from 'whatsapp-web.js/src/structures';
|
||||
|
||||
import { WAJSPresenceChatStateType, WebJSPresence } from './types';
|
||||
|
||||
export interface WebJSConfig {
|
||||
webVersion?: string;
|
||||
cacheType: 'local' | 'none';
|
||||
@@ -186,13 +197,19 @@ export class WhatsappSessionWebJSCore extends WhatsappSession {
|
||||
if (cacheType === 'local') {
|
||||
this.logger.info(`Using web version: '${webVersion}'`);
|
||||
}
|
||||
const args = this.getBrowserArgsForPuppeteer();
|
||||
// add at the start
|
||||
args.unshift(`--a-waha-timestamp=${new Date()}`);
|
||||
args.unshift(`--a-waha-session=${this.name}`);
|
||||
return {
|
||||
puppeteer: {
|
||||
headless: true,
|
||||
executablePath: this.getBrowserExecutablePath(),
|
||||
args: this.getBrowserArgsForPuppeteer(),
|
||||
args: args,
|
||||
dumpio: this.isDebugEnabled(),
|
||||
},
|
||||
userAgent:
|
||||
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36',
|
||||
webVersion: webVersion,
|
||||
webVersionCache: {
|
||||
type: cacheType,
|
||||
@@ -260,7 +277,11 @@ export class WhatsappSessionWebJSCore extends WhatsappSession {
|
||||
.then(() => {
|
||||
// Listen for browser disconnected event
|
||||
this.whatsapp.pupBrowser.on('disconnected', () => {
|
||||
this.logger.error('The browser has been disconnected');
|
||||
if (this.shouldRestart) {
|
||||
this.logger.error('The browser has been disconnected');
|
||||
} else {
|
||||
this.logger.info('The browser has been disconnected');
|
||||
}
|
||||
this.failed();
|
||||
});
|
||||
|
||||
@@ -425,6 +446,22 @@ export class WhatsappSessionWebJSCore extends WhatsappSession {
|
||||
this.logger.info(`Session '${this.name}' is ready!`);
|
||||
});
|
||||
|
||||
//
|
||||
// Temp fix for hiding "Fresh look" modal
|
||||
// https://github.com/devlikeapro/waha/issues/987
|
||||
//
|
||||
this.whatsapp.on(Events.READY, async () => {
|
||||
try {
|
||||
const hidden = await this.whatsapp.hideUXFreshLook();
|
||||
if (hidden) {
|
||||
this.logger.info('"Fresh look" modal has been hidden');
|
||||
}
|
||||
} catch (err) {
|
||||
this.logger.warn('Failed to hide "Fresh look" modal');
|
||||
this.logger.warn(err, err.stack);
|
||||
}
|
||||
});
|
||||
|
||||
this.whatsapp.on(Events.AUTHENTICATED, (args) => {
|
||||
this.qr.save('');
|
||||
this.logger.info({ args: args }, `Session has been authenticated!`);
|
||||
@@ -485,6 +522,23 @@ export class WhatsappSessionWebJSCore extends WhatsappSession {
|
||||
/**
|
||||
* START - Methods for API
|
||||
*/
|
||||
public async browserTrace(query: BrowserTraceQuery): Promise<string> {
|
||||
const tmpdir = new TmpDir(
|
||||
this.logger,
|
||||
`waha-browser-trace-${this.name}-`,
|
||||
(10 * query.seconds + 120) * 1000,
|
||||
);
|
||||
const page = this.whatsapp.pupPage;
|
||||
return await tmpdir.use(async (dir) => {
|
||||
this.logger.info({ query }, `Starting browser tracing...`);
|
||||
const filepath = path.join(dir, 'trace.json');
|
||||
await page.tracing.start({ path: filepath });
|
||||
await sleep(query.seconds * 1000);
|
||||
await page.tracing.stop();
|
||||
this.logger.info(`Browser tracing finished, saved to ${filepath}`);
|
||||
return filepath;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Auth methods
|
||||
@@ -677,7 +731,7 @@ export class WhatsappSessionWebJSCore extends WhatsappSession {
|
||||
/**
|
||||
* Chats methods
|
||||
*/
|
||||
getChats(pagination: PaginationParams) {
|
||||
getChats(pagination: PaginationParams, filter: OverviewFilter | null = null) {
|
||||
switch (pagination.sortBy) {
|
||||
case ChatSortField.ID:
|
||||
pagination.sortBy = 'id._serialized';
|
||||
@@ -686,18 +740,19 @@ export class WhatsappSessionWebJSCore extends WhatsappSession {
|
||||
pagination.sortBy = 't';
|
||||
break;
|
||||
}
|
||||
return this.whatsapp.getChats(pagination);
|
||||
return this.whatsapp.getChats(pagination, filter);
|
||||
}
|
||||
|
||||
public async getChatsOverview(
|
||||
pagination: PaginationParams,
|
||||
filter?: OverviewFilter,
|
||||
): Promise<ChatSummary[]> {
|
||||
pagination = {
|
||||
...pagination,
|
||||
sortBy: ChatSortField.CONVERSATION_TIMESTAMP,
|
||||
sortOrder: SortOrder.DESC,
|
||||
};
|
||||
const chats = await this.getChats(pagination);
|
||||
const chats = await this.getChats(pagination, filter);
|
||||
|
||||
const promises = [];
|
||||
for (const chat of chats) {
|
||||
@@ -926,6 +981,38 @@ export class WhatsappSessionWebJSCore extends WhatsappSession {
|
||||
await contact.unblock();
|
||||
}
|
||||
|
||||
/**
|
||||
* Lid to Phone Number methods
|
||||
*/
|
||||
public async getAllLids(
|
||||
pagination: PaginationParams,
|
||||
): Promise<Array<LidToPhoneNumber>> {
|
||||
return this.whatsapp.getAllLids(pagination);
|
||||
}
|
||||
|
||||
public async getLidsCount(): Promise<number> {
|
||||
return this.whatsapp.getLidsCount();
|
||||
}
|
||||
|
||||
public async findPNByLid(lid: string): Promise<LidToPhoneNumber> {
|
||||
const phoneNumber = await this.whatsapp.findPNByLid(lid);
|
||||
return {
|
||||
lid: lid,
|
||||
pn: phoneNumber,
|
||||
};
|
||||
}
|
||||
|
||||
public async findLIDByPhoneNumber(
|
||||
phoneNumber: string,
|
||||
): Promise<LidToPhoneNumber> {
|
||||
const pn = toCusFormat(phoneNumber);
|
||||
const lid = await this.whatsapp.findLIDByPhoneNumber(pn);
|
||||
return {
|
||||
lid: lid,
|
||||
pn: pn,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Group methods
|
||||
*/
|
||||
@@ -1229,6 +1316,52 @@ export class WhatsappSessionWebJSCore extends WhatsappSession {
|
||||
}
|
||||
}
|
||||
|
||||
public getPresences(): Promise<WAHAChatPresences[]> {
|
||||
throw new NotImplementedByEngineError();
|
||||
}
|
||||
|
||||
public async getPresence(id: string): Promise<WAHAChatPresences> {
|
||||
const chatId = toCusFormat(id);
|
||||
const presences = await this.whatsapp.getPresence(chatId);
|
||||
return this.toWahaPresences(chatId, presences);
|
||||
}
|
||||
|
||||
public async subscribePresence(id: string): Promise<any> {
|
||||
const chatId = toCusFormat(id);
|
||||
await this.whatsapp.subscribePresence(chatId);
|
||||
}
|
||||
|
||||
private toWahaPresences(
|
||||
chatId: string,
|
||||
data: WebJSPresence[],
|
||||
): WAHAChatPresences {
|
||||
const presences: WAHAPresenceData[] = data.map((presence) => {
|
||||
let status: WAHAPresenceStatus = WAHAPresenceStatus.OFFLINE;
|
||||
switch (presence.state) {
|
||||
case WAJSPresenceChatStateType.AVAILABLE:
|
||||
status = WAHAPresenceStatus.ONLINE;
|
||||
break;
|
||||
case WAJSPresenceChatStateType.UNAVAILABLE:
|
||||
status = WAHAPresenceStatus.OFFLINE;
|
||||
break;
|
||||
case WAJSPresenceChatStateType.TYPING:
|
||||
status = WAHAPresenceStatus.TYPING;
|
||||
break;
|
||||
case WAJSPresenceChatStateType.RECORDING_AUDIO:
|
||||
status = WAHAPresenceStatus.RECORDING;
|
||||
}
|
||||
return {
|
||||
participant: presence.participant,
|
||||
lastSeen: presence.lastSeen || null,
|
||||
lastKnownPresence: status,
|
||||
};
|
||||
});
|
||||
return {
|
||||
id: toCusFormat(chatId),
|
||||
presences: presences,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Status methods
|
||||
*/
|
||||
@@ -1312,9 +1445,12 @@ export class WhatsappSessionWebJSCore extends WhatsappSession {
|
||||
const beforeMessage = event.before
|
||||
? this.toWAMessage(event.before)
|
||||
: null;
|
||||
// Extract the revoked message ID from the protocolMessageKey.id field
|
||||
const revokedMessageId = afterMessage?._data?.protocolMessageKey?.id;
|
||||
return {
|
||||
after: afterMessage,
|
||||
before: beforeMessage,
|
||||
revokedMessageId: revokedMessageId,
|
||||
};
|
||||
}),
|
||||
);
|
||||
@@ -1326,6 +1462,26 @@ export class WhatsappSessionWebJSCore extends WhatsappSession {
|
||||
);
|
||||
this.events2.get(WAHAEvents.MESSAGE_REACTION).switch(messagesReaction$);
|
||||
|
||||
const messageEdit$ = fromEvent(
|
||||
this.whatsapp,
|
||||
Events.MESSAGE_EDIT,
|
||||
(message, newBody, prevBody) => {
|
||||
return { message, newBody, prevBody };
|
||||
},
|
||||
);
|
||||
const messagesEdit$ = messageEdit$.pipe(
|
||||
map((event): WAMessageEditedBody => {
|
||||
const message = this.toWAMessage(event.message);
|
||||
return {
|
||||
...message,
|
||||
body: event.newBody,
|
||||
editedMessageId: message._data?.id?.id,
|
||||
_data: event,
|
||||
};
|
||||
}),
|
||||
);
|
||||
this.events2.get(WAHAEvents.MESSAGE_EDITED).switch(messagesEdit$);
|
||||
|
||||
const messageAckWEBJS$ = fromEvent(
|
||||
this.whatsapp,
|
||||
Events.MESSAGE_ACK,
|
||||
@@ -1358,6 +1514,22 @@ export class WhatsappSessionWebJSCore extends WhatsappSession {
|
||||
const stateChanged$ = fromEvent(this.whatsapp, Events.STATE_CHANGED);
|
||||
this.events2.get(WAHAEvents.STATE_CHANGE).switch(stateChanged$);
|
||||
|
||||
//
|
||||
// Presence
|
||||
//
|
||||
const tagPresenceNode$ = fromEvent(this.whatsapp, Events.TAG_PRESENCE);
|
||||
const presences$ = tagPresenceNode$.pipe(
|
||||
map(TagPresenceToPresence),
|
||||
filter(Boolean),
|
||||
);
|
||||
const tagChatstateNode$ = fromEvent(this.whatsapp, 'tag:chatstate');
|
||||
const chatstatePresences$ = tagChatstateNode$.pipe(
|
||||
map(TagChatstateToPresence),
|
||||
filter(Boolean),
|
||||
);
|
||||
const presenceUpdate$ = merge(presences$, chatstatePresences$);
|
||||
this.events2.get(WAHAEvents.PRESENCE_UPDATE).switch(presenceUpdate$);
|
||||
|
||||
//
|
||||
// Groups
|
||||
//
|
||||
@@ -1555,10 +1727,12 @@ export class WhatsappSessionWebJSCore extends WhatsappSession {
|
||||
}
|
||||
|
||||
public async getEngineInfo() {
|
||||
// Add 1 seconds timeout
|
||||
if (!this.whatsapp || !this.whatsapp.pupPage) {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
WWebVersion: await this.whatsapp?.getWWebVersion(),
|
||||
state: await this.whatsapp?.getState(),
|
||||
WWebVersion: await this.whatsapp.getWWebVersion(),
|
||||
state: await this.whatsapp.getState(),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
export enum WAJSPresenceChatStateType {
|
||||
AVAILABLE = 'available', // User is online
|
||||
UNAVAILABLE = 'unavailable', // User went offline (may include `t`)
|
||||
TYPING = 'typing', // User is typing text
|
||||
RECORDING_AUDIO = 'recording_audio', // User is recording a voice message
|
||||
}
|
||||
|
||||
export interface WebJSPresence {
|
||||
participant: string;
|
||||
lastSeen?: number;
|
||||
state: WAJSPresenceChatStateType;
|
||||
}
|
||||
@@ -1,8 +1,11 @@
|
||||
import { UnprocessableEntityException } from '@nestjs/common';
|
||||
import {
|
||||
NotImplementedException,
|
||||
UnprocessableEntityException,
|
||||
} from '@nestjs/common';
|
||||
|
||||
export const DOCS_URL = 'https://waha.devlike.pro/';
|
||||
|
||||
export class NotImplementedByEngineError extends UnprocessableEntityException {
|
||||
export class NotImplementedByEngineError extends NotImplementedException {
|
||||
constructor(msg = '') {
|
||||
let error = 'The method is not implemented by the engine.';
|
||||
if (msg) {
|
||||
|
||||
+10
-7
@@ -1,20 +1,23 @@
|
||||
export function toVcard(data): string {
|
||||
import { Contact, VCardContact } from '@waha/structures/chatting.dto';
|
||||
|
||||
export function toVcard(data: Contact | VCardContact): string {
|
||||
if (data.vcard) {
|
||||
return data.vcard;
|
||||
}
|
||||
const contact: Contact = data as any;
|
||||
const parts = [];
|
||||
parts.push('BEGIN:VCARD');
|
||||
parts.push('VERSION:3.0');
|
||||
parts.push(`FN:${data.fullName}`);
|
||||
if (data.organization) {
|
||||
parts.push(`ORG:${data.organization};`);
|
||||
parts.push(`FN:${contact.fullName}`);
|
||||
if (contact.organization) {
|
||||
parts.push(`ORG:${contact.organization};`);
|
||||
}
|
||||
if (data.whatsappId) {
|
||||
if (contact.whatsappId) {
|
||||
parts.push(
|
||||
`TEL;type=CELL;type=VOICE;waid=${data.whatsappId}:${data.phoneNumber}`,
|
||||
`TEL;type=CELL;type=VOICE;waid=${contact.whatsappId}:${contact.phoneNumber}`,
|
||||
);
|
||||
} else {
|
||||
parts.push(`TEL;type=CELL;type=VOICE:${data.phoneNumber}`);
|
||||
parts.push(`TEL;type=CELL;type=VOICE:${contact.phoneNumber}`);
|
||||
}
|
||||
parts.push('END:VCARD');
|
||||
return parts.join('\n');
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
import { AvailableInPlusVersion } from '../exceptions';
|
||||
|
||||
export interface IMediaConverter {
|
||||
voice(content: Buffer): Promise<Buffer>;
|
||||
video(content: Buffer): Promise<Buffer>;
|
||||
}
|
||||
|
||||
export class CoreMediaConverter implements IMediaConverter {
|
||||
video(content: Buffer): Promise<Buffer> {
|
||||
throw new AvailableInPlusVersion();
|
||||
}
|
||||
|
||||
voice(content: Buffer): Promise<Buffer> {
|
||||
throw new AvailableInPlusVersion();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
export enum WAMimeType {
|
||||
VOICE = 'audio/ogg; codecs=opus',
|
||||
VIDEO = 'video/mp4',
|
||||
}
|
||||
@@ -4,8 +4,8 @@ import * as fsp from 'fs/promises';
|
||||
import * as path from 'path';
|
||||
import { Logger } from 'pino';
|
||||
import fs = require('fs');
|
||||
import del = require('del');
|
||||
import { fileExists } from '@waha/utils/files';
|
||||
import { deleteAsync } from 'del';
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
const writeFileAtomic = require('write-file-atomic');
|
||||
@@ -59,7 +59,7 @@ export class MediaLocalStorage implements IMediaStorage {
|
||||
}
|
||||
|
||||
if (fs.existsSync(this.filesFolder)) {
|
||||
del([`${this.filesFolder}/*`], { force: true }).then((paths) => {
|
||||
deleteAsync([`${this.filesFolder}/*`], { force: true }).then((paths) => {
|
||||
if (paths.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import Knex from 'knex';
|
||||
|
||||
import { DataStore } from '../abc/DataStore';
|
||||
|
||||
export abstract class LocalStore extends DataStore {
|
||||
@@ -23,5 +25,5 @@ export abstract class LocalStore extends DataStore {
|
||||
*/
|
||||
abstract getFilePath(session: string, file: string): string;
|
||||
|
||||
abstract getWAHADatabase(): any;
|
||||
abstract getWAHADatabase(): Knex.Knex;
|
||||
}
|
||||
@@ -1,19 +1,16 @@
|
||||
import * as crypto from 'crypto';
|
||||
import { safeJoin } from '@waha/utils/files';
|
||||
import * as fs from 'fs/promises';
|
||||
import * as os from 'os';
|
||||
import Knex from 'knex';
|
||||
import * as path from 'path';
|
||||
|
||||
import { LocalStore } from './LocalStore';
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
const Database = require('better-sqlite3');
|
||||
|
||||
export class LocalStoreCore extends LocalStore {
|
||||
protected readonly baseDirectory: string =
|
||||
process.env.WAHA_LOCAL_STORE_BASE_DIR || './.sessions';
|
||||
|
||||
private readonly engine: string;
|
||||
private db: any;
|
||||
private knex: Knex.Knex;
|
||||
|
||||
constructor(engine: string) {
|
||||
super();
|
||||
@@ -22,6 +19,11 @@ export class LocalStoreCore extends LocalStore {
|
||||
|
||||
async init(sessionName?: string) {
|
||||
await fs.mkdir(this.getEngineDirectory(), { recursive: true });
|
||||
if (!this.knex) {
|
||||
this.knex = this.buildKnex();
|
||||
await this.knex.raw('PRAGMA journal_mode = WAL;');
|
||||
await this.knex.raw('PRAGMA foreign_keys = ON;');
|
||||
}
|
||||
if (sessionName) {
|
||||
await fs.mkdir(this.getSessionDirectory(sessionName), {
|
||||
recursive: true,
|
||||
@@ -33,14 +35,14 @@ export class LocalStoreCore extends LocalStore {
|
||||
* Get the directory where all the engines and sessions are stored
|
||||
*/
|
||||
getBaseDirectory() {
|
||||
return path.join(this.baseDirectory);
|
||||
return path.resolve(this.baseDirectory);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the directory where the engine sessions are stored
|
||||
*/
|
||||
getEngineDirectory() {
|
||||
return path.join(this.baseDirectory, this.engine);
|
||||
return safeJoin(this.baseDirectory, this.engine);
|
||||
}
|
||||
|
||||
getSessionDirectory(name: string): string {
|
||||
@@ -48,24 +50,31 @@ export class LocalStoreCore extends LocalStore {
|
||||
}
|
||||
|
||||
getFilePath(session: string, file: string): string {
|
||||
return path.join(this.getSessionDirectory(session), file);
|
||||
return safeJoin(this.getSessionDirectory(session), file);
|
||||
}
|
||||
|
||||
protected getDirectoryPath(name: string): string {
|
||||
return path.join(this.getEngineDirectory(), name);
|
||||
return safeJoin(this.getEngineDirectory(), name);
|
||||
}
|
||||
|
||||
getWAHADatabase(): any {
|
||||
if (!this.db) {
|
||||
const engineDir = this.getEngineDirectory();
|
||||
const database = path.join(engineDir, 'waha.sqlite3');
|
||||
this.db = new Database(database);
|
||||
this.db.pragma('journal_mode = WAL;');
|
||||
getWAHADatabase(): Knex.Knex {
|
||||
if (!this.knex) {
|
||||
throw new Error('Knex is not initialized, call LocalStore.init() first');
|
||||
}
|
||||
return this.db;
|
||||
return this.knex;
|
||||
}
|
||||
|
||||
buildKnex(): Knex.Knex {
|
||||
const engineDir = this.getEngineDirectory();
|
||||
const database = path.join(engineDir, 'waha.sqlite3');
|
||||
return Knex({
|
||||
client: 'better-sqlite3',
|
||||
connection: { filename: database },
|
||||
useNullAsDefault: true,
|
||||
});
|
||||
}
|
||||
|
||||
async close() {
|
||||
this.db?.close();
|
||||
await this.knex.destroy();
|
||||
}
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
import Knex from 'knex';
|
||||
|
||||
export interface ISQLEngine {
|
||||
/**
|
||||
* Run a query that does not return any data
|
||||
*/
|
||||
run(query: Knex.QueryBuilder): Promise<void>;
|
||||
|
||||
/**
|
||||
* Get a single row from the database
|
||||
*/
|
||||
get(query: Knex.QueryBuilder): Promise<any | null>;
|
||||
|
||||
/**
|
||||
* Get all rows from the database
|
||||
*/
|
||||
all(query: Knex.QueryBuilder): Promise<any[]>;
|
||||
|
||||
/**
|
||||
* Run a raw SQL query with bindings
|
||||
*/
|
||||
raw(sql: string, bindings: any[]): Promise<void>;
|
||||
|
||||
/**
|
||||
* Execute a raw SQL query
|
||||
*/
|
||||
exec(sql: string): Promise<void>;
|
||||
}
|
||||
@@ -1,9 +1,8 @@
|
||||
import { Field, Schema } from '@waha/core/storage/Schema';
|
||||
import { IJsonQuery } from '@waha/core/storage/sql/IJsonQuery';
|
||||
import { ISQLEngine } from '@waha/core/storage/sql/ISQLEngine';
|
||||
import { PaginationParams } from '@waha/structures/pagination.dto';
|
||||
import { KnexPaginator } from '@waha/utils/Paginator';
|
||||
import Knex from 'knex';
|
||||
import { Knex } from 'knex';
|
||||
import * as lodash from 'lodash';
|
||||
|
||||
export type Migration = string;
|
||||
@@ -26,10 +25,7 @@ export class SqlKVRepository<Entity> {
|
||||
return [];
|
||||
}
|
||||
|
||||
constructor(
|
||||
private engine: ISQLEngine,
|
||||
protected knex: Knex.Knex,
|
||||
) {}
|
||||
constructor(protected knex: Knex) {}
|
||||
|
||||
get columns(): Field[] {
|
||||
return this.schema.columns;
|
||||
@@ -49,7 +45,7 @@ export class SqlKVRepository<Entity> {
|
||||
|
||||
protected async applyMigrations() {
|
||||
for (const migration of this.migrations) {
|
||||
await this.engine.exec(migration);
|
||||
await this.knex.raw(migration);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -79,15 +75,13 @@ export class SqlKVRepository<Entity> {
|
||||
}
|
||||
}
|
||||
|
||||
private async upsertBatch(entities: Entity[]): Promise<void> {
|
||||
protected async upsertBatch(entities: Entity[]): Promise<void> {
|
||||
const all = entities.map((entity) => this.dump(entity));
|
||||
// make it unique by .id
|
||||
const data = lodash.uniqBy(all, (d: any) => d.id);
|
||||
if (data.length != all.length) {
|
||||
console.warn(
|
||||
`WARNING - Duplicated entities for upsert batch: ${JSON.stringify(
|
||||
entities,
|
||||
)}`,
|
||||
`WARNING - Duplicated entities for upsert batch: all=${all.length}, data=${data.length}`,
|
||||
);
|
||||
}
|
||||
const columns = this.columns.map((c) => `"${c.fieldName}"`);
|
||||
@@ -95,11 +89,11 @@ export class SqlKVRepository<Entity> {
|
||||
const sql = `INSERT INTO "${this.table}" (${columns.join(', ')})
|
||||
VALUES ${data
|
||||
.map(() => `(${columns.map(() => '?').join(', ')})`)
|
||||
.join(', ')}
|
||||
ON CONFLICT(id) DO UPDATE
|
||||
SET ${columns
|
||||
.map((column) => `${column} = excluded.${column}`)
|
||||
.join(', ')}`;
|
||||
.join(', ')} ON CONFLICT(id) DO
|
||||
UPDATE
|
||||
SET ${columns
|
||||
.map((column) => `${column} = excluded.${column}`)
|
||||
.join(', ')}`;
|
||||
try {
|
||||
await this.raw(sql, values);
|
||||
} catch (err) {
|
||||
@@ -119,6 +113,15 @@ export class SqlKVRepository<Entity> {
|
||||
return this.all(query);
|
||||
}
|
||||
|
||||
async getCount(): Promise<number> {
|
||||
const query = this.select().count({ count: 'id' });
|
||||
const row = await query.first();
|
||||
if (!row) {
|
||||
return 0;
|
||||
}
|
||||
return parseInt(row.count, 10);
|
||||
}
|
||||
|
||||
async getAllByIds(ids: string[]) {
|
||||
const entitiesMap = await this.getEntitiesByIds(ids);
|
||||
return Array.from(entitiesMap.values()).filter(
|
||||
@@ -131,7 +134,7 @@ export class SqlKVRepository<Entity> {
|
||||
return new Map();
|
||||
}
|
||||
|
||||
const rows = await this.engine.all(this.select().whereIn('id', ids));
|
||||
const rows = await this.select().whereIn('id', ids);
|
||||
const entitiesMap = new Map<string, Entity | null>();
|
||||
|
||||
// Initialize a map with null values for all requested IDs
|
||||
@@ -184,15 +187,15 @@ export class SqlKVRepository<Entity> {
|
||||
* SQL Implementation details
|
||||
*/
|
||||
public async raw(sql: string, bindings: any[]): Promise<void> {
|
||||
await this.engine.raw(sql, bindings);
|
||||
await this.knex.raw(sql, bindings);
|
||||
}
|
||||
|
||||
protected async run(query: Knex.QueryBuilder): Promise<void> {
|
||||
await this.engine.run(query);
|
||||
await query;
|
||||
}
|
||||
|
||||
protected async get(query: Knex.QueryBuilder): Promise<Entity | null> {
|
||||
const row = await this.engine.get(query);
|
||||
const row = await query.first();
|
||||
if (!row) {
|
||||
return null;
|
||||
}
|
||||
@@ -200,7 +203,7 @@ export class SqlKVRepository<Entity> {
|
||||
}
|
||||
|
||||
public async all(query: Knex.QueryBuilder): Promise<Entity[]> {
|
||||
const rows = await this.engine.all(query);
|
||||
const rows = await query;
|
||||
return rows.map((row) => this.parse(row));
|
||||
}
|
||||
|
||||
|
||||
@@ -1,38 +0,0 @@
|
||||
import { ISQLEngine } from '@waha/core/storage/sql/ISQLEngine';
|
||||
import { Database } from 'better-sqlite3';
|
||||
import { Knex } from 'knex';
|
||||
|
||||
export class Sqlite3Engine implements ISQLEngine {
|
||||
constructor(protected db: Database) {}
|
||||
|
||||
async all(query: Knex.QueryBuilder): Promise<any[]> {
|
||||
const sql = query.toSQL().sql;
|
||||
const bind = query.toSQL().bindings;
|
||||
return this.db.prepare(sql).all(bind);
|
||||
}
|
||||
|
||||
async get(query: Knex.QueryBuilder): Promise<any | null> {
|
||||
const sql = query.toSQL().sql;
|
||||
const bind = query.toSQL().bindings;
|
||||
const row: any = this.db.prepare(sql).get(bind);
|
||||
if (!row) {
|
||||
return null;
|
||||
}
|
||||
return row;
|
||||
}
|
||||
|
||||
async run(query: Knex.QueryBuilder): Promise<void> {
|
||||
const sql = query.toSQL().sql;
|
||||
const bind = query.toSQL().bindings;
|
||||
this.db.prepare(sql).run(bind);
|
||||
return;
|
||||
}
|
||||
|
||||
async raw(sql: string, bindings: any[]) {
|
||||
this.db.prepare(sql).run(bindings);
|
||||
}
|
||||
|
||||
async exec(sql): Promise<void> {
|
||||
this.db.exec(sql);
|
||||
}
|
||||
}
|
||||
@@ -1,21 +1,24 @@
|
||||
import { SqlKVRepository } from '@waha/core/storage/sql/SqlKVRepository';
|
||||
import { Sqlite3Engine } from '@waha/core/storage/sqlite3/Sqlite3Engine';
|
||||
import { Sqlite3JsonQuery } from '@waha/core/storage/sqlite3/Sqlite3JsonQuery';
|
||||
import { Database } from 'better-sqlite3';
|
||||
import { sleep } from '@waha/utils/promiseTimeout';
|
||||
import Knex from 'knex';
|
||||
|
||||
/**
|
||||
* Key value repository with extra metadata
|
||||
*/
|
||||
export class Sqlite3KVRepository<Entity> extends SqlKVRepository<Entity> {
|
||||
protected db: Database;
|
||||
protected knex: Knex.Knex;
|
||||
protected jsonQuery = new Sqlite3JsonQuery();
|
||||
|
||||
constructor(db: Database) {
|
||||
// sqlite does not support inserting default values. Set the `useNullAsDefault` flag to hide this warning. (see docs https://knexjs.org/guide/query-builder.html#insert).
|
||||
const knex = Knex({ client: 'better-sqlite3', useNullAsDefault: true });
|
||||
const engine = new Sqlite3Engine(db);
|
||||
super(engine, knex);
|
||||
this.db = db;
|
||||
constructor(knex: Knex.Knex) {
|
||||
super(knex);
|
||||
}
|
||||
|
||||
protected async upsertBatch(entities: Entity[]): Promise<void> {
|
||||
await super.upsertBatch(entities);
|
||||
// Give some time to the Node.js loop because we're using sync better-sqlite
|
||||
if (entities.length >= this.UPSERT_BATCH_SIZE) {
|
||||
await sleep(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -23,8 +23,8 @@ export class Sqlite3SessionMeRepository
|
||||
}
|
||||
|
||||
constructor(store: LocalStore) {
|
||||
const db = store.getWAHADatabase();
|
||||
super(db);
|
||||
const knex = store.getWAHADatabase();
|
||||
super(knex);
|
||||
}
|
||||
|
||||
upsertMe(sessionName: string, me: MeInfo): Promise<void> {
|
||||
@@ -41,7 +41,7 @@ export class Sqlite3SessionMeRepository
|
||||
}
|
||||
|
||||
protected async validateSchema() {
|
||||
const validation = new Sqlite3SchemaValidation(this.schema, this.db);
|
||||
validation.validate();
|
||||
const validation = new Sqlite3SchemaValidation(this.schema, this.knex);
|
||||
await validation.validate();
|
||||
}
|
||||
}
|
||||
@@ -23,8 +23,8 @@ export class Sqlite3SessionWorkerRepository
|
||||
}
|
||||
|
||||
constructor(store: LocalStore) {
|
||||
const db = store.getWAHADatabase();
|
||||
super(db);
|
||||
const knex = store.getWAHADatabase();
|
||||
super(knex);
|
||||
}
|
||||
|
||||
assign(session: string, worker: string): Promise<void> {
|
||||
@@ -45,7 +45,7 @@ export class Sqlite3SessionWorkerRepository
|
||||
}
|
||||
|
||||
protected async validateSchema() {
|
||||
const validation = new Sqlite3SchemaValidation(this.schema, this.db);
|
||||
validation.validate();
|
||||
const validation = new Sqlite3SchemaValidation(this.schema, this.knex);
|
||||
await validation.validate();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
import { EventResponseType } from '@waha/structures/events.dto';
|
||||
|
||||
export function ParseEventResponseType(response: number) {
|
||||
switch (response) {
|
||||
case 0:
|
||||
return EventResponseType.UNKNOWN;
|
||||
case 1:
|
||||
return EventResponseType.GOING;
|
||||
case 2:
|
||||
return EventResponseType.NOT_GOING;
|
||||
case 3:
|
||||
return EventResponseType.MAYBE;
|
||||
default:
|
||||
return EventResponseType.UNKNOWN;
|
||||
}
|
||||
}
|
||||
+8
-15
@@ -47,31 +47,24 @@ process.on('SIGTERM', () => {
|
||||
logger.info('SIGTERM received');
|
||||
});
|
||||
|
||||
async function loadModules(): Promise<
|
||||
[typeof AppModuleCore, typeof SwaggerConfiguratorCore]
|
||||
> {
|
||||
async function loadModules(): Promise<typeof AppModuleCore> {
|
||||
const version = getWAHAVersion();
|
||||
|
||||
if (version === WAHAVersion.CORE) {
|
||||
const { AppModuleCore } = await import('./core/app.module.core');
|
||||
const { SwaggerConfiguratorCore } = await import(
|
||||
'./core/SwaggerConfiguratorCore'
|
||||
);
|
||||
return [AppModuleCore, SwaggerConfiguratorCore];
|
||||
return AppModuleCore;
|
||||
}
|
||||
// Ignore if it's core version - there's no plus module
|
||||
// Ignore if it's a core version - there's no plus module
|
||||
// @ts-ignore
|
||||
const { AppModulePlus } = await import('./plus/app.module.plus');
|
||||
// @ts-ignore
|
||||
const { SwaggerConfiguratorPlus } = await import('./plus/SwaggerConfiguratorPlus'); // prettier-ignore
|
||||
// @ts-ignore
|
||||
return [AppModulePlus, SwaggerConfiguratorPlus];
|
||||
return AppModulePlus;
|
||||
}
|
||||
|
||||
async function bootstrap() {
|
||||
const version = getWAHAVersion();
|
||||
logger.info(`WAHA (WhatsApp HTTP API) - Running ${version} version...`);
|
||||
const [AppModule, SwaggerModule] = await loadModules();
|
||||
const AppModule = await loadModules();
|
||||
const httpsOptions = AppModule.getHttpsOptions(logger);
|
||||
const app = await NestFactory.create(AppModule, {
|
||||
logger: getNestJSLogLevels(),
|
||||
@@ -81,7 +74,7 @@ async function bootstrap() {
|
||||
});
|
||||
app.useLogger(app.get(NestJSPinoLogger));
|
||||
|
||||
// Print original stack, not pino one
|
||||
// Print the original stack, not pino one
|
||||
// https://github.com/iamolegga/nestjs-pino?tab=readme-ov-file#expose-stack-trace-and-error-class-in-err-property
|
||||
app.useGlobalInterceptors(new LoggerErrorInterceptor());
|
||||
|
||||
@@ -91,13 +84,13 @@ async function bootstrap() {
|
||||
// but for now we added it ValidationPipe on Controller or endpoint level
|
||||
// app.useGlobalPipes(new ValidationPipe({ transform: true }));
|
||||
|
||||
// Allow to send big body - for images and attachments
|
||||
// Allow sending big body - for images and attachments
|
||||
app.use(json({ limit: '50mb' }));
|
||||
app.use(urlencoded({ limit: '50mb', extended: false }));
|
||||
app.useWebSocketAdapter(new WsAdapter(app));
|
||||
|
||||
// Configure swagger
|
||||
const swaggerConfigurator = new SwaggerModule(app);
|
||||
const swaggerConfigurator = new SwaggerConfiguratorCore(app);
|
||||
swaggerConfigurator.configure(WAHA_WEBHOOKS);
|
||||
|
||||
AppModule.appReady(app, logger);
|
||||
|
||||
@@ -11,7 +11,7 @@ function getRefSchemaPaths(models) {
|
||||
/**
|
||||
* Decorator to add a file accept header to the swagger documentation
|
||||
*/
|
||||
export function ApiFileAcceptHeader(...models) {
|
||||
export function ApiFileAcceptHeader(mimetype: string, ...models) {
|
||||
models = models.length ? models : [Base64File];
|
||||
return applyDecorators(
|
||||
// Add extra models, otherwise it'll give a error
|
||||
@@ -20,7 +20,7 @@ export function ApiFileAcceptHeader(...models) {
|
||||
ApiResponse({
|
||||
status: 200,
|
||||
content: {
|
||||
'image/png': {
|
||||
[mimetype]: {
|
||||
schema: {
|
||||
type: 'string',
|
||||
format: 'binary',
|
||||
|
||||
@@ -21,6 +21,11 @@ import { Base64File } from '../structures/files.dto';
|
||||
export class BufferResponseInterceptor<T>
|
||||
implements NestInterceptor<Buffer, Base64File | StreamableFile>
|
||||
{
|
||||
constructor(
|
||||
private mimetype: string,
|
||||
private filename: string = null,
|
||||
) {}
|
||||
|
||||
intercept(
|
||||
context: ExecutionContext,
|
||||
next: CallHandler,
|
||||
@@ -28,27 +33,35 @@ export class BufferResponseInterceptor<T>
|
||||
const ctx = context.switchToHttp();
|
||||
const request = ctx.getRequest<Request>();
|
||||
const response = ctx.getResponse<Response>();
|
||||
return next.handle().pipe(map(processBuffer));
|
||||
|
||||
function processBuffer(buffer: Buffer) {
|
||||
// Check buffer is Buffer
|
||||
if (!Buffer.isBuffer(buffer)) {
|
||||
return buffer;
|
||||
}
|
||||
return next
|
||||
.handle()
|
||||
.pipe(map((buffer) => this.processBuffer(buffer, request, response)));
|
||||
}
|
||||
|
||||
const accept = request.headers['accept'];
|
||||
if (accept == 'application/json') {
|
||||
return {
|
||||
mimetype: 'image/png',
|
||||
data: buffer.toString('base64'),
|
||||
};
|
||||
}
|
||||
const file = new StreamableFile(buffer);
|
||||
response.set({
|
||||
'Content-Type': 'image/png',
|
||||
'Content-Length': buffer.length,
|
||||
});
|
||||
return file;
|
||||
processBuffer(buffer: Buffer, request: Request, response: Response) {
|
||||
// Check buffer is Buffer
|
||||
if (!Buffer.isBuffer(buffer)) {
|
||||
return buffer;
|
||||
}
|
||||
|
||||
const accept = request.headers['accept'];
|
||||
if (accept == 'application/json') {
|
||||
return {
|
||||
mimetype: this.mimetype,
|
||||
data: buffer.toString('base64'),
|
||||
};
|
||||
}
|
||||
const file = new StreamableFile(buffer);
|
||||
response.set({
|
||||
'Content-Type': this.mimetype,
|
||||
'Content-Length': buffer.length,
|
||||
});
|
||||
if (this.filename) {
|
||||
response.set({
|
||||
'Content-Disposition': `attachment; filename="${this.filename}"`,
|
||||
});
|
||||
}
|
||||
return file;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
import * as fs from 'node:fs';
|
||||
|
||||
import { LoggerBuilder } from '@waha/utils/logging';
|
||||
import { Logger } from 'pino';
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
const chokidar = require('chokidar');
|
||||
|
||||
export class HttpsExpress {
|
||||
private readonly keyPath: string;
|
||||
private readonly certPath: string;
|
||||
private readonly caPath: string;
|
||||
|
||||
constructor(private logger: Logger) {
|
||||
//
|
||||
// Let's encrypt certificates default paths
|
||||
// cert.pem chain.pem fullchain.pem privkey.pem
|
||||
//
|
||||
this.keyPath = process.env.WAHA_HTTPS_PATH_KEY || './.secrets/privkey.pem';
|
||||
this.certPath = process.env.WAHA_HTTPS_PATH_CERT || './.secrets/cert.pem';
|
||||
this.caPath = process.env.WAHA_HTTPS_PATH_CA;
|
||||
if (this.caPath == null) {
|
||||
this.caPath = './.secrets/chain.pem';
|
||||
}
|
||||
}
|
||||
|
||||
readSync() {
|
||||
this.logger.info('Reading HTTPS certificates...');
|
||||
this.logger.info('HTTPS Key Path:', this.keyPath);
|
||||
const key = fs.readFileSync(this.keyPath);
|
||||
|
||||
this.logger.info('HTTPS Cert Path:', this.certPath);
|
||||
const cert = fs.readFileSync(this.certPath);
|
||||
|
||||
this.logger.info('HTTPS CA Path:', this.caPath);
|
||||
const ca = this.caPath ? fs.readFileSync(this.caPath) : undefined;
|
||||
|
||||
this.logger.info('HTTPS certificates read successfully');
|
||||
return { key: key, cert: cert, ca: ca };
|
||||
}
|
||||
|
||||
/**
|
||||
* https://stackoverflow.com/a/74076392
|
||||
*/
|
||||
watchCertChanges(httpd) {
|
||||
let waitForCertAndFullChainToGetUpdatedTooTimeout: any;
|
||||
const paths = [this.keyPath, this.certPath, this.caPath].filter(
|
||||
(path) => !!path,
|
||||
);
|
||||
const watcher = chokidar.watch(paths, {
|
||||
followSymlinks: false,
|
||||
persistent: true,
|
||||
ignoreInitial: true,
|
||||
disableGlobbing: true,
|
||||
});
|
||||
// IDK why, but it has few bugs:
|
||||
// 1. It issues 'add' event at the start, even tho ignoreInitial is set to true
|
||||
// 2. It issues additional 'add' for the same file, but without full path
|
||||
watcher.on('all', (eventName, path, stats) => {
|
||||
this.logger.info(`HTTPS file '${path}' has been '${eventName}'...`);
|
||||
clearTimeout(waitForCertAndFullChainToGetUpdatedTooTimeout);
|
||||
waitForCertAndFullChainToGetUpdatedTooTimeout = setTimeout(() => {
|
||||
this.logger.info('Updating HTTPS configuration...');
|
||||
httpd.setSecureContext(this.readSync());
|
||||
}, 1000);
|
||||
});
|
||||
process.on('SIGTERM', () => {
|
||||
this.logger.info('SIGTERM received, closing HTTP file watchers');
|
||||
clearTimeout(waitForCertAndFullChainToGetUpdatedTooTimeout);
|
||||
watcher.close();
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -7,8 +7,17 @@ import {
|
||||
PaginationParams,
|
||||
} from '@waha/structures/pagination.dto';
|
||||
import { ChatIdProperty } from '@waha/structures/properties.dto';
|
||||
import { SessionConfig } from '@waha/structures/sessions.dto';
|
||||
import { Transform, Type } from 'class-transformer';
|
||||
import { IsBoolean, IsEnum, IsNumber, IsOptional } from 'class-validator';
|
||||
import {
|
||||
IsArray,
|
||||
IsBoolean,
|
||||
IsEnum,
|
||||
IsNumber,
|
||||
IsOptional,
|
||||
IsString,
|
||||
ValidateNested,
|
||||
} from 'class-validator';
|
||||
|
||||
/**
|
||||
* Queries
|
||||
@@ -187,6 +196,29 @@ export class OverviewPaginationParams extends LimitOffsetParams {
|
||||
limit?: number = 20;
|
||||
}
|
||||
|
||||
export class OverviewFilter {
|
||||
@IsOptional()
|
||||
@IsArray()
|
||||
@IsString({ each: true })
|
||||
@Transform(({ value }) => (Array.isArray(value) ? value : [value]))
|
||||
@ApiProperty({
|
||||
description: 'Filter by chat ids',
|
||||
required: false,
|
||||
example: ['111111111@c.us'],
|
||||
})
|
||||
ids?: string[];
|
||||
}
|
||||
|
||||
export class OverviewBodyRequest {
|
||||
@ValidateNested()
|
||||
@Type(() => OverviewPaginationParams)
|
||||
pagination: OverviewPaginationParams;
|
||||
|
||||
@ValidateNested()
|
||||
@Type(() => OverviewFilter)
|
||||
filter: OverviewFilter;
|
||||
}
|
||||
|
||||
export class ChatSummary {
|
||||
id: string;
|
||||
name: string | null;
|
||||
|
||||
@@ -32,7 +32,11 @@ import {
|
||||
VoiceBinaryFile,
|
||||
VoiceRemoteFile,
|
||||
} from './files.dto';
|
||||
import { ChatIdProperty, ReplyToProperty } from './properties.dto';
|
||||
import {
|
||||
ChatIdProperty,
|
||||
ConvertApiProperty,
|
||||
ReplyToProperty,
|
||||
} from './properties.dto';
|
||||
|
||||
/**
|
||||
* Queries
|
||||
@@ -164,6 +168,9 @@ export class MessageContactVcardRequest extends ChatRequest {
|
||||
],
|
||||
})
|
||||
contacts: (VCardContact | Contact)[];
|
||||
|
||||
@ReplyToProperty()
|
||||
reply_to?: string;
|
||||
}
|
||||
|
||||
export class MessageTextRequest extends ChatRequest {
|
||||
@@ -317,6 +324,9 @@ export class MessageVoiceRequest extends ChatRequest {
|
||||
|
||||
@ReplyToProperty()
|
||||
reply_to?: string;
|
||||
|
||||
@ConvertApiProperty()
|
||||
convert: boolean;
|
||||
}
|
||||
|
||||
@ApiExtraModels(VideoRemoteFile, VideoBinaryFile)
|
||||
@@ -343,6 +353,9 @@ export class MessageVideoRequest extends ChatRequest {
|
||||
example: false,
|
||||
})
|
||||
asNote?: boolean;
|
||||
|
||||
@ConvertApiProperty()
|
||||
convert: boolean;
|
||||
}
|
||||
|
||||
export class MessageLinkPreviewRequest extends ChatRequest {
|
||||
@@ -417,6 +430,7 @@ export class MessageDestination {
|
||||
to: string;
|
||||
from: string;
|
||||
fromMe: boolean;
|
||||
participant?: string;
|
||||
}
|
||||
|
||||
export class MessageButtonReply extends ChatRequest {
|
||||
|
||||
@@ -8,6 +8,7 @@ export enum WAHAEvents {
|
||||
MESSAGE_ACK = 'message.ack',
|
||||
MESSAGE_WAITING = 'message.waiting',
|
||||
MESSAGE_REVOKED = 'message.revoked',
|
||||
MESSAGE_EDITED = 'message.edited',
|
||||
STATE_CHANGE = 'state.change',
|
||||
GROUP_JOIN = 'group.join',
|
||||
GROUP_LEAVE = 'group.leave',
|
||||
@@ -26,6 +27,8 @@ export enum WAHAEvents {
|
||||
LABEL_DELETED = 'label.deleted',
|
||||
LABEL_CHAT_ADDED = 'label.chat.added',
|
||||
LABEL_CHAT_DELETED = 'label.chat.deleted',
|
||||
EVENT_RESPONSE = 'event.response',
|
||||
EVENT_RESPONSE_FAILED = 'event.response.failed',
|
||||
ENGINE_EVENT = 'engine.event',
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,144 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { MessageDestination } from '@waha/structures/chatting.dto';
|
||||
import { WAMessageBase } from '@waha/structures/responses.dto';
|
||||
import { Type } from 'class-transformer';
|
||||
import {
|
||||
IsBoolean,
|
||||
IsNotEmpty,
|
||||
IsNumber,
|
||||
IsOptional,
|
||||
IsString,
|
||||
ValidateNested,
|
||||
} from 'class-validator';
|
||||
|
||||
import { ChatIdProperty, ReplyToProperty } from './properties.dto';
|
||||
|
||||
export class EventLocation {
|
||||
@ApiProperty({
|
||||
description: 'Name of the location',
|
||||
example: 'Luxe Nail Studio 💅',
|
||||
})
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
name: string;
|
||||
|
||||
//
|
||||
// Doesn't work right now
|
||||
//
|
||||
// @ApiProperty({
|
||||
// description: 'Latitude of the location',
|
||||
// example: 38.8937255,
|
||||
// })
|
||||
// @IsNumber()
|
||||
// @IsOptional()
|
||||
// degreesLatitude?: number;
|
||||
//
|
||||
// @ApiProperty({
|
||||
// description: 'Longitude of the location',
|
||||
// example: -77.0969763,
|
||||
// })
|
||||
// @IsNumber()
|
||||
// @IsOptional()
|
||||
// degreesLongitude?: number;
|
||||
}
|
||||
|
||||
export class EventMessage {
|
||||
@ApiProperty({
|
||||
description: 'Name of the event',
|
||||
example: "John's Nail Appointment 💅",
|
||||
})
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
name: string;
|
||||
|
||||
@ApiProperty({
|
||||
description: 'Description of the event',
|
||||
example:
|
||||
"It's time for your nail care session! 🌟\\n\\nYou'll be getting a *classic gel manicure* – clean, polished, and long-lasting. 💖\\n\\n📍 *Location:* Luxe Nail Studio\\nWe're on the *2nd floor of the Plaza Mall*, next to the flower shop. Look for the *pink neon sign*!\\n\\nFeel free to arrive *5–10 mins early* so we can get started on time 😊",
|
||||
required: false,
|
||||
})
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
description?: string;
|
||||
|
||||
@ApiProperty({
|
||||
description: 'Start time of the event (Unix timestamp in seconds)',
|
||||
example: 2063137000,
|
||||
})
|
||||
@IsNumber()
|
||||
@IsNotEmpty()
|
||||
startTime: number;
|
||||
|
||||
@ApiProperty({
|
||||
description: 'End time of the event (Unix timestamp in seconds)',
|
||||
example: null,
|
||||
required: false,
|
||||
})
|
||||
@IsNumber()
|
||||
@IsOptional()
|
||||
endTime?: number;
|
||||
|
||||
@ApiProperty({
|
||||
description: 'Location of the event',
|
||||
required: false,
|
||||
type: EventLocation,
|
||||
})
|
||||
@ValidateNested()
|
||||
@Type(() => EventLocation)
|
||||
@IsOptional()
|
||||
location?: EventLocation;
|
||||
|
||||
@ApiProperty({
|
||||
description: 'Whether extra guests are allowed',
|
||||
example: false,
|
||||
required: false,
|
||||
})
|
||||
@IsBoolean()
|
||||
@IsOptional()
|
||||
extraGuestsAllowed?: boolean;
|
||||
}
|
||||
|
||||
export class EventMessageRequest {
|
||||
@ChatIdProperty()
|
||||
chatId: string;
|
||||
|
||||
event: EventMessage;
|
||||
|
||||
@ReplyToProperty()
|
||||
reply_to?: string;
|
||||
}
|
||||
|
||||
export class EventCancelRequest {
|
||||
@ApiProperty({
|
||||
description: 'ID of the event message to cancel',
|
||||
example: 'true_12345678901@c.us_ABCDEFGHIJKLMNOPQRST',
|
||||
})
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
id: string;
|
||||
}
|
||||
|
||||
export enum EventResponseType {
|
||||
UNKNOWN = 'UNKNOWN',
|
||||
GOING = 'GOING',
|
||||
NOT_GOING = 'NOT_GOING',
|
||||
MAYBE = 'MAYBE',
|
||||
}
|
||||
|
||||
export class EventResponse {
|
||||
response: EventResponseType;
|
||||
timestampMs: number;
|
||||
extraGuestCount: number;
|
||||
}
|
||||
|
||||
export class EventResponsePayload extends WAMessageBase {
|
||||
eventCreationKey: MessageDestination;
|
||||
eventResponse?: EventResponse;
|
||||
|
||||
/** Returns a message in a raw format */
|
||||
@ApiProperty({
|
||||
description:
|
||||
'Message in a raw format that we get from WhatsApp. May be changed anytime, use it with caution! It depends a lot on the underlying backend.',
|
||||
})
|
||||
_data?: any;
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { LimitOffsetParams } from '@waha/structures/pagination.dto';
|
||||
|
||||
export class LidToPhoneNumber {
|
||||
@ApiProperty({
|
||||
description: 'Linked ID for the user',
|
||||
example: '1111111@lid',
|
||||
})
|
||||
lid?: string;
|
||||
|
||||
@ApiProperty({
|
||||
description: 'Phone number (chat id) for the user',
|
||||
example: '3333333@c.us',
|
||||
})
|
||||
pn?: string;
|
||||
}
|
||||
|
||||
export class LidsListQueryParams extends LimitOffsetParams {
|
||||
limit?: number = 100;
|
||||
offset?: number = 0;
|
||||
}
|
||||
@@ -35,3 +35,34 @@ export class WAMedia {
|
||||
// eslint-disable-next-line @typescript-eslint/ban-types
|
||||
error?: object;
|
||||
}
|
||||
|
||||
export class FileDTO {
|
||||
@ApiProperty({
|
||||
description: 'The URL for the file',
|
||||
})
|
||||
url?: string;
|
||||
|
||||
@ApiProperty({
|
||||
description: 'Base64 content of the file',
|
||||
example: null,
|
||||
})
|
||||
data?: string;
|
||||
}
|
||||
|
||||
export class VoiceFileDTO extends FileDTO {
|
||||
@ApiProperty({
|
||||
description: 'The URL for the voice file',
|
||||
example:
|
||||
process.env.WHATSAPP_SWAGGER_VIDEO_EXAMPLE_URL ||
|
||||
'https://github.com/devlikeapro/waha/raw/core/examples/voice.mp3',
|
||||
})
|
||||
url?: string;
|
||||
}
|
||||
|
||||
export class VideoFileDTO extends FileDTO {
|
||||
@ApiProperty({
|
||||
description: 'The URL for the video file',
|
||||
example: 'https://github.com/devlikeapro/waha/raw/core/examples/video.mp4',
|
||||
})
|
||||
url?: string;
|
||||
}
|
||||
@@ -30,6 +30,14 @@ function ReplyToProperty() {
|
||||
});
|
||||
}
|
||||
|
||||
export function ConvertApiProperty() {
|
||||
return ApiProperty({
|
||||
description:
|
||||
'Convert the input file to the required format using ffmpeg before sending',
|
||||
example: true,
|
||||
});
|
||||
}
|
||||
|
||||
export {
|
||||
ChatIdProperty,
|
||||
MessageIdOnlyProperty,
|
||||
|
||||
@@ -16,7 +16,7 @@ export enum MessageSource {
|
||||
APP = 'app',
|
||||
}
|
||||
|
||||
class WAMessageBase {
|
||||
export class WAMessageBase {
|
||||
@MessageIdProperty()
|
||||
id: string;
|
||||
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { Transform, Type } from 'class-transformer';
|
||||
import { IsArray, IsNumber, IsOptional, IsString } from 'class-validator';
|
||||
|
||||
export class BrowserTraceQuery {
|
||||
@Type(() => Number)
|
||||
@IsNumber()
|
||||
@ApiProperty({
|
||||
description: 'How many seconds to trace',
|
||||
example: 30,
|
||||
required: true,
|
||||
})
|
||||
seconds: number = 30;
|
||||
|
||||
@IsArray()
|
||||
@IsString({ each: true })
|
||||
@Transform(({ value }) => (Array.isArray(value) ? value : [value]))
|
||||
@IsOptional()
|
||||
@ApiProperty({
|
||||
description: 'Categories to trace (all by default)',
|
||||
example: ['*'],
|
||||
required: true,
|
||||
})
|
||||
categories: string[] = ['*'];
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
import { ApiExtraModels, ApiProperty, getSchemaPath } from '@nestjs/swagger';
|
||||
import { ConvertApiProperty } from '@waha/structures/properties.dto';
|
||||
|
||||
import {
|
||||
BinaryFile,
|
||||
@@ -63,6 +64,9 @@ export class VoiceStatus extends StatusRequest {
|
||||
file: VoiceRemoteFile | VoiceBinaryFile;
|
||||
|
||||
backgroundColor: string = '#38b42f';
|
||||
|
||||
@ConvertApiProperty()
|
||||
convert: boolean;
|
||||
}
|
||||
|
||||
@ApiExtraModels(VideoRemoteFile, VideoBinaryFile)
|
||||
@@ -76,6 +80,9 @@ export class VideoStatus extends StatusRequest {
|
||||
file: VideoRemoteFile | VideoBinaryFile;
|
||||
|
||||
caption?: string;
|
||||
|
||||
@ConvertApiProperty()
|
||||
convert: boolean;
|
||||
}
|
||||
|
||||
export class DeleteStatusRequest extends StatusRequest {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { CallData } from '@waha/structures/calls.dto';
|
||||
import { EventResponsePayload } from '@waha/structures/events.dto';
|
||||
import { Label, LabelChatAssociation } from '@waha/structures/labels.dto';
|
||||
|
||||
import { ChatArchiveEvent } from './chats.dto';
|
||||
@@ -86,14 +87,30 @@ export class PollVote extends MessageDestination {
|
||||
export class PollVotePayload {
|
||||
vote: PollVote;
|
||||
poll: MessageDestination;
|
||||
_data?: any;
|
||||
}
|
||||
|
||||
export class WAMessageRevokedBody {
|
||||
after?: WAMessage;
|
||||
before?: WAMessage;
|
||||
|
||||
@ApiProperty({
|
||||
description: 'ID of the message that was revoked',
|
||||
example: 'A06CA7BB5DD8C8F705628CDB7E3A33C9',
|
||||
})
|
||||
revokedMessageId?: string;
|
||||
|
||||
_data?: any;
|
||||
}
|
||||
|
||||
export class WAMessageEditedBody extends WAMessage {
|
||||
@ApiProperty({
|
||||
description: 'ID of the original message that was edited',
|
||||
example: 'A06CA7BB5DD8C8F705628CDB7E3A33C9',
|
||||
})
|
||||
editedMessageId?: string;
|
||||
}
|
||||
|
||||
export class WASessionStatusBody {
|
||||
@ApiProperty({
|
||||
example: 'default',
|
||||
@@ -204,6 +221,16 @@ export class WAHAWebhookMessageRevoked extends WAHAWebhook {
|
||||
payload: WAMessageRevokedBody;
|
||||
}
|
||||
|
||||
export class WAHAWebhookMessageEdited extends WAHAWebhook {
|
||||
@ApiProperty({
|
||||
description:
|
||||
'The event is triggered when a user edits a previously sent message.',
|
||||
})
|
||||
event = WAHAEvents.MESSAGE_EDITED;
|
||||
|
||||
payload: WAMessageEditedBody;
|
||||
}
|
||||
|
||||
export class WAHAWebhookStateChange extends WAHAWebhook {
|
||||
@ApiProperty({
|
||||
description: 'It’s an internal engine’s state, not session status.',
|
||||
@@ -344,6 +371,25 @@ export class EnginePayload {
|
||||
data: any;
|
||||
}
|
||||
|
||||
export class WAHAWebhookEventResponse extends WAHAWebhook {
|
||||
@ApiProperty({
|
||||
description: 'The event is triggered when the event response is received.',
|
||||
})
|
||||
event = WAHAEvents.EVENT_RESPONSE;
|
||||
|
||||
payload: EventResponsePayload;
|
||||
}
|
||||
|
||||
export class WAHAWebhookEventResponseFailed extends WAHAWebhook {
|
||||
@ApiProperty({
|
||||
description:
|
||||
'The event is triggered when the event response is failed to decrypt.',
|
||||
})
|
||||
event = WAHAEvents.EVENT_RESPONSE_FAILED;
|
||||
|
||||
payload: EventResponsePayload;
|
||||
}
|
||||
|
||||
export class WAHAWebhookEngineEvent extends WAHAWebhook {
|
||||
@ApiProperty({
|
||||
description: 'Internal engine event.',
|
||||
|
||||
@@ -10,6 +10,8 @@ import {
|
||||
WAHAWebhookCallRejected,
|
||||
WAHAWebhookChatArchive,
|
||||
WAHAWebhookEngineEvent,
|
||||
WAHAWebhookEventResponse,
|
||||
WAHAWebhookEventResponseFailed,
|
||||
WAHAWebhookGroupJoin,
|
||||
WAHAWebhookGroupLeave,
|
||||
WAHAWebhookLabelChatAdded,
|
||||
@@ -19,6 +21,7 @@ import {
|
||||
WAHAWebhookMessage,
|
||||
WAHAWebhookMessageAck,
|
||||
WAHAWebhookMessageAny,
|
||||
WAHAWebhookMessageEdited,
|
||||
WAHAWebhookMessageReaction,
|
||||
WAHAWebhookMessageRevoked,
|
||||
WAHAWebhookPollVote,
|
||||
@@ -35,6 +38,7 @@ const WAHA_WEBHOOKS = [
|
||||
WAHAWebhookMessageAny,
|
||||
WAHAWebhookMessageAck,
|
||||
WAHAWebhookMessageRevoked,
|
||||
WAHAWebhookMessageEdited,
|
||||
WebhookGroupV2Join,
|
||||
WebhookGroupV2Leave,
|
||||
WebhookGroupV2Update,
|
||||
@@ -50,6 +54,8 @@ const WAHA_WEBHOOKS = [
|
||||
WAHAWebhookLabelDeleted,
|
||||
WAHAWebhookLabelChatAdded,
|
||||
WAHAWebhookLabelChatDeleted,
|
||||
WAHAWebhookEventResponse,
|
||||
WAHAWebhookEventResponseFailed,
|
||||
WAHAWebhookEngineEvent,
|
||||
WAHAWebhookGroupJoin,
|
||||
WAHAWebhookGroupLeave,
|
||||
|
||||
Loaded 100 of 106 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user