Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 |
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
|
||||
+17
-2
@@ -1,7 +1,7 @@
|
||||
#
|
||||
# Build
|
||||
#
|
||||
ARG NODE_VERSION=22.8-bullseye
|
||||
ARG NODE_VERSION=22.16-bullseye
|
||||
FROM node:${NODE_VERSION} AS build
|
||||
ENV PUPPETEER_SKIP_DOWNLOAD=True
|
||||
|
||||
@@ -119,6 +119,21 @@ RUN if [ "$USE_BROWSER" = "chromium" ] || [ "$USE_BROWSER" = "chrome" ]; then \
|
||||
&& rm -rf /var/lib/apt/lists/*; \
|
||||
fi
|
||||
|
||||
# Install xvfb
|
||||
RUN if [ "$USE_BROWSER" = "chromium" ] || [ "$USE_BROWSER" = "chrome" ]; then \
|
||||
apt-get update && apt-get install -y --no-install-recommends \
|
||||
xvfb \
|
||||
libnss3 \
|
||||
libxss1 \
|
||||
libasound2 \
|
||||
libatk-bridge2.0-0 \
|
||||
libgtk-3-0 \
|
||||
libdrm2 \
|
||||
ca-certificates \
|
||||
curl \
|
||||
&& rm -rf /var/lib/apt/lists/*; \
|
||||
fi
|
||||
|
||||
# Install Chromium
|
||||
RUN if [ "$USE_BROWSER" = "chromium" ]; then \
|
||||
apt-get update \
|
||||
@@ -131,7 +146,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 \
|
||||
|
||||
@@ -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
|
||||
|
||||
+1
-1
@@ -38,7 +38,7 @@ services:
|
||||
# POSTGRES_PASSWORD: postgres
|
||||
# POSTGRES_DB: postgres
|
||||
# ports:
|
||||
# - "5432:5432"
|
||||
# - "127.0.0.1:5432:5432"
|
||||
# volumes:
|
||||
# - pg_data:/var/lib/postgresql/data
|
||||
# command:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1,11 +1,70 @@
|
||||
#!/bin/sh
|
||||
|
||||
#
|
||||
# Run Xvfb if exists
|
||||
#
|
||||
if command -v Xvfb > /dev/null 2>&1; then
|
||||
# Start virtual X server in the background
|
||||
Xvfb :99 -screen 0 1280x720x24 &
|
||||
export DISPLAY=:99
|
||||
sleep 2
|
||||
else
|
||||
echo "Xvfb command not found, skipping virtual X server setup"
|
||||
fi
|
||||
|
||||
#
|
||||
# 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}"
|
||||
|
||||
#
|
||||
# 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 "⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️"
|
||||
|
||||
# Start a background task to display another warning after 5 seconds
|
||||
(
|
||||
sleep 5
|
||||
echo "⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️"
|
||||
echo "SECURITY REMINDER: Your plain text API key has been hashed for this session."
|
||||
echo "In the future, please use WAHA_API_KEY=sha512:{SHA512_HASH_FOR_YOUR_API_KEY}"
|
||||
echo "to avoid exposing your API key in environment variables or process lists."
|
||||
echo "⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️"
|
||||
) &
|
||||
|
||||
# Hash the key using sha512sum
|
||||
HASHED_KEY=$(echo -n "$key" | sha512sum | awk '{print $1}')
|
||||
export WAHA_API_KEY="sha512:$HASHED_KEY"
|
||||
fi
|
||||
fi
|
||||
|
||||
#
|
||||
# Start your application using node with exec to ensure proper signal handling
|
||||
#
|
||||
exec node dist/main
|
||||
Binary file not shown.
Binary file not shown.
+7
-6
@@ -49,7 +49,7 @@
|
||||
"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",
|
||||
@@ -88,7 +88,7 @@
|
||||
"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": {
|
||||
@@ -97,12 +97,13 @@
|
||||
"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": "^11.0.0",
|
||||
"@nestjs/schematics": "^11.0.0",
|
||||
@@ -117,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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -76,6 +93,7 @@ export class SwaggerConfiguratorCore {
|
||||
'🏷️ Labels',
|
||||
'Labels - available only for WhatsApp Business accounts',
|
||||
)
|
||||
.addTag('🖼️ Media', 'Media methods')
|
||||
.addTag('🔍 Observability', 'Other methods')
|
||||
.addTag('🗄️ Storage', 'Storage methods')
|
||||
.addApiKey({
|
||||
@@ -162,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);
|
||||
}
|
||||
}
|
||||
@@ -57,6 +57,7 @@ export abstract class SessionManager
|
||||
protected gowsConfigService: GowsEngineConfigService,
|
||||
) {
|
||||
this.lock = new AsyncLock({
|
||||
timeout: 5_000,
|
||||
maxPending: Infinity,
|
||||
maxExecutionTime: 30_000,
|
||||
});
|
||||
|
||||
@@ -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 {
|
||||
@@ -24,16 +28,15 @@ 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 {
|
||||
LimitOffsetParams,
|
||||
PaginationParams,
|
||||
} from '@waha/structures/pagination.dto';
|
||||
import { PaginationParams } from '@waha/structures/pagination.dto';
|
||||
import { MessageSource, WAMessage } from '@waha/structures/responses.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';
|
||||
@@ -109,9 +112,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';
|
||||
|
||||
@@ -168,6 +174,8 @@ export abstract class WhatsappSession {
|
||||
stdTTL: 10 * 60, // 10 minutes
|
||||
});
|
||||
|
||||
public mediaConverter: IMediaConverter = new CoreMediaConverter();
|
||||
|
||||
public constructor({
|
||||
name,
|
||||
printQR,
|
||||
@@ -993,6 +1001,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) {
|
||||
|
||||
+72
-17
@@ -1,4 +1,7 @@
|
||||
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';
|
||||
@@ -11,20 +14,29 @@ import {
|
||||
ServerController,
|
||||
ServerDebugController,
|
||||
} from '@waha/api/server.controller';
|
||||
import { WebsocketGatewayCore } from '@waha/core/api/websocket.gateway.core';
|
||||
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';
|
||||
|
||||
@@ -36,6 +48,7 @@ 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';
|
||||
@@ -45,6 +58,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';
|
||||
@@ -141,7 +155,30 @@ export const CONTROLLERS = [
|
||||
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,
|
||||
@@ -151,19 +188,8 @@ const PROVIDERS = [
|
||||
provide: WAHAHealthCheckService,
|
||||
useClass: WAHAHealthCheckServiceCore,
|
||||
},
|
||||
{
|
||||
provide: APP_INTERCEPTOR,
|
||||
useClass: BufferJsonReplacerInterceptor,
|
||||
},
|
||||
ChannelsInfoServiceCore,
|
||||
DashboardConfigServiceCore,
|
||||
SwaggerConfigServiceCore,
|
||||
WebJSEngineConfigService,
|
||||
GowsEngineConfigService,
|
||||
WhatsappConfigService,
|
||||
EngineConfigService,
|
||||
WebsocketGatewayCore,
|
||||
MediaLocalStorageConfig,
|
||||
...PROVIDERS_BASE,
|
||||
];
|
||||
|
||||
@Module({
|
||||
@@ -174,15 +200,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}`);
|
||||
|
||||
@@ -671,7 +671,7 @@ var EventStreamService = exports.EventStreamService = {
|
||||
},
|
||||
};
|
||||
|
||||
exports.EventStreamClient = grpc.makeGenericClientConstructor(EventStreamService);
|
||||
exports.EventStreamClient = grpc.makeGenericClientConstructor(EventStreamService, 'EventStream');
|
||||
var MessageServiceService = exports.MessageServiceService = {
|
||||
//
|
||||
// Session management
|
||||
@@ -1381,4 +1381,4 @@ getMessageById: {
|
||||
},
|
||||
};
|
||||
|
||||
exports.MessageServiceClient = grpc.makeGenericClientConstructor(MessageServiceService);
|
||||
exports.MessageServiceClient = grpc.makeGenericClientConstructor(MessageServiceService, 'MessageService');
|
||||
@@ -129,6 +129,8 @@ import {
|
||||
EnginePayload,
|
||||
PollVotePayload,
|
||||
WAMessageAckBody,
|
||||
WAMessageEditedBody,
|
||||
WAMessageRevokedBody,
|
||||
} from '@waha/structures/webhooks.dto';
|
||||
import { PaginatorInMemory } from '@waha/utils/Paginator';
|
||||
import { sleep, waitUntil } from '@waha/utils/promiseTimeout';
|
||||
@@ -407,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)),
|
||||
|
||||
@@ -159,6 +159,7 @@ import {
|
||||
PollVote,
|
||||
PollVotePayload,
|
||||
WAMessageAckBody,
|
||||
WAMessageEditedBody,
|
||||
WAMessageRevokedBody,
|
||||
} from '@waha/structures/webhooks.dto';
|
||||
import { LoggerBuilder } from '@waha/utils/logging';
|
||||
@@ -595,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', [
|
||||
{
|
||||
@@ -1829,15 +1831,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
|
||||
//
|
||||
@@ -2101,12 +2133,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
|
||||
|
||||
@@ -64,11 +64,13 @@ export class NowebPersistentStore implements INowebStore {
|
||||
public presences: 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,
|
||||
});
|
||||
|
||||
@@ -19,6 +19,7 @@ const AsyncLock = require('async-lock');
|
||||
// Default pending is 1000, set it to infinity
|
||||
// https://github.com/rogierschouten/async-lock/issues/63
|
||||
const fileLock = new AsyncLock({
|
||||
timeout: 5_000,
|
||||
maxPending: Infinity,
|
||||
maxExecutionTime: 30_000,
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
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';
|
||||
@@ -10,7 +11,6 @@ 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';
|
||||
|
||||
|
||||
@@ -111,6 +111,7 @@ import { StatusRequest, TextStatus } from '@waha/structures/status.dto';
|
||||
import {
|
||||
EnginePayload,
|
||||
WAMessageAckBody,
|
||||
WAMessageEditedBody,
|
||||
WAMessageRevokedBody,
|
||||
} from '@waha/structures/webhooks.dto';
|
||||
import { PaginatorInMemory } from '@waha/utils/Paginator';
|
||||
@@ -1418,9 +1419,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,
|
||||
};
|
||||
}),
|
||||
);
|
||||
@@ -1432,6 +1436,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,
|
||||
|
||||
@@ -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',
|
||||
}
|
||||
@@ -79,15 +79,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}"`);
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
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 { sleep } from '@waha/utils/promiseTimeout';
|
||||
import { Database } from 'better-sqlite3';
|
||||
import Knex from 'knex';
|
||||
|
||||
@@ -18,4 +19,12 @@ export class Sqlite3KVRepository<Entity> extends SqlKVRepository<Entity> {
|
||||
super(engine, knex);
|
||||
this.db = db;
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
+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();
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -32,7 +32,11 @@ import {
|
||||
VoiceBinaryFile,
|
||||
VoiceRemoteFile,
|
||||
} from './files.dto';
|
||||
import { ChatIdProperty, ReplyToProperty } from './properties.dto';
|
||||
import {
|
||||
ChatIdProperty,
|
||||
ConvertApiProperty,
|
||||
ReplyToProperty,
|
||||
} from './properties.dto';
|
||||
|
||||
/**
|
||||
* Queries
|
||||
@@ -320,6 +324,9 @@ export class MessageVoiceRequest extends ChatRequest {
|
||||
|
||||
@ReplyToProperty()
|
||||
reply_to?: string;
|
||||
|
||||
@ConvertApiProperty()
|
||||
convert: boolean;
|
||||
}
|
||||
|
||||
@ApiExtraModels(VideoRemoteFile, VideoBinaryFile)
|
||||
@@ -346,6 +353,9 @@ export class MessageVideoRequest extends ChatRequest {
|
||||
example: false,
|
||||
})
|
||||
asNote?: boolean;
|
||||
|
||||
@ConvertApiProperty()
|
||||
convert: boolean;
|
||||
}
|
||||
|
||||
export class MessageLinkPreviewRequest 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',
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -93,9 +93,24 @@ export class PollVotePayload {
|
||||
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',
|
||||
@@ -206,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.',
|
||||
|
||||
@@ -21,6 +21,7 @@ import {
|
||||
WAHAWebhookMessage,
|
||||
WAHAWebhookMessageAck,
|
||||
WAHAWebhookMessageAny,
|
||||
WAHAWebhookMessageEdited,
|
||||
WAHAWebhookMessageReaction,
|
||||
WAHAWebhookMessageRevoked,
|
||||
WAHAWebhookPollVote,
|
||||
@@ -37,6 +38,7 @@ const WAHA_WEBHOOKS = [
|
||||
WAHAWebhookMessageAny,
|
||||
WAHAWebhookMessageAck,
|
||||
WAHAWebhookMessageRevoked,
|
||||
WAHAWebhookMessageEdited,
|
||||
WebhookGroupV2Join,
|
||||
WebhookGroupV2Leave,
|
||||
WebhookGroupV2Update,
|
||||
|
||||
+3
-1
@@ -33,9 +33,11 @@ export function getEngineName(): string {
|
||||
}
|
||||
|
||||
export const VERSION: WAHAEnvironment = {
|
||||
version: '2025.5.5',
|
||||
version: '2025.6.5',
|
||||
engine: getEngineName(),
|
||||
tier: getWAHAVersion(),
|
||||
browser:
|
||||
getEngineName() === WAHAEngine.WEBJS ? getBrowserExecutablePath() : null,
|
||||
};
|
||||
|
||||
export const IsChrome = VERSION.browser?.includes('chrome');
|
||||
+2
-2
@@ -2,11 +2,11 @@
|
||||
"waha": {
|
||||
"gows": {
|
||||
"repo": "devlikeapro/gows",
|
||||
"ref": "v1.0.7"
|
||||
"ref": "v1.0.8"
|
||||
},
|
||||
"dashboard": {
|
||||
"repo": "devlikeapro/dashboard",
|
||||
"ref": "3fb6694d849ca0b567679dd59311a50b9d727f2f"
|
||||
"ref": "52750393583e215d212d22bc613d75c106add3a1"
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user