Compare commits

..
57 Commits
Author SHA1 Message Date
devlikepro 78fb4b1a65 [core] Init store
Release / WEBJS - chrome - amd64 - chrome (push) Waiting to run
Release / WEBJS - chromium - amd64 - latest (push) Waiting to run
Release / WEBJS - chromium - linux/arm64 - arm (push) Waiting to run
Release / GOWS - none - amd64 - gows (push) Waiting to run
Release / GOWS - none - linux/arm64 - gows-arm (push) Waiting to run
Release / NOWEB - none - amd64 - noweb (push) Waiting to run
Release / NOWEB - none - linux/arm64 - noweb-arm (push) Waiting to run
2025-01-19 15:18:07 +07:00
devlikepro 8e566cba72 [core] 2025.1.4 2025-01-19 14:44:06 +07:00
devlikepro 36184288de [core] NOWEB PostgreSQL 2025-01-19 14:42:59 +07:00
devlikepro 6fc5bf38a7 [core] NOWEB - handle not found profile picture 2025-01-19 14:42:59 +07:00
devlikepro e146ecde70 [core] Add local store to WAHA Core
fix #741
2025-01-19 14:42:58 +07:00
devlikepro 72b796019a [core] PostgreSQL Media 2025-01-19 14:42:58 +07:00
devlikepro 0f66d15ba8 [core] PSQL Media 2025-01-19 14:42:57 +07:00
devlikepro 4695e933b8 [core] Up GOWS
[core] GOWS up
2025-01-19 14:42:57 +07:00
devlikepro 5e1da6cd6f [core] PostgreSQL docker-compose.yaml 2025-01-19 14:42:56 +07:00
devlikepro 0c3bbc0980 [core] PostgreSQL 2025-01-19 14:42:56 +07:00
devlikepro d2bb5bea63 [core] Add session config schema + migrations 2025-01-19 14:42:55 +07:00
devlikepro c76e191d4d [core] Extract general SqlKVRepository 2025-01-19 14:42:54 +07:00
devlikepro 171594d0ae [core] NOWEB Update contact if not found 2025-01-19 14:42:53 +07:00
devlikepro 4e5f499b48 [core] Rename db max name limit 2025-01-19 14:42:53 +07:00
devlikepro bb75ad8b8e [core] Add pg 2025-01-19 14:42:52 +07:00
devlikepro 482cc7028b [core] Move sqlite3 2025-01-19 14:42:51 +07:00
devlikepro 735bdb3854 [core] Websocket - close server and then terminate clients 2025-01-19 14:42:51 +07:00
devlikepro 1ee1bd78af [core] Stop events after we stop the session 2025-01-19 14:42:51 +07:00
devlikepro 00463c729d [core] SIGTERM to GOWS 2025-01-19 14:42:51 +07:00
devlikepro dac636b3bb [core] detached GOWS subprocess 2025-01-19 14:42:51 +07:00
devlikepro ad87121506 [core] make copy-dashboard (dist) 2025-01-19 14:42:51 +07:00
devlikepro 55d802f005 [core] Disable some flag for WEBJS stability 2025-01-19 14:42:51 +07:00
devlikepro f17164a3d9 [core] 2025.1.3
Release / WEBJS - chrome - amd64 - chrome (push) Waiting to run
Release / WEBJS - chromium - amd64 - latest (push) Waiting to run
Release / WEBJS - chromium - linux/arm64 - arm (push) Waiting to run
Release / GOWS - none - amd64 - gows (push) Waiting to run
Release / GOWS - none - linux/arm64 - gows-arm (push) Waiting to run
Release / NOWEB - none - amd64 - noweb (push) Waiting to run
Release / NOWEB - none - linux/arm64 - noweb-arm (push) Waiting to run
2025-01-10 11:01:11 +07:00
devlikepro 0b9f9b2335 [core] Add GOWS engine 2025-01-10 11:00:56 +07:00
devlikepro 71d3e24241 [core] nestjs forceCloseConnections 2025-01-10 11:00:56 +07:00
devlikepro ccd4512e8f [core] Get QR PNG on demand 2025-01-10 11:00:55 +07:00
devlikepro fe6b77d67d [core] Use UV_THREADPOOL_SIZE as cpu*1 2025-01-10 11:00:55 +07:00
devlikepro 58d12f4e70 [core] NOWEB - sleep 1s to get latest presence 2025-01-10 11:00:55 +07:00
devlikepro 17303039b8 [core] NOWEB subscribe presence 2025-01-10 11:00:55 +07:00
devlikepro f70d5203a3 [core] Websocket logs verbose 2025-01-10 11:00:55 +07:00
devlikepro efd6b1db65 [core] WEBJS Remove in-process-gpu
Release / WEBJS - chrome - amd64 - chrome (push) Waiting to run
Release / WEBJS - chromium - amd64 - latest (push) Waiting to run
Release / WEBJS - chromium - linux/arm64 - arm (push) Waiting to run
Release / NOWEB - none - amd64 - noweb (push) Waiting to run
Release / NOWEB - none - linux/arm64 - noweb-arm (push) Waiting to run
fix #725
2025-01-09 16:23:27 +07:00
devlikepro dcca0fa921 [core] 2025.1.2 2025-01-09 16:03:04 +07:00
devlikepro e5bc447807 [core] Load name from contact
fix #732
2025-01-09 14:31:22 +07:00
devlikepro cd6b4b5ab1 [core] Use atomic writes
fix #731
2025-01-09 14:21:38 +07:00
devlikepro fb79ae6138 [core] 2025.1.1
Release / NOWEB - none - amd64 - noweb (push) Waiting to run
Release / WEBJS - chrome - amd64 - chrome (push) Waiting to run
Release / WEBJS - chromium - amd64 - latest (push) Waiting to run
Release / WEBJS - chromium - linux/arm64 - arm (push) Waiting to run
Release / NOWEB - none - linux/arm64 - noweb-arm (push) Waiting to run
2025-01-08 09:46:15 +07:00
devlikepro 17801b0b13 [core] Set log level to "trace" if specified 2025-01-08 09:45:52 +07:00
devlikepro 9010388de0 [core] Add websocket send error handler 2025-01-08 09:45:52 +07:00
devlikepro 2da901f994 [core] Add lock timeout 10s to session operations 2025-01-08 09:45:52 +07:00
devlikepro 7832cc7a5d [core] NOWEB - save creds if any 2025-01-08 09:41:20 +07:00
devlikepro 13ab2e8d0e [core] downloadMedia=false in list messages 2024-12-29 14:52:53 +07:00
devlikepro 868d23a278 [core] Mark /api/messages as deprecated 2024-12-29 14:48:47 +07:00
devlikepro eaf2bca44e [core] 2024.12.6
Release / WEBJS - chrome - amd64 - chrome (push) Waiting to run
Release / WEBJS - chromium - amd64 - latest (push) Waiting to run
Release / WEBJS - chromium - linux/arm64 - arm (push) Waiting to run
Release / NOWEB - none - amd64 - noweb (push) Waiting to run
Release / NOWEB - none - linux/arm64 - noweb-arm (push) Waiting to run
2024-12-24 13:35:25 +07:00
devlikepro af8f537f58 [core] Up dashboard 2024-12-24 13:34:21 +07:00
devlikepro bc7ff472be [core] Chats overview API 2024-12-24 13:33:41 +07:00
devlikepro 9b9a2b5203 [core] NOWEB - listen update profile image 2024-12-24 13:33:40 +07:00
devlikepro 347259394b [core] Cache null picture to avoid additional requests 2024-12-24 13:33:40 +07:00
devlikepro ead61b5e37 [core] Add GET /api/:session/chats/:chatId/picture 2024-12-24 13:33:40 +07:00
devlikepro c9f7814f5d [core] Refresh profile picture 2024-12-24 13:33:39 +07:00
devlikepro 3ee3355e81 [core] Add in-memory cache for profile picture 2024-12-24 13:33:39 +07:00
devlikepro ce8f8b0ce9 [core] Add dedicated fetch contact profile picture
fix #381
2024-12-24 13:33:39 +07:00
devlikepro cae35cf3b9 [core] Up WEBJS
Fix #576 - add retries and send regular message anyway

fix #698
2024-12-24 13:33:39 +07:00
devlikepro 900e3c23bc [core] 2024.12.5
Release / WEBJS - chrome - amd64 - chrome (push) Waiting to run
Release / WEBJS - chromium - amd64 - latest (push) Waiting to run
Release / WEBJS - chromium - linux/arm64 - arm (push) Waiting to run
Release / NOWEB - none - amd64 - noweb (push) Waiting to run
Release / NOWEB - none - linux/arm64 - noweb-arm (push) Waiting to run
2024-12-16 11:56:50 +07:00
devlikepro aaeffeb983 [core] Update libsignal 2024-12-16 11:56:39 +07:00
devlikepro 15600324be [core] Update NOWEB 2024-12-16 11:56:39 +07:00
devlikepro da4913edb5 [core] Use libsignal fork
Add logging
2024-12-16 11:56:39 +07:00
devlikepro e8cc1e4906 [core] 2024.12.4
Release / WEBJS - chrome - amd64 - chrome (push) Waiting to run
Release / WEBJS - chromium - amd64 - latest (push) Waiting to run
Release / WEBJS - chromium - linux/arm64 - arm (push) Waiting to run
Release / NOWEB - none - amd64 - noweb (push) Waiting to run
Release / NOWEB - none - linux/arm64 - noweb-arm (push) Waiting to run
2024-12-14 19:15:09 +07:00
devlikepro 801fc3f9fd [core] Join group via code
fix #688
2024-12-14 19:14:56 +07:00
93 changed files with 3242 additions and 689 deletions

No files matched your search

+1
View File
@@ -1,4 +1,5 @@
Dockerfile
src/core/engines/gows/grpc
.*sessions
sessions
files
+32 -17
View File
@@ -6,10 +6,12 @@
# 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
WAHA_BASE_URL=http://localhost:3000
#WHATSAPP_API_SCHEMA=http
#WHATSAPP_API_PORT=3000
#WHATSAPP_API_HOSTNAME=localhost
#WAHA_BASE_URL=http://localhost:3000
#
# Set your timezone to see the right time on screenshots
# Find your timezone name in the list
@@ -90,6 +92,17 @@ WHATSAPP_SWAGGER_PASSWORD=admin
#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
#
#
# Global Proxy Settings
# https://waha.devlike.pro/docs/how-to/config/#global-proxy-configuration
@@ -101,22 +114,6 @@ WHATSAPP_SWAGGER_PASSWORD=admin
#WHATSAPP_PROXY_SERVER_PASSWORD=pass
#
#
# HTTPS Configuration
# https://waha.devlike.pro/docs/how-to/config/#https
#
# Consider using certbot for HTTPS
# https://waha.devlike.pro/blog/setting-up-https-for-waha/#lets-encrypt-certbot--waha
#
#WAHA_HTTPS_ENABLED=true
#WAHA_HTTPS_PATH_KEY=/etc/letsencrypt/live/waha.example.pro/privkey.pem
#WAHA_HTTPS_PATH_CERT=/etc/letsencrypt/live/waha.example.pro/cert.pem \
#WAHA_HTTPS_PATH_CA=/etc/letsencrypt/live/waha.example.pro/chain.pem
#WHATSAPP_API_SCHEMA=https
#WHATSAPP_API_PORT=3000
#WHATSAPP_API_HOSTNAME=waha.example.pro
#
#
# Media Configuration
#
@@ -169,8 +166,26 @@ WHATSAPP_FILES_FOLDER=/app/.media
# volumes:
# - './.sessions:/app/sessions'
#
# MongoDB Configuration
# https://waha.devlike.pro/docs/how-to/storages/#sessions---mongodb
#WHATSAPP_SESSIONS_MONGO_URL=mongodb://mongouser:mongopassword@mongodb:27017
#
#
# 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
#
+1
View File
@@ -0,0 +1 @@
src/core/engines/gows/grpc/*
+18 -2
View File
@@ -45,7 +45,7 @@ jobs:
# browser: "chrome"
# goss: 'goss-linux-arm'
# No browser - x86
# NOWEB - x86
- runner: 'buildjet-4vcpu-ubuntu-2204'
tag: 'noweb'
platform: 'amd64'
@@ -53,7 +53,7 @@ jobs:
engine: 'NOWEB'
goss: 'goss-linux-amd64'
# No browser - ARM
# NOWEB - ARM
- runner: 'buildjet-4vcpu-ubuntu-2204-arm'
tag: 'noweb-arm'
platform: 'linux/arm64'
@@ -61,6 +61,22 @@ jobs:
engine: 'NOWEB'
goss: 'goss-linux-arm'
# GOWS - x86
- runner: 'buildjet-4vcpu-ubuntu-2204'
tag: 'gows'
platform: 'amd64'
browser: 'none'
engine: 'GOWS'
goss: 'goss-linux-amd64'
# No browser - ARM
- runner: 'buildjet-4vcpu-ubuntu-2204-arm'
tag: 'gows-arm'
platform: 'linux/arm64'
browser: 'none'
engine: 'GOWS'
goss: 'goss-linux-arm'
steps:
- name: Checkout
uses: actions/checkout@v3
+1
View File
@@ -1,3 +1,4 @@
src/core/engines/gows/grpc
.env
*.heapsnapshot
.yarn
+1
View File
@@ -2,6 +2,7 @@ exclude: |
(?x)^(
^src/dashboard/.*|
^src/core/engines/webjs/.*html|
^src/core/engines/gows/grpc/.*|
^src/plus/engines/webjs/.*html|
^.yarn/.*
)$
+64 -3
View File
@@ -1,3 +1,9 @@
# GOWS
ARG GOWS_GITHUB_REPO=devlikeapro/gows
ARG GOWS_SHA=39ba4dcceeeb6e6fab172ee71ad1c09deea4db16
ARG WAHA_DASHBOARD_GITHUB_REPO=devlikeapro/dashboard
ARG WAHA_DASHBOARD_SHA=a1c839d8da94d1cade182addffdfc42fc0cf7ed3
#
# Build
#
@@ -5,6 +11,10 @@ ARG NODE_VERSION=22.8-bullseye
FROM node:${NODE_VERSION} AS build
ENV PUPPETEER_SKIP_DOWNLOAD=True
# install protoc
RUN apt-get update && \
apt-get install protobuf-compiler -y
# npm packages
WORKDIR /src
COPY package.json .
@@ -18,6 +28,16 @@ RUN yarn install
WORKDIR /src
ADD . /src
RUN yarn install
# Build node grpc client for GOWS engine
WORKDIR /gows/proto
ARG GOWS_GITHUB_REPO
ARG GOWS_SHA
RUN wget https://raw.githubusercontent.com/${GOWS_GITHUB_REPO}/${GOWS_SHA}/proto/gows.proto
WORKDIR /src
RUN make proto-gows
RUN yarn build && find ./dist -name "*.d.ts" -delete
#
@@ -26,15 +46,41 @@ RUN yarn build && find ./dist -name "*.d.ts" -delete
FROM node:${NODE_VERSION} AS dashboard
# Download WAHA Dashboard
ENV WAHA_DASHBOARD_SHA 45db5b46a944f4320136f2409d8b970483c8bd9d
ARG WAHA_DASHBOARD_GITHUB_REPO
ARG WAHA_DASHBOARD_SHA
RUN \
wget https://github.com/devlikeapro/dashboard/archive/${WAHA_DASHBOARD_SHA}.zip \
wget https://github.com/${WAHA_DASHBOARD_GITHUB_REPO}/archive/${WAHA_DASHBOARD_SHA}.zip \
&& unzip ${WAHA_DASHBOARD_SHA}.zip -d /tmp/dashboard \
&& mkdir -p /dashboard \
&& mv /tmp/dashboard/dashboard-${WAHA_DASHBOARD_SHA}/* /dashboard/ \
&& rm -rf ${WAHA_DASHBOARD_SHA}.zip \
&& rm -rf /tmp/dashboard/dashboard-${WAHA_DASHBOARD_SHA}
#
# GOWS
#
FROM golang:1.23-bullseye AS gows
# install protoc
RUN apt-get update && \
apt-get install protobuf-compiler -y
# Image processing for thumbnails
RUN apt-get update \
&& apt-get install -y libvips-dev \
&& rm -rf /var/lib/apt/lists/*
ARG GOWS_GITHUB_REPO
ARG GOWS_SHA
RUN git clone https://github.com/${GOWS_GITHUB_REPO} gows && \
cd gows && \
git checkout ${GOWS_SHA}
WORKDIR /go/gows
RUN go install google.golang.org/protobuf/cmd/protoc-gen-go@latest
RUN go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest
RUN make all
#
# Final
#
@@ -51,6 +97,11 @@ RUN echo "USE_BROWSER=$USE_BROWSER"
# Install ffmpeg to generate previews for videos
RUN apt-get update && apt-get install -y ffmpeg --no-install-recommends && rm -rf /var/lib/apt/lists/*
# Image processing for thumbnails
RUN apt-get update \
&& apt-get install -y libvips \
&& rm -rf /var/lib/apt/lists/*
# Install zip and unzip - either for chromium or chrome
RUN if [ "$USE_BROWSER" = "chromium" ] || [ "$USE_BROWSER" = "chrome" ]; then \
apt-get update \
@@ -100,7 +151,13 @@ RUN if [ "$USE_BROWSER" = "chrome" ]; then \
&& rm -rf /var/lib/apt/lists/*; \
fi
# Set the ENV for NOWEB docker image
# GOWS requirements
# libc6
RUN apt-get update \
&& apt-get install -y libc6 \
&& rm -rf /var/lib/apt/lists/*
# Set the ENV for docker image
ENV WHATSAPP_DEFAULT_ENGINE=$WHATSAPP_DEFAULT_ENGINE
# Attach sources, install packages
@@ -109,6 +166,10 @@ COPY package.json ./
COPY --from=build /src/node_modules ./node_modules
COPY --from=build /src/dist ./dist
COPY --from=dashboard /dashboard ./dist/dashboard
COPY --from=gows /go/gows/bin/gows /app/gows
ENV WAHA_GOWS_PATH /app/gows
ENV WAHA_GOWS_SOCKET /tmp/gows.sock
COPY entrypoint.sh /entrypoint.sh
# Chokidar options to monitor file changes
+27
View File
@@ -7,6 +7,9 @@ build-chrome:
build-noweb:
docker build . -t devlikeapro/waha:noweb --build-arg USE_BROWSER=none --build-arg WHATSAPP_DEFAULT_ENGINE=NOWEB
build-plus-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:
@@ -34,8 +37,32 @@ for-swagger:
up-noweb:
. ${NVM_DIR}/nvm.sh && nvm exec yarn up @adiwajshing/baileys@github:devlikeapro/Baileys#fork-master-channels-678
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
start-proxy:
docker run --rm -d --name squid-container -e TZ=UTC -p 3128:3128 ubuntu/squid:5.2-22.04_beta
proto-gows:
rm -rf src/core/engines/gows/grpc
mkdir -p src/core/engines/gows/grpc
protoc \
--plugin=protoc-gen-ts=node_modules/.bin/protoc-gen-ts \
--plugin=protoc-gen-grpc=node_modules/.bin/grpc_tools_node_protoc_plugin \
--js_out=import_style=commonjs,binary:src/core/engines/gows/grpc \
--grpc_out=grpc_js:src/core/engines/gows/grpc \
--ts_out=grpc_js:src/core/engines/gows/grpc \
-I ../gows/proto ../gows/proto/gows.proto
gows:
cd ../gows && \
export PATH=${HOME}/go/bin:${PATH} && \
make all
copy-dashboard:
cd ../waha-hub/ui && \
make copy-waha
+22 -4
View File
@@ -22,9 +22,27 @@ services:
env_file:
- .env
# NOTE: Only if you're using MongoDB
# https://waha.devlike.pro/docs/how-to/storages/#sessions---mongodb
# Uncomment this block if you're using MongoDB
# NOTE: Only if you're using PostgreSQL to save sessions
# https://waha.devlike.pro/docs/how-to/storages/#media---postgresql
# postgres:
# image: postgres:17
# restart: always
# environment:
# POSTGRES_USER: postgres
# POSTGRES_PASSWORD: postgres
# POSTGRES_DB: postgres
# ports:
# - "5432:5432"
# volumes:
# - pg_data:/var/lib/postgresql/data
# command:
# - postgres
# - "-c"
# - "max_connections=1000"
# NOTE: Only if you're using MongoDB
# https://waha.devlike.pro/docs/how-to/storages/#sessions---mongodb
# Uncomment this block if you're using MongoDB
# mongodb:
# image: mongo
# container_name: mongodb
@@ -55,6 +73,6 @@ services:
# command: server /data --console-address ":9001"
volumes:
# NOTE: Only if you're using MongoDB
mongodb_data: {}
minio_data: {}
pg_data: {}
+1 -1
View File
@@ -2,7 +2,7 @@
# Calculate UV_THREADPOOL_SIZE based on number of CPUs
cpus=$(node -e "const os = require('os'); console.log(os.cpus().length);")
uv_threadpool_size=$(($cpus * 2))
uv_threadpool_size=$(($cpus * 1))
# Set UV_THREADPOOL_SIZE as an environment variable
export UV_THREADPOOL_SIZE="${UV_THREADPOOL_SIZE:-$uv_threadpool_size}"
+11 -2
View File
@@ -55,10 +55,12 @@
"express-basic-auth": "^1.2.1",
"file-type": "16.5.4",
"fs-extra": "^11.2.0",
"google-protobuf": "^3.21.4",
"https-proxy-agent": "^7.0.0",
"joi": "^17.13.3",
"knex": "^3.1.0",
"libphonenumber-js": "^1.10.36",
"libsignal": "github:devlikeapro/libsignal-node#fork-master",
"link-preview-js": "^3.0.4",
"lodash": "^4.17.21",
"mime-types": "^2.1.27",
@@ -66,6 +68,7 @@
"nestjs-pino": "^4.1.0",
"passport": "^0.6.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",
@@ -79,7 +82,8 @@
"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-channels",
"write-file-atomic": "^6.0.0"
},
"optionalDependencies": {
"bufferutil": "^4.0.8"
@@ -88,9 +92,12 @@
"bufferutil": "^4.0.8",
"ws": "^8.18.0",
"puppeteer": "^23.6.0",
"whatwg-url": "13.0.0"
"whatwg-url": "13.0.0",
"libsignal": "github:devlikeapro/libsignal-node#fork-master"
},
"devDependencies": {
"@grpc/grpc-js": "^1.12.4",
"@grpc/proto-loader": "^0.7.13",
"@nestjs/cli": "^9.0.0",
"@nestjs/schematics": "^9.0.1",
"@nestjs/testing": "^9.0.9",
@@ -104,8 +111,10 @@
"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",
"jest": "^29.7.0",
"prettier": "^1.19.1",
"protoc-gen-ts": "^0.8.7",
"supertest": "^4.0.2",
"ts-jest": "^29.1.3",
"ts-loader": "^6.2.1",
+31 -1
View File
@@ -21,12 +21,15 @@ import {
import { SessionManager } from '../core/abc/manager.abc';
import { WhatsappSession } from '../core/abc/session.abc';
import { parseBool } from '../helpers';
import {
ChatPictureQuery,
ChatPictureResponse,
ChatsPaginationParams,
ChatSummary,
GetChatMessageQuery,
GetChatMessagesFilter,
GetChatMessagesQuery,
OverviewPaginationParams,
PinMessageRequest,
} from '../structures/chats.dto';
import { EditMessageRequest } from '../structures/chatting.dto';
@@ -48,6 +51,20 @@ class ChatsController {
return session.getChats(pagination);
}
@Get('overview')
@SessionApiParam
@ApiOperation({
summary:
'Get chats overview. Includes all necessary things to build UI "your chats overview" page - chat id, name, picture, last message. Sorting by last message timestamp',
})
@UsePipes(new ValidationPipe({ transform: true, whitelist: true }))
getChatsOverview(
@WorkingSessionParam session: WhatsappSession,
@Query() pagination: OverviewPaginationParams,
): Promise<ChatSummary[]> {
return session.getChatsOverview(pagination);
}
@Delete(':chatId')
@SessionApiParam
@ApiOperation({ summary: 'Deletes the chat' })
@@ -59,6 +76,19 @@ class ChatsController {
return session.deleteChat(chatId);
}
@Get(':chatId/picture')
@SessionApiParam
@ApiOperation({ summary: 'Gets chat picture' })
@UsePipes(new ValidationPipe({ transform: true, whitelist: true }))
async getChatPicture(
@WorkingSessionParam session: WhatsappSession,
@Param('chatId') chatId: string,
@Query() query: ChatPictureQuery,
): Promise<ChatPictureResponse> {
const url = await session.getContactProfilePicture(chatId, query.refresh);
return { url: url };
}
@Get(':chatId/messages')
@SessionApiParam
@ApiOperation({ summary: 'Gets messages in the chat' })
+17 -13
View File
@@ -171,15 +171,10 @@ export class ChattingController {
return whatsapp.sendLinkPreview(request);
}
@Get('/messages')
@ApiOperation({ summary: 'Get messages in a chat' })
@UsePipes(new ValidationPipe({ transform: true, whitelist: true }))
async getMessages(
@Query() query: GetMessageQuery,
@Query() filter: GetChatMessagesFilter,
) {
const whatsapp = await this.manager.getWorkingSession(query.session);
return whatsapp.getChatMessages(query.chatId, query, filter);
@Post('/sendContactVcard')
async sendContactVcard(@Body() request: MessageContactVcardRequest) {
const whatsapp = await this.manager.getWorkingSession(request.session);
return whatsapp.sendContactVCard(request);
}
@Get('/sendText')
@@ -192,10 +187,19 @@ export class ChattingController {
return whatsapp.sendText(msg);
}
@Post('/sendContactVcard')
async sendContactVcard(@Body() request: MessageContactVcardRequest) {
const whatsapp = await this.manager.getWorkingSession(request.session);
return whatsapp.sendContactVCard(request);
@Get('/messages')
@ApiOperation({
summary: 'Get messages in a chat',
description: 'DEPRECATED. Use "GET /api/chats/{id}/messages" instead',
deprecated: true,
})
@UsePipes(new ValidationPipe({ transform: true, whitelist: true }))
async getMessages(
@Query() query: GetMessageQuery,
@Query() filter: GetChatMessagesFilter,
) {
const whatsapp = await this.manager.getWorkingSession(query.session);
return whatsapp.getChatMessages(query.chatId, query, filter);
}
@Get('/checkNumberStatus')
+8 -2
View File
@@ -16,6 +16,7 @@ import {
WANumberExistResult,
} from '../structures/chatting.dto';
import {
ContactProfilePictureQuery,
ContactQuery,
ContactRequest,
ContactsPaginationParams,
@@ -75,9 +76,14 @@ export class ContactsController {
description:
'If privacy settings do not allow to get the picture, the method will return null.',
})
async getProfilePicture(@Query() query: ContactQuery) {
@UsePipes(new ValidationPipe({ transform: true, whitelist: true }))
async getProfilePicture(@Query() query: ContactProfilePictureQuery) {
const whatsapp = await this.manager.getWorkingSession(query.session);
return whatsapp.getContactProfilePicture(query);
const url = await whatsapp.getContactProfilePicture(
query.contactId,
query.refresh,
);
return { profilePictureURL: url };
}
@Post('/block')
+27
View File
@@ -23,6 +23,8 @@ import {
CreateGroupRequest,
DescriptionRequest,
GroupsPaginationParams,
JoinGroupRequest,
JoinGroupResponse,
ParticipantsRequest,
SettingsSecurityChangeInfo,
SubjectRequest,
@@ -44,6 +46,31 @@ export class GroupsController {
return session.createGroup(request);
}
@Get('join-info')
@SessionApiParam
@ApiOperation({ summary: 'Get info about the group before joining.' })
async joinInfoGroup(
@WorkingSessionParam session: WhatsappSession,
@Query() query: JoinGroupRequest,
): Promise<any> {
// https://chat.whatsapp.com/123 => 123
const code = query.code.split('/').pop();
return session.joinInfoGroup(code);
}
@Post('join')
@SessionApiParam
@ApiOperation({ summary: 'Join group via code' })
async joinGroup(
@WorkingSessionParam session: WhatsappSession,
@Body() request: JoinGroupRequest,
): Promise<JoinGroupResponse> {
// https://chat.whatsapp.com/123 => 123
const code = request.code.split('/').pop();
const id = await session.joinGroup(code);
return { id: id };
}
@Get('')
@SessionApiParam
@ApiOperation({ summary: 'Get all groups.' })
+7
View File
@@ -134,6 +134,13 @@ export class WhatsappConfigService {
return this.configService.get('WHATSAPP_SESSIONS_MONGO_URL', undefined);
}
getSessionPostgresUrl(): string | undefined {
return this.configService.get(
'WHATSAPP_SESSIONS_POSTGRESQL_URL',
undefined,
);
}
get(name: string, defaultValue: any = undefined): any {
return this.configService.get(name, defaultValue);
}
+8 -5
View File
@@ -1,13 +1,16 @@
// eslint-disable-next-line @typescript-eslint/no-var-requires
const QRCode = require('qrcode');
export class QR {
private base64: string;
public raw?: string;
save(base64: string, raw?: string) {
this.base64 = base64.replace(/^data:image\/png;base64,/, '');
save(raw?: string) {
this.raw = raw;
}
get(): Buffer {
return Buffer.from(this.base64, 'base64');
async get(): Promise<Buffer> {
const url = await QRCode.toDataURL(this.raw);
const base64 = url.replace(/^data:image\/png;base64,/, '');
return Buffer.from(base64, 'base64');
}
}
+15
View File
@@ -0,0 +1,15 @@
export interface EngineBootstrap {
bootstrap(): Promise<void>;
shutdown(): Promise<void>;
}
export class NoopEngineBootstrap implements EngineBootstrap {
async bootstrap(): Promise<void> {
return;
}
async shutdown(): Promise<void> {
return;
}
}
+31 -3
View File
@@ -1,8 +1,15 @@
import {
BeforeApplicationShutdown,
OnApplicationBootstrap,
UnprocessableEntityException,
} from '@nestjs/common';
import { WhatsappConfigService } from '@waha/config.service';
import {
EngineBootstrap,
NoopEngineBootstrap,
} from '@waha/core/abc/EngineBootstrap';
import { GowsEngineConfigService } from '@waha/core/config/GowsEngineConfigService';
import { GowsBootstrap } from '@waha/core/engines/gows/GowsBootstrap';
import { ISessionMeRepository } from '@waha/core/storage/ISessionMeRepository';
import { ISessionWorkerRepository } from '@waha/core/storage/ISessionWorkerRepository';
import { WAHAWebhook } from '@waha/structures/webhooks.dto';
@@ -29,7 +36,9 @@ import { WhatsappSession } from './session.abc';
// eslint-disable-next-line @typescript-eslint/no-var-requires
const AsyncLock = require('async-lock');
export abstract class SessionManager implements BeforeApplicationShutdown {
export abstract class SessionManager
implements BeforeApplicationShutdown, OnApplicationBootstrap
{
public store: any;
public sessionAuthRepository: ISessionAuthRepository;
public sessionConfigRepository: ISessionConfigRepository;
@@ -39,12 +48,17 @@ export abstract class SessionManager implements BeforeApplicationShutdown {
WAIT_STATUS_INTERVAL = 500;
WAIT_STATUS_TIMEOUT = 5_000;
LOCK_TIMEOUT = 10_000;
protected constructor(
protected config: WhatsappConfigService,
protected log: PinoLogger,
protected config: WhatsappConfigService,
protected gowsConfigService: GowsEngineConfigService,
) {
this.lock = new AsyncLock({ maxPending: Infinity });
this.lock = new AsyncLock({
maxPending: Infinity,
timeout: this.LOCK_TIMEOUT,
});
this.log.setContext(SessionManager.name);
}
@@ -156,7 +170,21 @@ export abstract class SessionManager implements BeforeApplicationShutdown {
beforeApplicationShutdown(signal?: string) {
return;
}
onApplicationBootstrap(): any {
return;
}
protected getEngineBootstrap(engine: WAHAEngine): EngineBootstrap {
const logger = this.log.logger.child({ engine: engine.toLowerCase() });
if (engine === WAHAEngine.GOWS) {
const config = this.gowsConfigService.getBootstrapConfig();
return new GowsBootstrap(logger, config);
}
return new NoopEngineBootstrap();
}
}
export function populateSessionInfo(
event: WAHAEvents,
session: WhatsappSession,
+57 -10
View File
@@ -4,6 +4,7 @@ import {
ListChannelsQuery,
} from '@waha/structures/channels.dto';
import {
ChatSummary,
GetChatMessageQuery,
GetChatMessagesFilter,
GetChatMessagesQuery,
@@ -19,6 +20,8 @@ import { complete } from '@waha/utils/reactive/complete';
import { SwitchObservable } from '@waha/utils/reactive/SwitchObservable';
import * as fs from 'fs';
import * as lodash from 'lodash';
import { PinoLogger } from 'nestjs-pino';
import * as NodeCache from 'node-cache';
import { Logger } from 'pino';
import {
BehaviorSubject,
@@ -51,6 +54,7 @@ import {
MessageTextRequest,
MessageVideoRequest,
MessageVoiceRequest,
SendSeenRequest,
} from '../../structures/chatting.dto';
import { ContactQuery, ContactRequest } from '../../structures/contacts.dto';
import {
@@ -132,6 +136,9 @@ export abstract class WhatsappSession {
private shouldPrintQR: boolean;
protected events2: DefaultMap<WAHAEvents, SwitchObservable<any>>;
private status$: Subject<WAHASessionStatus>;
protected profilePictures: NodeCache = new NodeCache({
stdTTL: 24 * 60 * 60, // 1 day
});
public constructor({
name,
@@ -235,12 +242,13 @@ export abstract class WhatsappSession {
// References:
// https://github.com/pedroslopez/whatsapp-web.js/issues/1420
// https://github.com/wppconnect-team/wppconnect/issues/1326
// https://superuser.com/questions/654565/how-to-run-google-chrome-in-a-single-process
// https://www.bannerbear.com/blog/ways-to-speed-up-puppeteer-screenshots/
return [
'--aggressive-cache-discard',
'--disable-accelerated-2d-canvas',
'--disable-application-cache',
'--disable-background-networking',
// '--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',
@@ -250,13 +258,13 @@ export abstract class WhatsappSession {
'--disable-default-apps',
'--disable-dev-shm-usage',
'--disable-extensions',
'--disable-features=site-per-process',
'--disable-gpu',
// '--disable-features=site-per-process', // COMMENTED to test WEBJS stability
//'--disable-gpu', // COMMENTED to test WEBJS stability
'--disable-offer-store-unmasked-wallet-cards',
'--disable-offline-load-stale-cache',
'--disable-popup-blocking',
'--disable-setuid-sandbox',
'--disable-site-isolation-trials', // https://superuser.com/questions/654565/how-to-run-google-chrome-in-a-single-process
'--disable-site-isolation-trials',
'--disable-speech-api',
'--disable-sync',
'--disable-translate',
@@ -265,7 +273,8 @@ export abstract class WhatsappSession {
'--hide-scrollbars',
'--ignore-certificate-errors',
'--ignore-ssl-errors',
'--in-process-gpu', // https://superuser.com/questions/654565/how-to-run-google-chrome-in-a-single-process
// https://github.com/devlikeapro/waha/issues/725
// '--in-process-gpu', // COMMENTED to test WEBJS stability
'--metrics-recording-only',
'--mute-audio',
'--no-default-browser-check',
@@ -274,7 +283,7 @@ export abstract class WhatsappSession {
'--no-sandbox',
'--no-zygote',
'--password-store=basic',
'--renderer-process-limit=2', // https://superuser.com/questions/654565/how-to-run-google-chrome-in-a-single-process
// '--renderer-process-limit=2', // COMMENTED to test WEBJS stability
'--safebrowsing-disable-auto-update',
// '--single-process',
'--use-mock-keychain',
@@ -374,7 +383,7 @@ export abstract class WhatsappSession {
abstract reply(request: MessageReplyRequest);
abstract sendSeen(chat: ChatRequest);
abstract sendSeen(chat: SendSeenRequest);
abstract startTyping(chat: ChatRequest);
@@ -393,6 +402,12 @@ export abstract class WhatsappSession {
throw new NotImplementedByEngineError();
}
public getChatsOverview(
pagination: PaginationParams,
): Promise<ChatSummary[]> {
throw new NotImplementedByEngineError();
}
public deleteChat(chatId) {
throw new NotImplementedByEngineError();
}
@@ -504,8 +519,32 @@ export abstract class WhatsappSession {
throw new NotImplementedByEngineError();
}
public getContactProfilePicture(query: ContactQuery) {
throw new NotImplementedByEngineError();
/**
* Fetch the latest profile picture of the contact (group, newsletter, etc.)
* @param id
*/
abstract fetchContactProfilePicture(id: string): Promise<string | null>;
public async getContactProfilePicture(
id: string,
refresh: boolean,
): Promise<string | null> {
const has: boolean = this.profilePictures.has(id);
if (!has || refresh) {
await this.refreshProfilePicture(id);
}
return this.profilePictures.get(id) || null;
}
protected async refreshProfilePicture(id: string) {
this.profilePictures.del(id);
const url = await this.fetchContactProfilePicture(id).catch((err) => {
this.logger.warn('Error fetching profile picture');
this.logger.warn(err, err.stack);
return null;
});
this.profilePictures.set(id, url);
return url;
}
public blockContact(request: ContactRequest) {
@@ -523,6 +562,14 @@ export abstract class WhatsappSession {
throw new NotImplementedByEngineError();
}
public joinGroup(code: string): Promise<string> {
throw new NotImplementedByEngineError();
}
public joinInfoGroup(code: string): Promise<any> {
throw new NotImplementedByEngineError();
}
public getGroups(pagination: PaginationParams) {
throw new NotImplementedByEngineError();
}
@@ -607,7 +654,7 @@ export abstract class WhatsappSession {
throw new NotImplementedByEngineError();
}
public subscribePresence(id: string): Promise<void> {
public subscribePresence(id: string): Promise<any> {
throw new NotImplementedByEngineError();
}
+22 -9
View File
@@ -65,7 +65,12 @@ export class WebsocketGatewayCore
.getSessionEvents(session, events)
.subscribe((data) => {
this.logger.debug(`Sending data to client, event.id: ${data.id}`, data);
socket.send(JSON.stringify(data));
socket.send(JSON.stringify(data), (err) => {
if (!err) {
return;
}
this.logger.error(`Error sending data to client: ${err}`);
});
});
socket.on('close', () => {
this.logger.debug(`Client disconnected - ${socket.id}`);
@@ -91,17 +96,25 @@ 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);
this.server?.clients.forEach((options, client) => {
// @ts-ignore
this.logger.debug(`Closing client connection - ${client.id}...`);
client.close(1001, 'Server is shutting down');
});
// Do not turn off heartbeat service here,
// it's responsible for terminating the connection that is not alive
// Close clients and server
await this.close(this.server);
this.logger.log('Websocket server is down');
this.heartbeat?.stop();
}
// 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) {
+5 -1
View File
@@ -10,6 +10,7 @@ import {
ServerDebugController,
} from '@waha/api/server.controller';
import { WebsocketGatewayCore } from '@waha/core/api/websocket.gateway.core';
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';
@@ -106,7 +107,9 @@ export const IMPORTS_CORE = [
const IMPORTS_MEDIA = [
ConfigModule.forRoot({
validationSchema: Joi.object({
WAHA_MEDIA_STORAGE: Joi.string().valid('LOCAL', 'S3').default('LOCAL'),
WAHA_MEDIA_STORAGE: Joi.string()
.valid('LOCAL', 'S3', 'POSTGRESQL')
.default('LOCAL'),
}),
}),
MediaLocalStorageModule,
@@ -148,6 +151,7 @@ const PROVIDERS = [
DashboardConfigServiceCore,
SwaggerConfigServiceCore,
WebJSEngineConfigService,
GowsEngineConfigService,
WhatsappConfigService,
EngineConfigService,
WebsocketGatewayCore,
@@ -0,0 +1,26 @@
import { Injectable } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
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) {}
getBootstrapConfig(): BootstrapConfig {
return {
path: this.configService.get('WAHA_GOWS_PATH'),
socket: this.getSocket(),
};
}
getSocket() {
return this.configService.get('WAHA_GOWS_SOCKET', '/tmp/gows.sock');
}
getConfig(): GowsConfig {
return {
connection: 'unix:' + this.getSocket(),
};
}
}
@@ -0,0 +1,31 @@
import { EventEmitter } from 'events';
import { Observable, Subscription } from 'rxjs';
interface EventValue<T> {
event: string;
data: T;
}
export class EventsFromObservable<Events extends string> extends EventEmitter {
private subscription: Subscription;
constructor(private observable: Observable<EventValue<any>>) {
super();
}
on(event: Events, listener: (...args: any[]) => void): this {
return super.on(event as string, listener);
}
start() {
this.subscription = this.observable.subscribe({
next: (data) => this.emit(data.event, data.data),
complete: () => this.removeAllListeners(),
error: (err) => null,
});
}
stop() {
this.subscription.unsubscribe();
}
}
+72
View File
@@ -0,0 +1,72 @@
import { EngineBootstrap } from '@waha/core/abc/EngineBootstrap';
import { GowsSubprocess } from '@waha/core/engines/gows/GowsSubprocess';
import { promises as fs } from 'fs';
import { Logger } from 'pino';
export async function isUnixSocket(socketPath: string): Promise<boolean> {
try {
// Get the file stats
const stats = await fs.lstat(socketPath);
// Check if the path is a socket
return stats.isSocket();
} catch (error) {
// Handle errors (e.g., file does not exist)
if (error.code === 'ENOENT') {
return false; // Path does not exist
}
// Re-throw other errors for debugging purposes
throw error;
}
}
export interface BootstrapConfig {
path: string;
socket: string;
}
export class GowsBootstrap implements EngineBootstrap {
private gows: GowsSubprocess;
constructor(
private logger: Logger,
private config: BootstrapConfig,
) {}
async bootstrap(): Promise<void> {
if (!this.config.path) {
this.logger.warn('GOWS path is not set, skipping GOWS initialization.');
this.logger.warn('Make sure to run GOWS manually.');
this.checkSocket(this.config.socket);
return;
}
this.gows = new GowsSubprocess(
this.logger,
this.config.path,
this.config.socket,
);
this.gows.start(() => {
this.logger.info(`GOWS stopped, exiting...`);
process.kill(process.pid, 'SIGTERM');
});
await this.gows.waitWhenReady(10_000);
await this.checkSocket(this.config.socket);
return;
}
async shutdown(): Promise<void> {
if (this.gows) {
await this.gows.stop();
}
}
/**
* Check the provided path exists and is a socket
* @param path
*/
async checkSocket(path: string) {
if (!(await isUnixSocket(path))) {
throw new Error(`Invalid socket path: ${path}`);
}
}
}
@@ -0,0 +1,69 @@
import * as grpc from '@grpc/grpc-js';
import { messages } from '@waha/core/engines/gows/grpc/gows';
import { EnginePayload } from '@waha/structures/webhooks.dto';
import { sleep } from '@waha/utils/promiseTimeout';
import { Logger } from 'pino';
import { Observable } from 'rxjs';
/**
* Observable that listens to a gRPC stream and emits EnginePayload objects.
* Pass a factory function that returns a client and a stream.
*/
export class GowsEventStreamObservable extends Observable<EnginePayload> {
_client: grpc.Client;
CLIENT_CLOSE_TIMEOUT = 1_000;
constructor(
private logger: Logger,
factory: () => {
client: grpc.Client;
stream: grpc.ClientReadableStream<messages.EventJson>;
},
) {
super((subscriber) => {
this.logger.debug('Creating grpc client and stream...');
const { client, stream } = factory();
this._client = client;
stream.on('data', (raw) => {
const obj = raw.toObject();
obj.data = JSON.parse(obj.data);
subscriber?.next(obj);
});
stream.on('end', (...args) => {
this.logger.debug('Stream ended', args);
subscriber?.complete();
subscriber = null;
});
stream.on('error', async (err: any) => {
const CLIENT_CANCELLED_CODE = grpc.status.CANCELLED;
if (err.code === CLIENT_CANCELLED_CODE) {
this.logger.debug('Stream cancelled by client');
return;
}
this.logger.error(err, 'Stream error');
// Give some time to node event loop to process the error
await sleep(100);
subscriber?.error(err);
subscriber = null;
});
return async () => {
this.logger.debug('Closing stream client...');
client.close();
await sleep(this.CLIENT_CLOSE_TIMEOUT);
this.logger.debug('Stream client closed');
this.logger.debug('Cancelling stream...');
stream.cancel();
this.logger.debug('Stream cancelled');
};
});
}
get client(): Omit<grpc.Client, 'close'> {
return this._client;
}
}
+100
View File
@@ -0,0 +1,100 @@
import { sleep, waitUntil } from '@waha/utils/promiseTimeout';
import { spawn } from 'child_process';
import { Logger } from 'pino';
export class GowsSubprocess {
private checkIntervalMs: number = 100;
private readyDelayMs: number = 1_000;
private readyText = 'gRPC server started!';
private child: any;
private ready: boolean = false;
constructor(
private logger: Logger,
readonly path: string,
readonly socket: string,
) {}
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], {
detached: true,
});
this.logger.debug(`GOWS started with PID: ${this.child.pid}`);
this.child.on('close', async (code, singal) => {
const msg = code
? `GOWS subprocess closed with code ${code}`
: `GOWS subprocess closed by signal ${singal}`;
this.logger.debug(msg);
onExit(code);
});
this.child.on('error', (err) => {
this.logger.error(`GOWS subprocess error: ${err}`);
});
this.child.stderr.setEncoding('utf8');
this.child.stderr.on('data', (data) => {
this.logger.error(data);
});
this.child.stdout.setEncoding('utf8');
this.child.stdout.on('data', async (data) => {
// remove empty line at the end, split by \n
const lines = data.trim().split('\n');
lines.forEach((line) => this.log(line));
});
this.listenReady();
}
listenReady() {
this.child.stdout.on('data', async (data) => {
if (this.ready) {
return;
}
if (!data.includes(this.readyText)) {
return;
}
await sleep(this.readyDelayMs);
this.ready = true;
this.logger.info('GOWS is ready');
});
}
async waitWhenReady(timeout: number) {
const started = await waitUntil(
async () => this.ready,
this.checkIntervalMs,
timeout,
);
if (!started) {
const msg = 'GOWS did not start after 10 seconds';
this.logger.error(msg);
throw new Error(msg);
}
}
async stop() {
this.logger.info('Stopping GOWS subprocess...');
this.child?.kill('SIGTERM');
this.logger.info('GOWS subprocess stopped');
}
private log(msg) {
if (msg.startsWith('ERROR | ')) {
this.logger.error(msg.slice(8));
} else if (msg.startsWith('WARN | ')) {
this.logger.warn(msg.slice(7));
} else if (msg.startsWith('INFO | ')) {
this.logger.info(msg.slice(7));
} else if (msg.startsWith('DEBUG | ')) {
this.logger.debug(msg.slice(8));
} else if (msg.startsWith('TRACE | ')) {
this.logger.trace(msg.slice(8));
} else {
this.logger.info(msg);
}
}
}
+896
View File
@@ -0,0 +1,896 @@
import { isJidGroup, jidNormalizedUser } from '@adiwajshing/baileys';
import * as grpc from '@grpc/grpc-js';
import { connectivityState } from '@grpc/grpc-js';
import { UnprocessableEntityException } from '@nestjs/common';
import { WhatsappSession } from '@waha/core/abc/session.abc';
import { EventsFromObservable } from '@waha/core/engines/gows/EventsFromObservable';
import { GowsEventStreamObservable } from '@waha/core/engines/gows/GowsEventStreamObservable';
import { messages } from '@waha/core/engines/gows/grpc/gows';
import { GowsAuthFactoryCore } from '@waha/core/engines/gows/store/GowsAuthFactoryCore';
import {
parseMessageIdSerialized,
toCusFormat,
toJID,
} from '@waha/core/engines/noweb/session.noweb.core';
import { AvailableInPlusVersion } from '@waha/core/exceptions';
import { IMediaEngineProcessor } from '@waha/core/media/IMediaEngineProcessor';
import { QR } from '@waha/core/QR';
import {
ChatRequest,
CheckNumberStatusQuery,
MessageFileRequest,
MessageForwardRequest,
MessageImageRequest,
MessageLocationRequest,
MessageReactionRequest,
MessageReplyRequest,
MessageTextRequest,
MessageVoiceRequest,
SendSeenRequest,
} from '@waha/structures/chatting.dto';
import {
ACK_UNKNOWN,
WAHAEvents,
WAHAPresenceStatus,
WAHASessionStatus,
WAMessageAck,
} from '@waha/structures/enums.dto';
import {
WAHAChatPresences,
WAHAPresenceData,
} from '@waha/structures/presence.dto';
import { WAMessage, WAMessageReaction } from '@waha/structures/responses.dto';
import { MeInfo, ProxyConfig } from '@waha/structures/sessions.dto';
import { EnginePayload, WAMessageAckBody } from '@waha/structures/webhooks.dto';
import { sleep, waitUntil } from '@waha/utils/promiseTimeout';
import { onlyEvent } from '@waha/utils/reactive/ops/onlyEvent';
import * as NodeCache from 'node-cache';
import {
debounceTime,
filter,
groupBy,
merge,
mergeMap,
Observable,
partition,
retry,
share,
} from 'rxjs';
import { map } from 'rxjs/operators';
import { promisify } from 'util';
import * as gows from './types';
import MessageServiceClient = messages.MessageServiceClient;
enum WhatsMeowEvent {
CONNECTED = 'gows.ConnectedEventData',
DISCONNECTED = 'events.Disconnected',
KEEP_ALIVE_TIMEOUT = 'events.KeepAliveTimeout',
QR_CHANNEL_ITEM = 'whatsmeow.QRChannelItem',
MESSAGE = 'events.Message',
RECEIPT = 'events.Receipt',
PRESENCE = 'events.Presence',
CHAT_PRESENCE = 'events.ChatPresence',
PUSH_NAME_SETTING = 'events.PushNameSetting',
LOGGED_OUT = 'events.LoggedOut',
}
export interface GowsConfig {
connection: string;
}
export class WhatsappSessionGoWSCore extends WhatsappSession {
protected authFactory = new GowsAuthFactoryCore();
protected qr: QR;
public client: MessageServiceClient;
protected stream$: GowsEventStreamObservable;
protected all$: Observable<EnginePayload>;
protected events: EventsFromObservable<WhatsMeowEvent>;
protected me: MeInfo | null;
public session: messages.Session;
protected presences: any;
public constructor(config) {
super(config);
this.qr = new QR();
this.session = new messages.Session({ id: this.name });
this.presences = new NodeCache({
stdTTL: 60 * 60, // 1 hour
useClones: false,
});
}
async start() {
this.status = WAHASessionStatus.STARTING;
this.buildStreams();
this.subscribeEvents();
this.subscribeEngineEvents2();
if (this.isDebugEnabled()) {
this.listenEngineEventsInDebugMode();
}
// start session
const auth = await this.authFactory.buildAuth(this.sessionStore, this.name);
const level = messages.LogLevel[this.logger.level.toUpperCase()];
const request = new messages.StartSessionRequest({
id: this.name,
config: new messages.SessionConfig({
store: new messages.SessionStoreConfig({
address: auth.address(),
dialect: auth.dialect(),
}),
log: new messages.SessionLogConfig({
level: level ?? messages.LogLevel.INFO,
}),
proxy: new messages.SessionProxyConfig({
url: this.getProxyUrl(this.proxyConfig),
}),
}),
});
this.client = new MessageServiceClient(
this.engineConfig.connection,
grpc.credentials.createInsecure(),
);
try {
await promisify(this.client.StartSession)(request);
} catch (err) {
this.logger.error('Failed to start the client');
this.logger.error(err, err.stack);
this.status = WAHASessionStatus.FAILED;
throw err;
}
}
protected getProxyUrl(config: ProxyConfig): string {
if (!config || !config.server) {
return '';
}
const { server, username, password } = config;
const auth = username && password ? `${username}:${password}@` : '';
const schema = 'http';
return `${schema}://${auth}${server}`;
}
buildStreams() {
this.stream$ = new GowsEventStreamObservable(
this.loggerBuilder.child({ grpc: 'stream' }),
() => {
const client = new messages.EventStreamClient(
this.engineConfig.connection,
grpc.credentials.createInsecure(),
);
const stream = client.StreamEvents(this.session);
return { client, stream };
},
);
// Retry on error with delay
this.all$ = this.stream$.pipe(retry({ delay: 2_000 }), share());
}
subscribeEvents() {
// Handle connection status
this.events = new EventsFromObservable<WhatsMeowEvent>(this.all$);
const events = this.events;
events.on(WhatsMeowEvent.CONNECTED, (data) => {
this.status = WAHASessionStatus.WORKING;
this.me = {
id: toCusFormat(jidNormalizedUser(data.ID)),
pushName: data.PushName,
};
});
events.on(WhatsMeowEvent.DISCONNECTED, () => {
if (this.status != WAHASessionStatus.STARTING) {
this.status = WAHASessionStatus.STARTING;
}
});
events.on(WhatsMeowEvent.KEEP_ALIVE_TIMEOUT, () => {
if (this.status != WAHASessionStatus.STARTING) {
this.status = WAHASessionStatus.STARTING;
}
});
events.on(WhatsMeowEvent.QR_CHANNEL_ITEM, async (data) => {
if (!data.Event) {
return;
}
if (data.Event == 'success') {
return;
}
if (data.Event != 'code') {
this.logger.warn(data, 'Failed QR item event');
this.status = WAHASessionStatus.FAILED;
return;
}
const qr = data.Code;
if (!qr) {
return;
}
this.qr.save(qr);
this.printQR(this.qr);
this.status = WAHASessionStatus.SCAN_QR_CODE;
});
events.on(WhatsMeowEvent.PUSH_NAME_SETTING, (data) => {
this.me = { ...this.me, pushName: data.Action.name };
});
events.on(WhatsMeowEvent.LOGGED_OUT, () => {
this.logger.error('Logged out');
this.status = WAHASessionStatus.FAILED;
});
events.on(WhatsMeowEvent.PRESENCE, (event: gows.Presence) => {
if (isJidGroup(event.From)) {
// So group is not "online"
return;
}
const Chat = event.From;
const Sender = event.From;
const stored = this.presences.get(Chat) || [];
// remove values by event.Sender
const filtered = stored.filter(
(p) => p.Sender !== Sender && p.From !== Sender,
);
// add new value
this.presences.set(Chat, [...filtered, event]);
});
events.on(WhatsMeowEvent.CHAT_PRESENCE, (event: gows.ChatPresence) => {
const Chat = event.Chat;
const Sender = event.Sender;
const stored = this.presences.get(Chat) || [];
// remove values by event.Sender
const filtered = stored.filter(
(p) => p.Sender !== Sender && p.From !== Sender,
);
// add new value
this.presences.set(Chat, [...filtered, event]);
});
events.start();
}
subscribeEngineEvents2() {
const all$ = this.all$;
this.events2.get(WAHAEvents.ENGINE_EVENT).switch(all$);
const messages$ = all$.pipe(onlyEvent(WhatsMeowEvent.MESSAGE));
let [messagesFromMe$, messagesFromOthers$] = partition(messages$, isMine);
messagesFromMe$ = messagesFromMe$.pipe(
mergeMap((msg) => this.processIncomingMessage(msg, true)),
share(), // share it so we don't process twice in message.any
);
messagesFromOthers$ = messagesFromOthers$.pipe(
mergeMap((msg) => this.processIncomingMessage(msg, true)),
share(), // share it so we don't process twice in message.any
);
const messagesFromAll$ = merge(messagesFromMe$, messagesFromOthers$);
this.events2.get(WAHAEvents.MESSAGE).switch(messagesFromOthers$);
this.events2.get(WAHAEvents.MESSAGE_ANY).switch(messagesFromAll$);
const receipt$ = all$.pipe(onlyEvent(WhatsMeowEvent.RECEIPT));
const messageAck$ = receipt$.pipe(
mergeMap(this.receiptToMessageAck.bind(this)),
// Create a composite key for deduplication
groupBy((message) => `${message.id}-${message.ack}`),
mergeMap((group$) =>
group$.pipe(
debounceTime(1000), // Wait 1 second for deduplication
map((message) => message), // Pass the latest message after debounce
),
),
);
this.events2.get(WAHAEvents.MESSAGE_ACK).switch(messageAck$);
const messageReactions$ = messages$.pipe(
filter((msg) => !!msg?.Message?.reactionMessage),
map(this.processMessageReaction.bind(this)),
);
this.events2.get(WAHAEvents.MESSAGE_REACTION).switch(messageReactions$);
const presence$ = all$.pipe(
onlyEvent(WhatsMeowEvent.PRESENCE),
filter((event) => !isJidGroup(event.From)),
);
const chatPresence$ = all$.pipe(onlyEvent(WhatsMeowEvent.CHAT_PRESENCE));
const presenceUpdates$ = merge(presence$, chatPresence$).pipe(
map((event) => this.toWahaPresences(event.From || event.Chat, [event])),
);
this.events2.get(WAHAEvents.PRESENCE_UPDATE).switch(presenceUpdates$);
}
async fetchContactProfilePicture(id: string): Promise<string> {
const jid = toJID(this.ensureSuffix(id));
const request = new messages.ProfilePictureRequest({
jid: jid,
session: this.session,
});
const response = await promisify(this.client.GetProfilePicture)(request);
const url = response.toObject().url;
return url;
}
protected listenEngineEventsInDebugMode() {
this.events2.get(WAHAEvents.ENGINE_EVENT).subscribe((data) => {
this.logger.debug({ events: data }, `GOWS event`);
});
}
async stop(): Promise<void> {
if (this.client) {
const response = await promisify(this.client.StopSession)(this.session);
response.toObject();
}
this.status = WAHASessionStatus.STOPPED;
this.events?.stop();
this.stopEvents();
this.client?.close();
this.mediaManager.close();
}
public async requestCode(phoneNumber: string, method: string, params?: any) {
if (this.status == WAHASessionStatus.STARTING) {
this.logger.debug('Waiting for connection update...');
await waitUntil(
async () => this.status === WAHASessionStatus.SCAN_QR_CODE,
100,
2000,
);
}
if (this.status != WAHASessionStatus.SCAN_QR_CODE) {
const err = `Can request code only in SCAN_QR_CODE status. The current status is ${this.status}`;
throw new UnprocessableEntityException(err);
}
const request = new messages.PairCodeRequest({
session: this.session,
phone: phoneNumber,
});
const response = await promisify(this.client.RequestCode)(request);
const code: string = response.toObject().code;
this.logger.info(`Your code: ${code}`);
return { code: code };
}
async unpair() {
await promisify(this.client.Logout)(this.session);
}
public getSessionMeInfo(): MeInfo | null {
return this.me;
}
/**
* START - Methods for API
*/
/**
* Auth methods
*/
public getQR(): QR {
return this.qr;
}
async getScreenshot(): Promise<Buffer> {
if (this.status === WAHASessionStatus.STARTING) {
throw new UnprocessableEntityException(
`The session is starting, please try again after few seconds`,
);
} else if (this.status === WAHASessionStatus.SCAN_QR_CODE) {
return this.qr.get();
} else if (this.status === WAHASessionStatus.WORKING) {
throw new UnprocessableEntityException(
`Can not get screenshot for non chrome based engine.`,
);
} else {
throw new UnprocessableEntityException(`Unknown status - ${this.status}`);
}
}
async sendText(request: MessageTextRequest) {
const jid = toJID(this.ensureSuffix(request.chatId));
const message = new messages.MessageRequest({
jid: jid,
text: request.text,
session: this.session,
});
const response = await promisify(this.client.SendMessage)(message);
const data = response.toObject();
return this.messageResponse(jid, data);
}
protected messageResponse(jid, data) {
const id = buildMessageId({
ID: data.id,
IsFromMe: true,
IsGroup: isJidGroup(jid),
Chat: jid,
Sender: this.me.id,
});
return {
id: id,
_data: data,
};
}
checkNumberStatus(request: CheckNumberStatusQuery) {
throw new Error('Method not implemented.');
}
sendLocation(request: MessageLocationRequest) {
throw new Error('Method not implemented.');
}
forwardMessage(request: MessageForwardRequest): Promise<WAMessage> {
throw new Error('Method not implemented.');
}
sendImage(request: MessageImageRequest) {
throw new AvailableInPlusVersion();
}
sendFile(request: MessageFileRequest) {
throw new AvailableInPlusVersion();
}
sendVoice(request: MessageVoiceRequest) {
throw new AvailableInPlusVersion();
}
reply(request: MessageReplyRequest) {
throw new Error('Method not implemented.');
}
async sendSeen(request: SendSeenRequest) {
const key = parseMessageIdSerialized(request.messageId);
const req = new messages.MarkReadRequest({
session: this.session,
jid: key.remoteJid,
messageId: key.id,
sender: key.fromMe ? this.me.id : key.participant,
});
const response = await promisify(this.client.MarkRead)(req);
response.toObject();
return;
}
startTyping(chat: ChatRequest) {
throw new Error('Method not implemented.');
}
stopTyping(chat: ChatRequest) {
throw new Error('Method not implemented.');
}
async setReaction(request: MessageReactionRequest) {
const key = parseMessageIdSerialized(request.messageId);
const message = new messages.MessageReaction({
session: this.session,
jid: key.remoteJid,
messageId: key.id,
reaction: request.reaction,
sender: key.fromMe ? this.me.id : key.participant,
});
const response = await promisify(this.client.SendReaction)(message);
const data = response.toObject();
return this.messageResponse(key.remoteJid, data);
}
public async setPresence(presence: WAHAPresenceStatus, chatId?: string) {
let request: any;
let method: any;
const jid = chatId ? toJID(this.ensureSuffix(chatId)) : null;
switch (presence) {
case WAHAPresenceStatus.ONLINE:
request = new messages.PresenceRequest({
session: this.session,
status: messages.Presence.AVAILABLE,
});
method = this.client.SendPresence;
break;
case WAHAPresenceStatus.OFFLINE:
request = new messages.PresenceRequest({
session: this.session,
status: messages.Presence.UNAVAILABLE,
});
method = this.client.SendPresence;
break;
case WAHAPresenceStatus.TYPING:
request = new messages.ChatPresenceRequest({
session: this.session,
jid: jid,
status: messages.ChatPresence.TYPING,
});
method = this.client.SendChatPresence;
break;
case WAHAPresenceStatus.RECORDING:
request = new messages.ChatPresenceRequest({
session: this.session,
jid: jid,
status: messages.ChatPresence.RECORDING,
});
method = this.client.SendChatPresence;
break;
case WAHAPresenceStatus.PAUSED:
request = new messages.ChatPresenceRequest({
session: this.session,
jid: jid,
status: messages.ChatPresence.PAUSED,
});
method = this.client.SendChatPresence;
break;
default:
throw new Error('Invalid presence status');
}
await promisify(method)(request);
}
public async getPresences(): Promise<WAHAChatPresences[]> {
const result: WAHAChatPresences[] = [];
for (const remoteJid in this.presences.keys()) {
const storedPresences = this.presences.get(remoteJid);
result.push(this.toWahaPresences(remoteJid, storedPresences));
}
return result;
}
public async getPresence(chatId: string): Promise<WAHAChatPresences> {
const remoteJid = toJID(chatId);
if (!(remoteJid in this.presences.keys())) {
await this.subscribePresence(remoteJid);
await sleep(1000);
}
const result = this.presences.get(remoteJid) || [];
return this.toWahaPresences(remoteJid, result);
}
async subscribePresence(chatId: string) {
const jid = toJID(chatId);
const req = new messages.SubscribePresenceRequest({
session: this.session,
jid: jid,
});
const response = await promisify(this.client.SubscribePresence)(req);
return response.toObject();
}
protected toWahaPresenceData(
data: gows.Presence | gows.ChatPresence,
): WAHAPresenceData {
if ('From' in data) {
data = data as gows.Presence;
const lastKnownPresence = data.Unavailable
? WAHAPresenceStatus.OFFLINE
: WAHAPresenceStatus.ONLINE;
return {
participant: toCusFormat(data.From),
lastKnownPresence: lastKnownPresence,
lastSeen: parseTimestamp(data.LastSeen),
};
}
data = data as gows.ChatPresence;
let lastKnownPresence: WAHAPresenceStatus;
if (data.State === gows.ChatPresenceState.PAUSED) {
lastKnownPresence = WAHAPresenceStatus.PAUSED;
} else if (
data.State === gows.ChatPresenceState.COMPOSING &&
data.Media === gows.ChatPresenceMedia.TEXT
) {
lastKnownPresence = WAHAPresenceStatus.TYPING;
} else if (
data.State === gows.ChatPresenceState.COMPOSING &&
data.Media === gows.ChatPresenceMedia.AUDIO
) {
lastKnownPresence = WAHAPresenceStatus.RECORDING;
}
return {
participant: toCusFormat(data.Sender),
lastKnownPresence: lastKnownPresence,
lastSeen: null,
};
}
protected toWahaPresences(
jid,
result: null | gows.Presence[] | gows.ChatPresence[],
): WAHAChatPresences {
const chatId = toCusFormat(jid);
return {
id: chatId,
presences: result?.map(this.toWahaPresenceData.bind(this)),
};
}
//
// END - Methods for API
//
private async processIncomingMessage(message, downloadMedia = true) {
// if there is no text or media message
if (!message) return;
if (!message.Message) return;
// Ignore reactions, we have dedicated handler for that
if (message.Message.reactionMessage) return;
// Ignore poll votes, we have dedicated handler for that
if (message.Message.pollUpdateMessage) return;
// Ignore protocol messages
if (message.Message.protocolMessage) return;
if (downloadMedia) {
try {
message = await this.downloadMedia(message);
} catch (e) {
this.logger.error('Failed when tried to download media for a message');
this.logger.error(e, e.stack);
}
}
return this.toWAMessage(message);
}
protected downloadMedia(message) {
const processor = new GOWSEngineMediaProcessor(this);
return this.mediaManager.processMedia(processor, message, this.name);
}
protected toWAMessage(message): WAMessage {
const fromToParticipant = getFromToParticipant(message);
const id = buildMessageId(message);
const body = this.extractBody(message.Message);
const replyTo = null; // TODO: this.extractReplyTo(message.message);
const ack = null; // TODO: Extract
return {
id: id,
timestamp: parseTimestamp(message.Info.Timestamp),
from: toCusFormat(fromToParticipant.from),
fromMe: message.Info.IsFromMe,
body: body,
to: toCusFormat(fromToParticipant.to),
participant: toCusFormat(fromToParticipant.participant),
// Media
hasMedia: Boolean(message.media),
media: message.media,
mediaUrl: message.media?.url,
// @ts-ignore
ack: ack,
// @ts-ignore
ackName: WAMessageAck[ack] || ACK_UNKNOWN,
replyTo: replyTo,
_data: message,
};
}
private extractBody(message) {
if (!message) {
return null;
}
let body = message.Conversation || message.conversation;
if (!body) {
body = message.extendedTextMessage?.text;
}
if (!body) {
const media = extractMediaContent(message);
body = media?.caption;
}
return body;
}
public async getEngineInfo() {
const clientState = this.client?.getChannel().getConnectivityState(false);
const streamState = this.stream$?.client
?.getChannel()
.getConnectivityState(false);
const grpc = {
client: connectivityState[clientState] || 'NO_CLIENT',
stream: connectivityState[streamState] || 'NO_STREAM',
};
if (!this.client) {
return {
grpc: grpc,
};
}
let gows;
try {
const response = await promisify(this.client.GetSessionState)(
this.session,
);
const info = response.toObject();
gows = { ...info };
} catch (err) {
gows = { error: err };
}
return {
grpc: grpc,
gows: gows,
};
}
receiptToMessageAck(receipt: any): WAMessageAckBody[] {
const fromToParticipant = getFromToParticipant(receipt);
let ack;
switch (receipt.Type) {
case '':
ack = WAMessageAck.DEVICE;
break;
case 'server-error':
ack = WAMessageAck.ERROR;
break;
case 'inactive':
ack = WAMessageAck.DEVICE;
break;
case 'active':
ack = WAMessageAck.DEVICE;
break;
case 'read':
ack = WAMessageAck.READ;
break;
case 'played':
ack = WAMessageAck.PLAYED;
break;
default:
return [];
}
const acks = [];
for (const messageId of receipt.MessageIDs) {
const msg = {
...receipt,
ID: messageId,
// Reverse the IsFromMe flag
IsFromMe: !receipt.IsFromMe,
Sender: receipt.MessageSender || this.me?.id,
};
const id = buildMessageId(msg);
const body: WAMessageAckBody = {
id: id,
from: toCusFormat(fromToParticipant.from),
to: toCusFormat(fromToParticipant.to),
participant: toCusFormat(fromToParticipant.participant),
fromMe: msg.IsFromMe,
ack: ack,
ackName: WAMessageAck[ack] || ACK_UNKNOWN,
_data: receipt,
};
acks.push(body);
}
return acks;
}
private processMessageReaction(message): WAMessageReaction | null {
if (!message) return null;
if (!message.Message) return null;
if (!message.Message.reactionMessage) return null;
const id = buildMessageId(message);
const fromToParticipant = getFromToParticipant(message);
const reactionMessage = message.Message.reactionMessage;
const messageId = this.buildMessageIdFromKey(reactionMessage.key);
const reaction: WAMessageReaction = {
id: id,
timestamp: parseTimestamp(message.Info.Timestamp),
from: toCusFormat(fromToParticipant.from),
fromMe: message.Info.IsFromMe,
to: toCusFormat(fromToParticipant.to),
participant: toCusFormat(fromToParticipant.participant),
reaction: {
text: reactionMessage.text,
messageId: messageId,
},
};
return reaction;
}
buildMessageIdFromKey(key: WARawKey) {
const sender = key.fromMe ? this.me.id : key.participant || key.remoteJID;
const info: MessageIdData = {
Chat: key.remoteJID,
Sender: sender,
ID: key.ID,
IsFromMe: key.fromMe,
IsGroup: isJidGroup(key.remoteJID),
};
return buildMessageId(info);
}
}
export class GOWSEngineMediaProcessor implements IMediaEngineProcessor<any> {
constructor(public session: WhatsappSessionGoWSCore) {}
hasMedia(message: any): boolean {
return Boolean(extractMediaContent(message.Message));
}
getChatId(message: any): string {
return toCusFormat(message.Info.Chat);
}
getMessageId(message: any): string {
return message.Info.ID;
}
getMimetype(message: any): string {
const content = extractMediaContent(message.Message);
return content.mimetype;
}
async getMediaBuffer(message: any): Promise<Buffer | null> {
const data = JSON.stringify(message.Message);
const request = new messages.DownloadMediaRequest({
session: this.session.session,
message: data,
});
const response = await promisify(this.session.client.DownloadMedia)(
request,
);
const obj = response.toObject();
return Buffer.from(obj.content);
}
getFilename(message: any): string | null {
const content = extractMediaContent(message.Message);
return content?.fileName;
}
}
export function extractMediaContent(message: any) {
const mediaContent =
message?.documentMessage ||
message?.imageMessage ||
message?.videoMessage ||
message?.audioMessage ||
message?.stickerMessage;
return mediaContent;
}
function isMine(message) {
return !!message.Info.IsFromMe;
}
function getFromToParticipant(message) {
const info = message.Info || message;
return {
from: info.Chat,
to: info.IsGroup ? info.Sender : null,
participant: info.IsGroup ? info.Sender : null,
};
}
interface MessageIdData {
Info?: MessageIdData;
Chat: string;
Sender: string;
ID: string;
IsFromMe: boolean;
IsGroup: boolean;
}
function buildMessageId(message: MessageIdData) {
const info = message.Info || message;
const chatId = toCusFormat(info.Chat);
const participant = toCusFormat(info.Sender);
if (info.IsGroup) {
return `${info.IsFromMe}_${chatId}_${info.ID}_${participant}`;
}
return `${info.IsFromMe}_${chatId}_${info.ID}`;
}
interface WARawKey {
remoteJID: string;
fromMe: boolean;
ID: string;
participant?: string;
}
function parseTimestamp(timestamp: string): number {
if (timestamp.startsWith('0001')) {
return null;
}
// "2024-12-25T14:28:42+03:00" => 1234567890
return new Date(timestamp).getTime();
}
+4
View File
@@ -0,0 +1,4 @@
export interface GowsAuth {
address(): string;
dialect(): string;
}
@@ -0,0 +1,26 @@
import * as path from 'node:path';
import { DataStore } from '@waha/core/abc/DataStore';
import { GowsAuthSimple } from '@waha/core/engines/gows/store/GowsAuthSimple';
import { LocalStore } from '@waha/core/storage/LocalStore';
import { GowsAuth } from './GowsAuth';
export class GowsAuthFactoryCore {
buildAuth(store: DataStore, name: string): Promise<GowsAuth> {
if (store instanceof LocalStore) return this.buildSqlite3(store, name);
throw new Error(`Unsupported store type '${store.constructor.name}'`);
}
protected async buildSqlite3(
store: LocalStore,
name: string,
): Promise<GowsAuth> {
await store.init(name);
const authFolder = store.getSessionDirectory(name);
// resolve path
const authFolderFullPath = path.resolve(authFolder);
const connection = `file:${authFolderFullPath}/gows.db`;
return new GowsAuthSimple(connection, 'sqlite3');
}
}
@@ -0,0 +1,19 @@
import { GowsAuth } from '@waha/core/engines/gows/store/GowsAuth';
export class GowsAuthSimple implements GowsAuth {
private _address: string;
private _dialect: string;
constructor(address: string, dialect: string) {
this._address = address;
this._dialect = dialect;
}
address() {
return this._address;
}
dialect(): string {
return this._dialect;
}
}
+24
View File
@@ -0,0 +1,24 @@
export interface Presence {
From: string;
Unavailable: boolean;
LastSeen: string;
}
export enum ChatPresenceState {
COMPOSING = 'composing',
PAUSED = 'paused',
}
export enum ChatPresenceMedia {
TEXT = '',
AUDIO = 'audio',
}
export interface ChatPresence {
Chat: string;
Sender: string;
IsFromMe: boolean;
IsGroup: boolean;
State: ChatPresenceState;
Media: ChatPresenceMedia;
}
@@ -11,7 +11,7 @@ export class NowebAuthFactoryCore {
protected async buildLocalAuth(store: LocalStore, name: string) {
await store.init(name);
const authFolder = store.getSessionDirectory(name);
const { state, saveCreds } = await useMultiFileAuthState(authFolder);
return { state, saveCreds };
const { state, saveCreds, close } = await useMultiFileAuthState(authFolder);
return { state, saveCreds, close };
}
}
+118 -16
View File
@@ -1,5 +1,6 @@
import makeWASocket, {
Browsers,
Chat,
Contact,
DisconnectReason,
downloadMediaMessage,
@@ -9,7 +10,9 @@ import makeWASocket, {
getKeyAuthor,
getUrlFromDirectPath,
isJidGroup,
isJidNewsletter,
isJidStatusBroadcast,
isJidUser,
isRealMessage,
jidNormalizedUser,
makeCacheableSignalKeyStore,
@@ -50,6 +53,7 @@ import {
ListChannelsQuery,
} from '@waha/structures/channels.dto';
import {
ChatSummary,
GetChatMessageQuery,
GetChatMessagesFilter,
GetChatMessagesQuery,
@@ -161,9 +165,6 @@ import { NowebPersistentStore } from './store/NowebPersistentStore';
import { NowebStorageFactoryCore } from './store/NowebStorageFactoryCore';
import { ensureNumber, extractMediaContent } from './utils';
// eslint-disable-next-line @typescript-eslint/no-var-requires
const QRCode = require('qrcode');
export const BaileysEvents = {
CONNECTION_UPDATE: 'connection.update',
CREDS_UPDATE: 'creds.update',
@@ -369,6 +370,7 @@ export class WhatsappSessionNoWebCore extends WhatsappSession {
this.connectStore();
this.listenConnectionEvents();
this.subscribeEngineEvents2();
this.listenContactsUpdatePictureProfile();
this.enableAutoRestart();
}
@@ -453,8 +455,7 @@ export class WhatsappSessionNoWebCore extends WhatsappSession {
// Save QR
if (qr) {
const url = await QRCode.toDataURL(qr);
this.qr.save(url, qr);
this.qr.save(qr);
this.printQR(this.qr);
this.status = WAHASessionStatus.SCAN_QR_CODE;
}
@@ -466,20 +467,25 @@ export class WhatsappSessionNoWebCore extends WhatsappSession {
this.startDelayedJob.cancel();
this.autoRestartJob.stop();
if (this.authNOWEBStore && this.status == WAHASessionStatus.WORKING) {
const hasCreds = this.authNOWEBStore?.state?.creds;
if (hasCreds && this.status == WAHASessionStatus.WORKING) {
this.logger.info('Saving creds before stopping...');
await this.authNOWEBStore.saveCreds().catch((e) => {
this.logger.error('Failed to save creds');
this.logger.error(e, e.stack);
});
this.logger.info('Creds saved');
this.authNOWEBStore = null;
}
this.status = WAHASessionStatus.STOPPED;
this.stopEvents();
this.mediaManager.close();
await this.end();
await this.store?.close();
this.authNOWEBStore?.close().catch((err) => {
this.logger.error('Failed to close NOWEB auth store');
this.logger.error(err, err.stack);
});
}
protected async failed() {
@@ -626,7 +632,7 @@ export class WhatsappSessionNoWebCore extends WhatsappSession {
`The session is starting, please try again after few seconds`,
);
} else if (this.status === WAHASessionStatus.SCAN_QR_CODE) {
return Promise.resolve(this.qr.get());
return this.qr.get();
} else if (this.status === WAHASessionStatus.WORKING) {
throw new UnprocessableEntityException(
`Can not get screenshot for non chrome based engine.`,
@@ -901,12 +907,52 @@ export class WhatsappSessionNoWebCore extends WhatsappSession {
*/
async getChats(pagination: PaginationParams) {
const chats = await this.store.getChats(pagination);
const chats = await this.store.getChats(pagination, true);
// Remove unreadCount, it's not ready yet
chats.forEach((chat) => delete chat.unreadCount);
return chats;
}
public async getChatsOverview(
pagination: PaginationParams,
): Promise<ChatSummary[]> {
const chats = await this.store.getChats(pagination, false);
// Remove unreadCount, it's not ready yet
chats.forEach((chat) => delete chat.unreadCount);
const promises = [];
for (const chat of chats) {
promises.push(this.fetchChatSummary(chat));
}
const result = await Promise.all(promises);
return result;
}
protected async fetchChatSummary(chat: Chat): Promise<ChatSummary> {
const id = toCusFormat(chat.id);
let name = chat.name;
if (!name) {
// Get name by contact
const jid = toJID(chat.id);
const contact = await this.store.getContactById(jid);
name = contact?.name || contact?.notify;
}
const picture = await this.getContactProfilePicture(chat.id, false);
const messages = await this.getChatMessages(
chat.id,
{ limit: 1, offset: 0, downloadMedia: false },
{},
);
const message = messages.length > 0 ? messages[0] : null;
return {
id: id,
name: name || null,
picture: picture,
lastMessage: message,
_chat: chat,
};
}
protected async chatsPutArchive(
chatId: string,
archive: boolean,
@@ -1062,10 +1108,26 @@ export class WhatsappSessionNoWebCore extends WhatsappSession {
throw new NotImplementedByEngineError();
}
public async getContactProfilePicture(query: ContactQuery) {
const contact = this.ensureSuffix(query.contactId);
const url = await this.sock.profilePictureUrl(contact, 'image');
return { profilePictureURL: url };
public async fetchContactProfilePicture(id: string) {
const contact = this.ensureSuffix(id);
if (isJidNewsletter(id)) {
return null;
}
if (isJidStatusBroadcast(id)) {
return null;
}
try {
const url = await this.sock.profilePictureUrl(contact, 'image');
return url;
} catch (err) {
if (err.message == 'item-not-found') {
return null;
}
if (err.message == 'not-authorized') {
return null;
}
throw err;
}
}
public async blockContact(request: ContactRequest) {
@@ -1084,6 +1146,14 @@ export class WhatsappSessionNoWebCore extends WhatsappSession {
return this.sock.groupCreate(request.name, participants);
}
public joinGroup(code: string) {
return this.sock.groupAcceptInvite(code);
}
public joinInfoGroup(code: string) {
return this.sock.groupGetInviteInfo(code);
}
public async getGroups(pagination: PaginationParams) {
const groups = await this.store.getGroups(pagination);
// return {id: group} mapping for backward compatability
@@ -1178,11 +1248,17 @@ export class WhatsappSessionNoWebCore extends WhatsappSession {
if (!(remoteJid in this.store.presences)) {
this.store.presences[remoteJid] = [];
await this.sock.presenceSubscribe(remoteJid);
await sleep(1000);
}
const result = this.store.presences[remoteJid];
return this.toWahaPresences(remoteJid, result);
}
public subscribePresence(id: string): Promise<void> {
const jid = toJID(id);
return this.sock.presenceSubscribe(jid);
}
/**
* Status methods
*/
@@ -1512,6 +1588,26 @@ export class WhatsappSessionNoWebCore extends WhatsappSession {
);
}
protected listenContactsUpdatePictureProfile() {
this.sock.ev.on('contacts.update', async (updates) => {
for (const update of updates) {
if (update.imgUrl !== 'changed') {
continue;
}
this.logger.debug({ jid: update.id }, 'Profile picture updated');
const url = await this.refreshProfilePicture(update.id);
if (isJidUser(update.id)) {
// update 123@c.us and 123 profiles as well
const cus = toCusFormat(update.id);
this.profilePictures.set(cus, url);
const phone = update.id.split('@')[0];
this.profilePictures.set(phone, url);
}
}
});
}
/**
* END - Methods for API
*/
@@ -1865,6 +1961,10 @@ export class NOWEBEngineMediaProcessor implements IMediaEngineProcessor<any> {
return message.key.id;
}
getChatId(message: any): string {
return toCusFormat(message.key.remoteJid);
}
getMimetype(message: any): string {
const content = extractMediaContent(message.message);
return content.mimetype;
@@ -1904,7 +2004,7 @@ export class NOWEBEngineMediaProcessor implements IMediaEngineProcessor<any> {
/**
* Convert from 11111111111@s.whatsapp.net to 11111111111@c.us
*/
function toCusFormat(remoteJid) {
export function toCusFormat(remoteJid) {
if (!remoteJid) {
return remoteJid;
}
@@ -1926,7 +2026,9 @@ function toCusFormat(remoteJid) {
if (remoteJid == 'me') {
return remoteJid;
}
const number = remoteJid.split('@')[0];
let number = remoteJid.split('@')[0];
// remove :{device} part
number = number.split(':')[0];
return ensureSuffix(number);
}
@@ -1969,7 +2071,7 @@ function buildMessageId({ id, remoteJid, fromMe, participant }: WAMessageKey) {
* false_11111111111@c.us_AAA
* {id: "AAA", remoteJid: "11111111111@s.whatsapp.net", "fromMe": false}
*/
function parseMessageIdSerialized(
export function parseMessageIdSerialized(
messageId: string,
soft: boolean = false,
): WAMessageKey {
@@ -6,7 +6,10 @@ export interface IChatRepository {
getAllByIds(ids: string[]): Promise<Chat[]>;
getAllWithMessages(pagination: PaginationParams): Promise<Chat[]>;
getAllWithMessages(
pagination: PaginationParams,
broadcast: boolean,
): Promise<Chat[]>;
getById(id: string): Promise<Chat | null>;
@@ -1,5 +1,3 @@
import { WAMessage } from '@adiwajshing/baileys';
import { LabelAssociation } from '@adiwajshing/baileys/lib/Types/LabelAssociation';
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';
@@ -24,20 +22,4 @@ export abstract class INowebStorage {
abstract getLabelsRepository(): ILabelsRepository;
abstract getLabelAssociationRepository(): ILabelAssociationRepository;
protected getMessagesMetadata(): Map<string, any> {
return new Map()
.set('jid', (msg: WAMessage) => msg.key.remoteJid)
.set('id', (msg: WAMessage) => msg.key.id)
.set('messageTimestamp', (msg: WAMessage) => msg.messageTimestamp);
}
protected getLabelAssociationMetadata() {
return new Map().set(
'id',
(a: LabelAssociation) =>
// @ts-ignore
`${a.type}_${a.labelId}_${a.chatId}_${a.messageId}`,
);
}
}
+1 -1
View File
@@ -28,7 +28,7 @@ export interface INowebStore {
getMessageById(chatId: string, messageId: string): Promise<any>;
getChats(pagination: PaginationParams): Promise<Chat[]>;
getChats(pagination: PaginationParams, broadcast: boolean): Promise<Chat[]>;
getContacts(pagination: PaginationParams): Promise<Contact[]>;
@@ -61,7 +61,7 @@ export class NowebInMemoryStore implements INowebStore {
throw new BadRequestException(this.errorMessage);
}
getChats(pagination: PaginationParams): Promise<Chat[]> {
getChats(pagination: PaginationParams, broadcast: boolean): Promise<Chat[]> {
throw new BadRequestException(this.errorMessage);
}
@@ -225,7 +225,7 @@ export class NowebPersistentStore implements INowebStore {
private async onChatUpsert(chats: Chat[]) {
for (const chat of chats) {
delete chat['messages'];
chat.conversationTimestamp = toNumber(chat.conversationTimestamp);
chat.conversationTimestamp = toNumber(chat.conversationTimestamp) || null;
await this.chatRepo.save(chat);
}
this.logger.info(`store sync - '${chats.length}' synced chats`);
@@ -312,7 +312,7 @@ export class NowebPersistentStore implements INowebStore {
for (const update of updates) {
const chat = (await this.chatRepo.getById(update.id)) || ({} as Chat);
Object.assign(chat, update);
chat.conversationTimestamp = toNumber(chat.conversationTimestamp);
chat.conversationTimestamp = toNumber(chat.conversationTimestamp) || null;
delete chat['messages'];
await this.chatRepo.save(chat);
}
@@ -343,7 +343,7 @@ export class NowebPersistentStore implements INowebStore {
private async onContactUpdate(updates: Partial<Contact>[]) {
for (const update of updates) {
const contact = await this.contactRepo.getById(update.id);
let contact = await this.contactRepo.getById(update.id);
if (!contact) {
this.logger.warn(
@@ -351,7 +351,7 @@ export class NowebPersistentStore implements INowebStore {
update,
)}'`,
);
continue;
contact = {} as Contact;
// TODO: Find contact by hash if not found
// find contact by attrs.hash, when user is not saved as a contact
// check the in-memory for that
@@ -448,10 +448,10 @@ export class NowebPersistentStore implements INowebStore {
return this.messagesRepo.getByJidById(chatId, messageId);
}
getChats(pagination: PaginationParams): Promise<Chat[]> {
getChats(pagination: PaginationParams, broadcast: boolean): Promise<Chat[]> {
pagination.sortBy ||= 'conversationTimestamp';
pagination.sortOrder ||= SortOrder.DESC;
return this.chatRepo.getAllWithMessages(pagination);
return this.chatRepo.getAllWithMessages(pagination, broadcast);
}
private shouldFetchGroup(): boolean {
@@ -1,9 +1,19 @@
import { LocalStore } from '@waha/core/storage/LocalStore';
import { DataStore } from '../../../abc/DataStore';
import { INowebStorage } from './INowebStorage';
import { Sqlite3Storage } from './sqlite3/Sqlite3Storage';
export class NowebStorageFactoryCore {
createStorage(store: DataStore, name: string): INowebStorage {
return new Sqlite3Storage(':memory:');
if (store instanceof LocalStore) {
return this.buildStorageSqlite3(store, name);
}
throw new Error(`Unsupported store type '${store.constructor.name}'`);
}
private buildStorageSqlite3(store: LocalStore, name: string) {
const filePath = store.getFilePath(name, 'store.sqlite3');
return new Sqlite3Storage(filePath);
}
}
-63
View File
@@ -1,63 +0,0 @@
import { Field, Index, Schema } from '@waha/core/storage/sqlite3/Schema';
export const NOWEB_STORE_SCHEMA = [
new Schema(
'contacts',
[new Field('id', 'TEXT'), new Field('data', 'TEXT')],
[new Index('contacts_id_index', ['id'])],
),
new Schema(
'chats',
[
new Field('id', 'TEXT'),
new Field('conversationTimestamp', 'INTEGER'),
new Field('data', 'TEXT'),
],
[
new Index('chats_id_index', ['id']),
new Index('chats_conversationTimestamp_index', ['conversationTimestamp']),
],
),
new Schema(
'groups',
[new Field('id', 'TEXT'), new Field('data', 'TEXT')],
[new Index('groups_id_index', ['id'])],
),
new Schema(
'messages',
[
new Field('jid', 'TEXT'),
new Field('id', 'TEXT'),
new Field('messageTimestamp', 'INTEGER'),
new Field('data', 'TEXT'),
],
[
new Index('messages_id_index', ['id']),
new Index('messages_jid_id_index', ['jid', 'id']),
new Index('messages_jid_timestamp_index', ['jid', 'messageTimestamp']),
new Index('timestamp_index', ['messageTimestamp']),
],
),
new Schema(
'labels',
[new Field('id', 'TEXT'), new Field('data', 'TEXT')],
[new Index('labels_id_index', ['id'])],
),
new Schema(
'labelAssociations',
[
new Field('id', 'TEXT'),
new Field('type', 'TEXT'),
new Field('labelId', 'TEXT'),
new Field('chatId', 'TEXT'),
new Field('messageId', 'TEXT'),
new Field('data', 'TEXT'),
],
[
new Index('label_assoc_id_index', ['id']),
new Index('label_assoc_type_label_index', ['type', 'labelId']),
new Index('label_assoc_type_chat_index', ['type', 'chatId']),
new Index('label_assoc_type_message_index', ['type', 'messageId']),
],
),
];
+14
View File
@@ -0,0 +1,14 @@
import { WAMessage } from '@adiwajshing/baileys';
import { LabelAssociation } from '@adiwajshing/baileys/lib/Types/LabelAssociation';
export const NowebMessagesMetadata = new Map()
.set('jid', (msg: WAMessage) => msg.key.remoteJid)
.set('id', (msg: WAMessage) => msg.key.id)
.set('messageTimestamp', (msg: WAMessage) => msg.messageTimestamp);
export const NowebLabelAssociationsMetadata = new Map().set(
'id',
(a: LabelAssociation) =>
// @ts-ignore
`${a.type}_${a.labelId}_${a.chatId}_${a.messageId}`,
);
+103
View File
@@ -0,0 +1,103 @@
import { Field, Index, Schema } from '@waha/core/storage/Schema';
export const NowebContactSchema = new Schema(
'contacts',
[new Field('id', 'TEXT'), new Field('data', 'TEXT')],
[new Index('contacts_id_index', ['id'])],
);
export const NowebChatSchema = new Schema(
'chats',
[
new Field('id', 'TEXT'),
new Field('conversationTimestamp', 'INTEGER'),
new Field('data', 'TEXT'),
],
[
new Index('chats_id_index', ['id']),
new Index('chats_conversationTimestamp_index', ['conversationTimestamp']),
],
);
export const NowebGroupsSchema = new Schema(
'groups',
[new Field('id', 'TEXT'), new Field('data', 'TEXT')],
[new Index('groups_id_index', ['id'])],
);
export const NowebMessagesSchema = new Schema(
'messages',
[
new Field('jid', 'TEXT'),
new Field('id', 'TEXT'),
new Field('messageTimestamp', 'INTEGER'),
new Field('data', 'TEXT'),
],
[
new Index('messages_id_index', ['id']),
new Index('messages_jid_id_index', ['jid', 'id']),
new Index('messages_jid_timestamp_index', ['jid', 'messageTimestamp']),
new Index('timestamp_index', ['messageTimestamp']),
],
);
export const NowebLabelsSchema = new Schema(
'labels',
[new Field('id', 'TEXT'), new Field('data', 'TEXT')],
[new Index('labels_id_index', ['id'])],
);
export const NowebLabelAssociationsSchema = new Schema(
'labelAssociations',
[
new Field('id', 'TEXT'),
new Field('type', 'TEXT'),
new Field('labelId', 'TEXT'),
new Field('chatId', 'TEXT'),
new Field('messageId', 'TEXT'),
new Field('data', 'TEXT'),
],
[
new Index('label_assoc_id_index', ['id']),
new Index('label_assoc_type_label_index', ['type', 'labelId']),
new Index('label_assoc_type_chat_index', ['type', 'chatId']),
new Index('label_assoc_type_message_index', ['type', 'messageId']),
],
);
export const NOWEB_STORE_SCHEMA = [
NowebContactSchema,
NowebChatSchema,
NowebGroupsSchema,
NowebMessagesSchema,
NowebLabelsSchema,
NowebLabelAssociationsSchema,
];
export const Migrations: string[] = [
// Contacts
'CREATE TABLE IF NOT EXISTS contacts (id TEXT PRIMARY KEY, data TEXT)',
'CREATE UNIQUE INDEX IF NOT EXISTS contacts_id_index ON contacts (id)',
// Chats
'CREATE TABLE IF NOT EXISTS chats (id TEXT PRIMARY KEY, "conversationTimestamp" INTEGER, data TEXT)',
'CREATE UNIQUE INDEX IF NOT EXISTS chats_id_index ON chats (id)',
'CREATE INDEX IF NOT EXISTS "chats_conversationTimestamp_index" ON chats ("conversationTimestamp")',
// Groups
'CREATE TABLE IF NOT EXISTS groups (id TEXT PRIMARY KEY, data TEXT)',
'CREATE UNIQUE INDEX IF NOT EXISTS groups_id_index ON groups (id)',
// Messages
'CREATE TABLE IF NOT EXISTS messages (jid TEXT, id TEXT, "messageTimestamp" INTEGER, data TEXT)',
'CREATE UNIQUE INDEX IF NOT EXISTS messages_id_index ON messages (id)',
'CREATE INDEX IF NOT EXISTS messages_jid_id_index ON messages (jid, id)',
'CREATE INDEX IF NOT EXISTS messages_jid_timestamp_index ON messages (jid, "messageTimestamp")',
'CREATE INDEX IF NOT EXISTS timestamp_index ON messages ("messageTimestamp")',
// Labels
'CREATE TABLE IF NOT EXISTS labels (id TEXT PRIMARY KEY, data TEXT)',
'CREATE UNIQUE INDEX IF NOT EXISTS labels_id_index ON labels (id)',
// Label associations
'CREATE TABLE IF NOT EXISTS "labelAssociations" (id TEXT PRIMARY KEY, type TEXT, "labelId" TEXT, "chatId" TEXT, "messageId" TEXT, data TEXT)',
'CREATE UNIQUE INDEX IF NOT EXISTS label_assoc_id_index ON "labelAssociations" (id)',
'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")',
];
@@ -0,0 +1,23 @@
import { Chat } from '@adiwajshing/baileys';
import { SqlKVRepository } from '@waha/core/storage/sql/SqlKVRepository';
import { PaginationParams } from '@waha/structures/pagination.dto';
export class SqlChatMethods {
constructor(private repository: SqlKVRepository<any>) {}
async getAllWithMessages(
pagination: PaginationParams,
broadcast: boolean,
): 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');
}
query = this.repository.pagination(query, pagination);
return await this.repository.all(query);
}
}
@@ -0,0 +1,40 @@
import {
LabelAssociation,
LabelAssociationType,
} from '@adiwajshing/baileys/lib/Types/LabelAssociation';
import { SqlKVRepository } from '@waha/core/storage/sql/SqlKVRepository';
export class SqlLabelAssociationsMethods {
constructor(private repository: SqlKVRepository<any>) {}
async deleteOne(association: LabelAssociation): Promise<void> {
await this.repository.deleteBy({
type: association.type,
chatId: association.chatId,
labelId: association.labelId,
// @ts-ignore: messageId doesn't existing in ChatLabelAssociation
messageId: association.messageId || null,
});
}
async deleteByLabelId(labelId: string): Promise<void> {
await this.repository.deleteBy({ labelId: labelId });
}
getAssociationsByLabelId(
labelId: string,
type: LabelAssociationType,
): Promise<LabelAssociation[]> {
return this.repository.getAllBy({
type: type,
labelId: labelId,
});
}
getAssociationsByChatId(chatId: string): Promise<LabelAssociation[]> {
return this.repository.getAllBy({
chatId: chatId,
type: LabelAssociationType.Chat,
});
}
}
@@ -0,0 +1,73 @@
import { ALL_JID } from '@waha/core/engines/noweb/session.noweb.core';
import { SqlKVRepository } from '@waha/core/storage/sql/SqlKVRepository';
import { GetChatMessagesFilter } from '@waha/structures/chats.dto';
import { PaginationParams } from '@waha/structures/pagination.dto';
export class SqlMessagesMethods {
constructor(private repository: SqlKVRepository<any>) {}
upsert(messages: any[]): Promise<void> {
return this.repository.upsertMany(messages);
}
async getAllByJid(
jid: string,
filter: GetChatMessagesFilter,
pagination: PaginationParams,
): Promise<any[]> {
let query = this.repository.select();
if (jid !== ALL_JID) {
query = this.repository.select().where({ jid: jid });
}
if (filter['filter.timestamp.lte'] != null) {
query = query.where(
'messageTimestamp',
'<=',
filter['filter.timestamp.lte'],
);
}
if (filter['filter.timestamp.gte'] != null) {
query = query.where(
'messageTimestamp',
'>=',
filter['filter.timestamp.gte'],
);
}
if (filter['filter.fromMe'] != null) {
// filter by data json inside
const sql = this.repository.filterJson('data', 'key.fromMe');
query = query.whereRaw(sql, [filter['filter.fromMe'] ? 'true' : 'false']);
}
query = this.repository.pagination(query, pagination);
return this.repository.all(query);
}
async getByJidById(jid: string, id: string): Promise<any> {
return this.repository.getBy({ jid: jid, id: id });
}
async updateByJidAndId(
jid: string,
id: string,
update: any,
): Promise<boolean> {
const entity = await this.getByJidById(jid, id);
if (!entity) {
return false;
}
Object.assign(entity, update);
await this.repository.upsertOne(entity);
}
async deleteByJidByIds(jid: string, ids: string[]): Promise<void> {
const query = `DELETE
FROM "${this.repository.table}"
WHERE jid = ?
AND id IN (${ids.map(() => '?').join(', ')})`;
await this.repository.raw(query, [jid, ...ids]);
}
deleteAllByJid(jid: string): Promise<void> {
return this.repository.deleteBy({ jid: jid });
}
}
@@ -1,4 +1,6 @@
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 { PaginationParams } from '@waha/structures/pagination.dto';
import { KnexPaginator } from '@waha/utils/Paginator';
@@ -15,10 +17,18 @@ export class Sqlite3ChatRepository
{
protected Paginator = ChatPaginator;
async getAllWithMessages(pagination: PaginationParams): Promise<Chat[]> {
// Get chats with conversationTimestamp is not Null
let query = this.select().whereNotNull('conversationTimestamp');
query = this.pagination(query, pagination);
return await this.all(query);
get schema() {
return NowebChatSchema;
}
get methods() {
return new SqlChatMethods(this);
}
getAllWithMessages(
pagination: PaginationParams,
broadcast: boolean,
): Promise<Chat[]> {
return this.methods.getAllWithMessages(pagination, broadcast);
}
}
@@ -1,4 +1,5 @@
import { Contact } from '@adiwajshing/baileys';
import { NowebContactSchema } from '@waha/core/engines/noweb/store/schemas';
import { KnexPaginator } from '@waha/utils/Paginator';
import { IContactRepository } from '../IContactRepository';
@@ -13,4 +14,8 @@ export class Sqlite3ContactRepository
implements IContactRepository
{
protected Paginator = ContactPaginator;
get schema() {
return NowebContactSchema;
}
}
@@ -1,5 +1,6 @@
import { GroupMetadata } from '@adiwajshing/baileys/lib/Types/GroupMetadata';
import { IGroupRepository } from '@waha/core/engines/noweb/store/IGroupRepository';
import { NowebGroupsSchema } from '@waha/core/engines/noweb/store/schemas';
import { KnexPaginator } from '@waha/utils/Paginator';
import { NOWEBSqlite3KVRepository } from './NOWEBSqlite3KVRepository';
@@ -13,4 +14,8 @@ export class Sqlite3GroupRepository
implements IGroupRepository
{
protected Paginator = Paginator;
get schema() {
return NowebGroupsSchema;
}
}
@@ -3,6 +3,9 @@ import {
LabelAssociationType,
} from '@adiwajshing/baileys/lib/Types/LabelAssociation';
import { ILabelAssociationRepository } from '@waha/core/engines/noweb/store/ILabelAssociationsRepository';
import { NowebLabelAssociationsMetadata } from '@waha/core/engines/noweb/store/metadata';
import { NowebLabelAssociationsSchema } from '@waha/core/engines/noweb/store/schemas';
import { SqlLabelAssociationsMethods } from '@waha/core/engines/noweb/store/sql/SqlLabelAssociationsMethods';
import { NOWEBSqlite3KVRepository } from './NOWEBSqlite3KVRepository';
@@ -10,31 +13,34 @@ export class Sqlite3LabelAssociationsRepository
extends NOWEBSqlite3KVRepository<LabelAssociation>
implements ILabelAssociationRepository
{
get schema() {
return NowebLabelAssociationsSchema;
}
get metadata() {
return NowebLabelAssociationsMetadata;
}
get methods() {
return new SqlLabelAssociationsMethods(this);
}
async deleteOne(association: LabelAssociation): Promise<void> {
await this.deleteBy({
type: association.type,
chatId: association.chatId,
labelId: association.labelId,
// @ts-ignore: messageId doesn't existing in ChatLabelAssociation
messageId: association.messageId || null,
});
return this.methods.deleteOne(association);
}
async deleteByLabelId(labelId: string): Promise<void> {
await this.deleteBy({ labelId: labelId });
return this.methods.deleteByLabelId(labelId);
}
getAssociationsByLabelId(
labelId: string,
type: LabelAssociationType,
): Promise<LabelAssociation[]> {
return this.getAllBy({
type: type,
labelId: labelId,
});
return this.methods.getAssociationsByLabelId(labelId, type);
}
getAssociationsByChatId(chatId: string): Promise<LabelAssociation[]> {
return this.getAllBy({ chatId: chatId, type: LabelAssociationType.Chat });
return this.methods.getAssociationsByChatId(chatId);
}
}
@@ -1,8 +1,14 @@
import { Label } from '@adiwajshing/baileys/lib/Types/Label';
import { ILabelsRepository } from '@waha/core/engines/noweb/store/ILabelsRepository';
import { NowebLabelsSchema } from '@waha/core/engines/noweb/store/schemas';
import { NOWEBSqlite3KVRepository } from './NOWEBSqlite3KVRepository';
export class Sqlite3LabelsRepository
extends NOWEBSqlite3KVRepository<Label>
implements ILabelsRepository {}
implements ILabelsRepository
{
get schema() {
return NowebLabelsSchema;
}
}
@@ -1,4 +1,6 @@
import { ALL_JID } from '@waha/core/engines/noweb/session.noweb.core';
import { NowebMessagesMetadata } from '@waha/core/engines/noweb/store/metadata';
import { NowebMessagesSchema } from '@waha/core/engines/noweb/store/schemas';
import { SqlMessagesMethods } from '@waha/core/engines/noweb/store/sql/SqlMessagesMethods';
import { GetChatMessagesFilter } from '@waha/structures/chats.dto';
import { PaginationParams } from '@waha/structures/pagination.dto';
@@ -9,8 +11,20 @@ export class Sqlite3MessagesRepository
extends NOWEBSqlite3KVRepository<any>
implements IMessagesRepository
{
get schema() {
return NowebMessagesSchema;
}
get methods() {
return new SqlMessagesMethods(this);
}
get metadata() {
return NowebMessagesMetadata;
}
upsert(messages: any[]): Promise<void> {
return this.upsertMany(messages);
return this.methods.upsert(messages);
}
async getAllByJid(
@@ -18,36 +32,11 @@ export class Sqlite3MessagesRepository
filter: GetChatMessagesFilter,
pagination: PaginationParams,
): Promise<any[]> {
let query = this.select();
if (jid !== ALL_JID) {
query = this.select().where({ jid: jid });
}
if (filter['filter.timestamp.lte'] != null) {
query = query.where(
'messageTimestamp',
'<=',
filter['filter.timestamp.lte'],
);
}
if (filter['filter.timestamp.gte'] != null) {
query = query.where(
'messageTimestamp',
'>=',
filter['filter.timestamp.gte'],
);
}
if (filter['filter.fromMe'] != null) {
// filter by data->"$.key.fromMe"
query = query.whereRaw("data->'$.key.fromMe' = ?", [
filter['filter.fromMe'] ? 'true' : 'false',
]);
}
query = this.pagination(query, pagination);
return this.all(query);
return this.methods.getAllByJid(jid, filter, pagination);
}
async getByJidById(jid: string, id: string): Promise<any> {
return this.getBy({ jid: jid, id: id });
return this.methods.getByJidById(jid, id);
}
async updateByJidAndId(
@@ -55,23 +44,14 @@ export class Sqlite3MessagesRepository
id: string,
update: any,
): Promise<boolean> {
const entity = await this.getByJidById(jid, id);
if (!entity) {
return false;
}
Object.assign(entity, update);
await this.upsertOne(entity);
return this.methods.updateByJidAndId(jid, id, update);
}
async deleteByJidByIds(jid: string, ids: string[]): Promise<void> {
const query = `DELETE
FROM ${this.table}
WHERE jid = ?
AND id IN (${ids.map(() => '?').join(', ')})`;
this.db.prepare(query).run([jid, ...ids]);
return this.methods.deleteByJidByIds(jid, ids);
}
deleteAllByJid(jid: string): Promise<void> {
return this.deleteBy({ jid: jid });
return this.methods.deleteAllByJid(jid);
}
}
@@ -1,4 +1,4 @@
import { Schema } from '@waha/core/storage/sqlite3/Schema';
import { Schema } from '@waha/core/storage/Schema';
// eslint-disable-next-line @typescript-eslint/no-var-requires
const Database = require('better-sqlite3');
@@ -1,14 +1,12 @@
import { WAMessage } from '@adiwajshing/baileys';
import { LabelAssociation } from '@adiwajshing/baileys/lib/Types/LabelAssociation';
import { ILabelAssociationRepository } from '@waha/core/engines/noweb/store/ILabelAssociationsRepository';
import { ILabelsRepository } from '@waha/core/engines/noweb/store/ILabelsRepository';
import { 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 { Field, Index, Schema } from '@waha/core/storage/sqlite3/Schema';
import { Schema } from '@waha/core/storage/Schema';
import { INowebStorage } from '../INowebStorage';
import { NOWEB_STORE_SCHEMA } from '../Schema';
import { Migrations, NOWEB_STORE_SCHEMA } from '../schemas';
import { Sqlite3ChatRepository } from './Sqlite3ChatRepository';
import { Sqlite3ContactRepository } from './Sqlite3ContactRepository';
import { Sqlite3MessagesRepository } from './Sqlite3MessagesRepository';
@@ -44,121 +42,36 @@ export class Sqlite3Storage extends INowebStorage {
}
private migration0001init() {
// Contacts
this.db.exec(
'CREATE TABLE IF NOT EXISTS contacts (id TEXT PRIMARY KEY, data TEXT)',
);
this.db.exec(
'CREATE UNIQUE INDEX IF NOT EXISTS contacts_id_index ON contacts (id)',
);
// Chats
this.db.exec(
'CREATE TABLE IF NOT EXISTS chats (id TEXT PRIMARY KEY, conversationTimestamp INTEGER, data TEXT)',
);
this.db.exec(
'CREATE UNIQUE INDEX IF NOT EXISTS chats_id_index ON chats (id)',
);
this.db.exec(
'CREATE INDEX IF NOT EXISTS chats_conversationTimestamp_index ON chats (conversationTimestamp)',
);
// Groups
this.db.exec(
'CREATE TABLE IF NOT EXISTS groups (id TEXT PRIMARY KEY, data TEXT)',
);
this.db.exec(
'CREATE UNIQUE INDEX IF NOT EXISTS groups_id_index ON groups (id)',
);
// Messages
this.db.exec(
'CREATE TABLE IF NOT EXISTS messages (jid TEXT, id TEXT, messageTimestamp INTEGER, data TEXT)',
);
this.db.exec(
'CREATE UNIQUE INDEX IF NOT EXISTS messages_id_index ON messages (id)',
);
this.db.exec(
'CREATE INDEX IF NOT EXISTS messages_jid_id_index ON messages (jid, id)',
);
this.db.exec(
'CREATE INDEX IF NOT EXISTS messages_jid_timestamp_index ON messages (jid, messageTimestamp)',
);
this.db.exec(
'CREATE INDEX IF NOT EXISTS timestamp_index ON messages (messageTimestamp)',
);
//
// Labels
//
this.db.exec(
'CREATE TABLE IF NOT EXISTS labels (id TEXT PRIMARY KEY, data TEXT)',
);
this.db.exec(
'CREATE UNIQUE INDEX IF NOT EXISTS labels_id_index ON labels (id)',
);
// Label associations
this.db.exec(
'CREATE TABLE IF NOT EXISTS labelAssociations (id TEXT PRIMARY KEY, type TEXT, labelId TEXT, chatId TEXT, messageId TEXT, data TEXT)',
);
this.db.exec(
'CREATE UNIQUE INDEX IF NOT EXISTS label_assoc_id_index ON labelAssociations (id)',
);
this.db.exec(
'CREATE INDEX IF NOT EXISTS label_assoc_type_label_index ON labelAssociations (type, labelId)',
);
this.db.exec(
'CREATE INDEX IF NOT EXISTS label_assoc_type_chat_index ON labelAssociations (type, chatId)',
);
this.db.exec(
'CREATE INDEX IF NOT EXISTS label_assoc_type_message_index ON labelAssociations (type, messageId)',
);
for (const migration of Migrations) {
this.db.exec(migration);
}
}
async close() {
this.db.close();
return this.db.close();
}
getContactsRepository() {
return new Sqlite3ContactRepository(this.db, this.getSchema('contacts'));
return new Sqlite3ContactRepository(this.db);
}
getChatRepository() {
return new Sqlite3ChatRepository(this.db, this.getSchema('chats'));
return new Sqlite3ChatRepository(this.db);
}
getGroupRepository() {
return new Sqlite3GroupRepository(this.db, this.getSchema('groups'));
return new Sqlite3GroupRepository(this.db);
}
getLabelsRepository(): ILabelsRepository {
return new Sqlite3LabelsRepository(this.db, this.getSchema('labels'));
return new Sqlite3LabelsRepository(this.db);
}
getLabelAssociationRepository(): ILabelAssociationRepository {
const metadata = this.getLabelAssociationMetadata();
return new Sqlite3LabelAssociationsRepository(
this.db,
this.getSchema('labelAssociations'),
metadata,
);
return new Sqlite3LabelAssociationsRepository(this.db);
}
getMessagesRepository() {
const metadata = this.getMessagesMetadata();
return new Sqlite3MessagesRepository(
this.db,
this.getSchema('messages'),
metadata,
);
}
getSchema(name: string) {
const schema = this.tables.find((table) => table.name === name);
if (!schema) {
throw new Error(`Schema not found: ${name}`);
}
return schema;
return new Sqlite3MessagesRepository(this.db);
}
}
@@ -4,9 +4,12 @@ import {
proto,
} from '@adiwajshing/baileys';
import { BufferJSON, initAuthCreds } from '@adiwajshing/baileys/lib/Utils';
import { mkdir, readFile, stat, unlink, writeFile } from 'fs/promises';
import { mkdir, readFile, stat, unlink } from 'fs/promises';
import { join } from 'path';
// eslint-disable-next-line @typescript-eslint/no-var-requires
const writeFileAtomic = require('write-file-atomic');
// eslint-disable-next-line @typescript-eslint/no-var-requires
const AsyncLock = require('async-lock');
@@ -29,11 +32,15 @@ export const useMultiFileAuthState = async (
): Promise<{
state: AuthenticationState;
saveCreds: () => Promise<void>;
close: () => Promise<void>;
}> => {
const writeData = (data: any, file: string) => {
const filePath = join(folder, fixFileName(file));
return fileLock.acquire(filePath, () =>
writeFile(join(filePath), JSON.stringify(data, BufferJSON.replacer)),
writeFileAtomic(
join(filePath),
JSON.stringify(data, BufferJSON.replacer),
),
);
};
@@ -111,5 +118,8 @@ export const useMultiFileAuthState = async (
saveCreds: () => {
return writeData(creds, 'creds.json');
},
close: async () => {
return;
},
};
};
+59 -20
View File
@@ -13,7 +13,6 @@ import { IMediaEngineProcessor } from '@waha/core/media/IMediaEngineProcessor';
import { QR } from '@waha/core/QR';
import { splitAt } from '@waha/helpers';
import { PairingCodeResponse } from '@waha/structures/auth.dto';
import { CallData } from '@waha/structures/calls.dto';
import {
Channel,
ChannelRole,
@@ -21,8 +20,8 @@ import {
ListChannelsQuery,
} from '@waha/structures/channels.dto';
import {
ChatArchiveEvent,
ChatSortField,
ChatSummary,
GetChatMessageQuery,
GetChatMessagesFilter,
GetChatMessagesQuery,
@@ -61,14 +60,10 @@ import {
} from '@waha/structures/groups.dto';
import { Label, LabelDTO, LabelID } from '@waha/structures/labels.dto';
import { ReplyToMessage } from '@waha/structures/message.dto';
import { PaginationParams } from '@waha/structures/pagination.dto';
import { PaginationParams, SortOrder } from '@waha/structures/pagination.dto';
import { WAMessage, WAMessageReaction } from '@waha/structures/responses.dto';
import { MeInfo } from '@waha/structures/sessions.dto';
import {
BROADCAST_ID,
StatusRequest,
TextStatus,
} from '@waha/structures/status.dto';
import { StatusRequest, TextStatus } from '@waha/structures/status.dto';
import {
EnginePayload,
WAMessageRevokedBody,
@@ -79,7 +74,7 @@ import { SingleDelayedJobRunner } from '@waha/utils/SingleDelayedJobRunner';
import * as lodash from 'lodash';
import { fromEvent, merge, mergeMap, Observable } from 'rxjs';
import { map } from 'rxjs/operators';
import {
import WAWebJS, {
Call,
Channel as WEBJSChannel,
Chat,
@@ -96,9 +91,6 @@ import {
} from 'whatsapp-web.js';
import { Message as MessageInstance } from 'whatsapp-web.js/src/structures';
// eslint-disable-next-line @typescript-eslint/no-var-requires
const QRCode = require('qrcode');
export interface WebJSConfig {
webVersion?: string;
cacheType: 'local' | 'none';
@@ -169,9 +161,10 @@ export class WhatsappSessionWebJSCore extends WhatsappSession {
protected async buildClient() {
const clientOptions = this.getClientOptions();
const base = process.env.WAHA_LOCAL_STORE_BASE_DIR || './.sessions';
clientOptions.authStrategy = new LocalAuth({
clientId: this.name,
dataPath: undefined,
dataPath: `${base}/webjs/default`,
logger: this.logger,
rmMaxRetries: undefined,
});
@@ -271,6 +264,7 @@ export class WhatsappSessionWebJSCore extends WhatsappSession {
this.status = WAHASessionStatus.STOPPED;
this.stopEvents();
this.startDelayedJob.cancel();
this.mediaManager.close();
await this.end();
}
@@ -340,8 +334,7 @@ export class WhatsappSessionWebJSCore extends WhatsappSession {
this.whatsapp.on(Events.QR_RECEIVED, async (qr) => {
this.logger.debug('QR received');
// Convert to image and save
const url = await QRCode.toDataURL(qr);
this.qr.save(url, qr);
this.qr.save(qr);
this.printQR(this.qr);
this.status = WAHASessionStatus.SCAN_QR_CODE;
});
@@ -573,6 +566,41 @@ export class WhatsappSessionWebJSCore extends WhatsappSession {
return this.whatsapp.getChats(pagination);
}
public async getChatsOverview(
pagination: PaginationParams,
): Promise<ChatSummary[]> {
pagination = {
...pagination,
sortBy: ChatSortField.CONVERSATION_TIMESTAMP,
sortOrder: SortOrder.DESC,
};
const chats = await this.getChats(pagination);
const promises = [];
for (const chat of chats) {
promises.push(this.fetchChatSummary(chat));
}
const result = await Promise.all(promises);
return result;
}
protected async fetchChatSummary(chat: Chat): Promise<ChatSummary> {
const picture = await this.getContactProfilePicture(
chat.id._serialized,
false,
);
const lastMessage = !!chat.lastMessage
? this.toWAMessage(chat.lastMessage)
: null;
return {
id: chat.id._serialized,
name: chat.name || null,
picture: picture,
lastMessage: lastMessage,
_chat: chat,
};
}
public async getChatMessages(
chatId: string,
query: GetChatMessagesQuery,
@@ -731,11 +759,10 @@ export class WhatsappSessionWebJSCore extends WhatsappSession {
return { about: await contact.getAbout() };
}
public async getContactProfilePicture(query: ContactQuery) {
const contact = await this.whatsapp.getContactById(
this.ensureSuffix(query.contactId),
);
return { profilePictureURL: await contact.getProfilePicUrl() };
public async fetchContactProfilePicture(id: string) {
const contact = await this.whatsapp.getContactById(this.ensureSuffix(id));
const url = await contact.getProfilePicUrl();
return url;
}
public async blockContact(request: ContactRequest) {
@@ -762,6 +789,14 @@ export class WhatsappSessionWebJSCore extends WhatsappSession {
return this.whatsapp.createGroup(request.name, participantIds);
}
public joinGroup(code: string) {
return this.whatsapp.acceptInvite(code);
}
public joinInfoGroup(code: string) {
return this.whatsapp.getInviteInfo(code);
}
public async getInfoAdminsOnly(id): Promise<SettingsSecurityChangeInfo> {
const groupChat = (await this.whatsapp.getChatById(id)) as GroupChat;
return {
@@ -1287,6 +1322,10 @@ export class WEBJSEngineMediaProcessor
return message.type !== 'revoked';
}
getChatId(message: Message): string {
return message.id.remote;
}
getMessageId(message: Message): string {
return message.id._serialized;
}
+34 -9
View File
@@ -1,9 +1,13 @@
import {
Injectable,
NotFoundException,
OnModuleInit,
UnprocessableEntityException,
} from '@nestjs/common';
import { EngineBootstrap } from '@waha/core/abc/EngineBootstrap';
import { GowsEngineConfigService } from '@waha/core/config/GowsEngineConfigService';
import { WebJSEngineConfigService } from '@waha/core/config/WebJSEngineConfigService';
import { WhatsappSessionGoWSCore } from '@waha/core/engines/gows/session.gows.core';
import { WebhookConductor } from '@waha/core/integrations/webhooks/WebhookConductor';
import { MediaStorageFactory } from '@waha/core/media/MediaStorageFactory';
import { DefaultMap } from '@waha/utils/DefaultMap';
@@ -54,7 +58,7 @@ enum DefaultSessionStatus {
}
@Injectable()
export class SessionManagerCore extends SessionManager {
export class SessionManagerCore extends SessionManager implements OnModuleInit {
SESSION_STOP_TIMEOUT = 3000;
// session - exists and running (or failed or smth)
@@ -66,19 +70,22 @@ export class SessionManagerCore extends SessionManager {
protected readonly EngineClass: typeof WhatsappSession;
protected events2: DefaultMap<WAHAEvents, SwitchObservable<any>>;
protected readonly engineBootstrap: EngineBootstrap;
constructor(
config: WhatsappConfigService,
private engineConfigService: EngineConfigService,
private webjsEngineConfigService: WebJSEngineConfigService,
gowsConfigService: GowsEngineConfigService,
log: PinoLogger,
private mediaStorageFactory: MediaStorageFactory,
) {
super(config, log);
super(log, config, gowsConfigService);
this.session = DefaultSessionStatus.STOPPED;
this.sessionConfig = null;
const engineName = this.engineConfigService.getDefaultEngineName();
this.EngineClass = this.getEngine(engineName);
this.engineBootstrap = this.getEngineBootstrap(engineName);
this.events2 = new DefaultMap<WAHAEvents, SwitchObservable<any>>(
(key) =>
@@ -89,7 +96,6 @@ export class SessionManagerCore extends SessionManager {
this.store = new LocalStoreCore(engineName.toLowerCase());
this.sessionAuthRepository = new LocalSessionAuthRepository(this.store);
this.startPredefinedSessions();
this.clearStorage().catch((error) => {
this.log.error({ error }, 'Error while clearing storage');
});
@@ -100,6 +106,8 @@ export class SessionManagerCore extends SessionManager {
return WhatsappSessionWebJSCore;
} else if (engine === WAHAEngine.NOWEB) {
return WhatsappSessionNoWebCore;
} else if (engine === WAHAEngine.GOWS) {
return WhatsappSessionGoWSCore;
} else {
throw new NotFoundException(`Unknown whatsapp engine '${engine}'.`);
}
@@ -112,15 +120,21 @@ export class SessionManagerCore extends SessionManager {
}
async beforeApplicationShutdown(signal?: string) {
this.stopEvents();
if (!this.session) {
return;
if (this.session) {
await this.stop(this.DEFAULT, true);
}
await this.stop(this.DEFAULT, true);
this.stopEvents();
await this.engineBootstrap.shutdown();
}
async onApplicationBootstrap() {
await this.engineBootstrap.bootstrap();
this.startPredefinedSessions();
}
private async clearStorage() {
const storage = this.mediaStorageFactory.build(
const storage = await this.mediaStorageFactory.build(
'all',
this.log.logger.child({ name: 'Storage' }),
);
await storage.purge();
@@ -156,7 +170,8 @@ export class SessionManagerCore extends SessionManager {
logger.level = getPinoLogLevel(this.sessionConfig?.debug);
const loggerBuilder: LoggerBuilder = logger;
const storage = this.mediaStorageFactory.build(
const storage = await this.mediaStorageFactory.build(
name,
loggerBuilder.child({ name: 'Storage' }),
);
await storage.init();
@@ -179,6 +194,8 @@ export class SessionManagerCore extends SessionManager {
};
if (this.EngineClass === WhatsappSessionWebJSCore) {
sessionConfig.engineConfig = this.webjsEngineConfigService.getConfig();
} else if (this.EngineClass === WhatsappSessionGoWSCore) {
sessionConfig.engineConfig = this.gowsConfigService.getConfig();
}
await this.sessionAuthRepository.init(name);
// @ts-ignore
@@ -374,4 +391,12 @@ export class SessionManagerCore extends SessionManager {
protected stopEvents() {
complete(this.events2);
}
async onModuleInit() {
await this.init();
}
async init() {
await this.store.init();
}
}
+2
View File
@@ -11,6 +11,8 @@ interface IMediaEngineProcessor<Message> {
getMessageId(message: Message): string;
getChatId(message: Message): string;
getMediaBuffer(message: Message): Promise<Buffer | null>;
}
+1
View File
@@ -10,6 +10,7 @@ interface IMediaManager {
message: Message,
session: string,
): Promise<Message>;
close(): void;
}
export { IMediaManager };
+16
View File
@@ -6,6 +6,9 @@ import { S3MediaData } from '@waha/structures/media.s3.dto';
interface Message {
// false_111111111@c.us_AAAAAAAAAAAAAA
id: string;
// 1111111@c.us
chatId: string;
}
interface File {
@@ -37,6 +40,19 @@ abstract class IMediaStorage {
abstract getStorageData(data: MediaData): Promise<MediaStorageData>;
abstract purge(): Promise<void>;
abstract close(): Promise<void>;
}
export function getMetadata(data: MediaData): any {
const now = new Date();
return {
'waha-session': data.session,
'waha-chat-id': data.message.chatId,
'waha-message-id': data.message.id,
'waha-media-filename': data.file.filename,
'waha-created-at': now.getTime(),
};
}
export { IMediaStorage };
+8
View File
@@ -52,6 +52,7 @@ export class MediaManager implements IMediaManager {
session: string,
): Promise<WAMedia | null> {
const messageId = processor.getMessageId(message);
const chatId = processor.getChatId(message);
const mimetype = processor.getMimetype(message);
const filename = processor.getFilename(message);
if (!this.shouldProcessMimetype(mimetype)) {
@@ -66,6 +67,7 @@ export class MediaManager implements IMediaManager {
session: session,
message: {
id: messageId,
chatId: chatId,
},
file: {
extension: extension,
@@ -195,4 +197,10 @@ export class MediaManager implements IMediaManager {
throw error;
}
}
close() {
this.storage.close().catch((err) => {
this.log.error(`Failed to close media storage: ${err}`);
});
}
}
+1 -1
View File
@@ -2,5 +2,5 @@ import { IMediaStorage } from '@waha/core/media/IMediaStorage';
import { Logger } from 'pino';
export abstract class MediaStorageFactory {
abstract build(logger: Logger): IMediaStorage;
abstract build(name: string, logger: Logger): Promise<IMediaStorage>;
}
+7 -3
View File
@@ -3,12 +3,12 @@ import { SECOND } from '@waha/structures/enums.dto';
import * as fsp from 'fs/promises';
import * as path from 'path';
import { Logger } from 'pino';
import { promisify } from 'util';
import fs = require('fs');
import del = require('del');
import { fileExists } from '@waha/utils/files';
const writeFileAsync = promisify(fs.writeFile);
// eslint-disable-next-line @typescript-eslint/no-var-requires
const writeFileAtomic = require('write-file-atomic');
/**
* Save files locally using the filesystem
@@ -41,7 +41,7 @@ export class MediaLocalStorage implements IMediaStorage {
const filepath = this.getFullPath(data);
const folder = path.dirname(filepath);
await fsp.mkdir(folder, { recursive: true });
await writeFileAsync(filepath, buffer);
await writeFileAtomic(filepath, buffer);
this.postponeRemoval(filepath);
return true;
}
@@ -92,4 +92,8 @@ export class MediaLocalStorage implements IMediaStorage {
this.lifetimeMs,
);
}
async close() {
return;
}
}
@@ -11,7 +11,8 @@ export class MediaLocalStorageFactory extends MediaStorageFactory {
super();
}
build(logger: Logger): IMediaStorage {
async build(name: string, logger: Logger): Promise<IMediaStorage> {
// Local storage uses the same folder for all sessions
return new MediaLocalStorage(
logger,
this.config.filesFolder,
+6 -4
View File
@@ -1,16 +1,18 @@
import { SessionConfig } from '../../structures/sessions.dto';
export abstract class ISessionConfigRepository {
abstract save(
abstract saveConfig(
sessionName: string,
config: SessionConfig | null,
): Promise<void>;
abstract get(sessionName: string): Promise<SessionConfig | null>;
abstract getConfig(sessionName: string): Promise<SessionConfig | null>;
abstract exists(sessionName: string): Promise<boolean>;
abstract delete(sessionName: string): Promise<void>;
abstract deleteConfig(sessionName: string): Promise<void>;
abstract getAll(): Promise<string[]>;
abstract getAllConfigs(): Promise<string[]>;
abstract init(): Promise<void>;
}
@@ -7,6 +7,9 @@ import { SessionConfig } from '../../structures/sessions.dto';
import { ISessionConfigRepository } from './ISessionConfigRepository';
import { LocalStore } from './LocalStore';
// eslint-disable-next-line @typescript-eslint/no-var-requires
const writeFileAtomic = require('write-file-atomic');
export class LocalSessionConfigRepository extends ISessionConfigRepository {
FILENAME = '.waha.session.config.json';
private store: LocalStore;
@@ -27,7 +30,7 @@ export class LocalSessionConfigRepository extends ISessionConfigRepository {
return true;
}
async get(sessionName: string): Promise<SessionConfig | null> {
async getConfig(sessionName: string): Promise<SessionConfig | null> {
const filepath = this.getFilePath(sessionName);
// Check file exists
if (!(await fileExists(filepath))) {
@@ -45,26 +48,26 @@ export class LocalSessionConfigRepository extends ISessionConfigRepository {
return JSON.parse(content);
}
async save(sessionName: string, config: SessionConfig | null) {
async saveConfig(sessionName: string, config: SessionConfig | null) {
// Create folder if not exist
const folder = this.store.getSessionDirectory(sessionName);
await fs.mkdir(folder, { recursive: true });
// Save config
const filepath = this.getFilePath(sessionName);
const content = JSON.stringify(config || null);
await fs.writeFile(filepath, content);
await writeFileAtomic(filepath, content);
}
private getFilePath(sessionName): string {
return this.store.getFilePath(sessionName, this.FILENAME);
}
async delete(sessionName: string): Promise<void> {
async deleteConfig(sessionName: string): Promise<void> {
const sessionDirectory = this.store.getSessionDirectory(sessionName);
await fs.remove(sessionDirectory);
}
async getAll(): Promise<string[]> {
async getAllConfigs(): Promise<string[]> {
await this.store.init();
const content = await fs.readdir(this.store.getEngineDirectory(), {
withFileTypes: true,
@@ -73,4 +76,8 @@ export class LocalSessionConfigRepository extends ISessionConfigRepository {
.filter((dirent) => dirent.isDirectory())
.map((dirent) => dirent.name);
}
async init() {
return;
}
}
@@ -1,74 +0,0 @@
import { Sqlite3SchemaValidation } from '@waha/core/engines/noweb/store/sqlite3/Sqlite3SchemaValidation';
import {
ISessionWorkerRepository,
SessionWorkerInfo,
} from '@waha/core/storage/ISessionWorkerRepository';
import { LocalStore } from '@waha/core/storage/LocalStore';
import { Field, Index, Schema } from '@waha/core/storage/sqlite3/Schema';
import { Sqlite3KVRepository } from '@waha/core/storage/sqlite3/Sqlite3KVRepository';
// eslint-disable-next-line @typescript-eslint/no-var-requires
const Database = require('better-sqlite3');
const SCHEMA = new Schema(
'session_worker',
[
new Field('id', 'TEXT'),
new Field('worker', 'TEXT'),
new Field('data', 'TEXT'),
],
[
new Index('session_worker_id_idx', ['id']),
new Index('session_worker_worker_idx', ['worker']),
],
);
export class LocalSessionWorkerRepository
extends Sqlite3KVRepository<SessionWorkerInfo>
implements ISessionWorkerRepository
{
constructor(store: LocalStore) {
const db = store.getWAHADatabase();
super(db, SCHEMA);
}
assign(session: string, worker: string): Promise<void> {
return this.upsertOne({ id: session, worker: worker });
}
unassign(session: string, worker: string): Promise<void> {
return this.deleteBy({ id: session, worker: worker });
}
remove(session: string) {
return this.deleteById(session);
}
async getSessionsByWorker(worker: string): Promise<string[]> {
const data = await this.getAllBy({ worker: worker });
return data.map((d) => d.id);
}
async init(): Promise<void> {
this.migrations();
this.validateSchema();
}
private migrations() {
this.db.exec(
'CREATE TABLE IF NOT EXISTS session_worker (id TEXT, worker TEXT, data TEXT)',
);
// Session can have only one record
this.db.exec(
'CREATE UNIQUE INDEX IF NOT EXISTS session_worker_id_idx ON session_worker (id)',
);
// Worker can have multiple records
this.db.exec(
'CREATE INDEX IF NOT EXISTS session_worker_worker_idx ON session_worker (worker)',
);
}
private validateSchema() {
new Sqlite3SchemaValidation(SCHEMA, this.db).validate();
}
}
+3 -6
View File
@@ -9,10 +9,8 @@ import { LocalStore } from './LocalStore';
const Database = require('better-sqlite3');
export class LocalStoreCore extends LocalStore {
protected readonly baseDirectory: string = path.join(
os.tmpdir(),
'waha-sessions',
);
protected readonly baseDirectory: string =
process.env.WAHA_LOCAL_STORE_BASE_DIR || './.sessions';
private readonly engine: string;
private db: any;
@@ -54,8 +52,7 @@ export class LocalStoreCore extends LocalStore {
}
protected getDirectoryPath(name: string): string {
const suffix = crypto.createHash('md5').update(name).digest('hex');
return path.join(this.getEngineDirectory(), `${name}-${suffix}`);
return path.join(this.getEngineDirectory(), name);
}
getWAHADatabase(): any {
File renamed without changes.
+4
View File
@@ -0,0 +1,4 @@
export interface IJsonQuery {
filter(field: string, key: string): string;
sortBy(field: string, sortBy: string, direction: string): string;
}
+28
View File
@@ -0,0 +1,28 @@
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>;
}
+217
View File
@@ -0,0 +1,217 @@
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';
export type Migration = string;
export class SqlKVRepository<Entity> {
protected UPSERT_BATCH_SIZE = 100;
protected Paginator: typeof KnexPaginator = KnexPaginator;
protected jsonQuery: IJsonQuery;
get schema(): Schema {
throw new Error('Not implemented');
}
get metadata(): Map<string, (entity: Entity) => any> {
return new Map();
}
get migrations(): Migration[] {
return [];
}
constructor(
private engine: ISQLEngine,
protected knex: Knex.Knex,
) {}
get columns(): Field[] {
return this.schema.columns;
}
get table(): string {
return this.schema.name;
}
/**
* Migrations
*/
async init(): Promise<void> {
await this.applyMigrations();
await this.validateSchema();
}
protected async applyMigrations() {
for (const migration of this.migrations) {
await this.engine.exec(migration);
}
}
protected async validateSchema() {
return;
}
/**
* Update methods
*/
save(entity: Entity) {
return this.upsertOne(entity);
}
async upsertOne(entity: Entity): Promise<void> {
await this.upsertMany([entity]);
}
async upsertMany(entities: Entity[]): Promise<void> {
if (entities.length === 0) {
return;
}
const batchSize = this.UPSERT_BATCH_SIZE;
for (let i = 0; i < entities.length; i += batchSize) {
const batch = entities.slice(i, i + batchSize);
await this.upsertBatch(batch);
}
}
private async upsertBatch(entities: Entity[]): Promise<void> {
const data = entities.map((entity) => this.dump(entity));
const columns = this.columns.map((c) => `"${c.fieldName}"`);
const values = data.map((d) => Object.values(d)).flat();
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(', ')}`;
try {
await this.raw(sql, values);
} catch (err) {
console.error(
`Error upserting data: ${err}, sql: ${sql}, values: ${values}`,
);
throw err;
}
}
/**
* Get methods
*/
getAll(pagination?: PaginationParams) {
let query = this.select();
query = this.pagination(query, pagination);
return this.all(query);
}
getAllByIds(ids: string[]) {
return this.all(this.select().whereIn('id', ids));
}
getById(id: string): Promise<Entity | null> {
return this.getBy({ id: id });
}
getAllBy(filters: any) {
const query = this.select().where(filters);
return this.all(query);
}
public async getBy(filters: any) {
const query = this.select().where(filters).limit(1);
return this.get(query);
}
/**
* Delete methods
*/
async deleteAll() {
const query = this.delete();
await this.run(query);
}
async deleteById(id: string) {
await this.deleteBy({ id: id });
}
public async deleteBy(filters: any) {
const query = this.delete().where(filters);
await this.run(query);
}
/**
* SQL Implementation details
*/
public async raw(sql: string, bindings: any[]): Promise<void> {
await this.engine.raw(sql, bindings);
}
protected async run(query: Knex.QueryBuilder): Promise<void> {
await this.engine.run(query);
}
protected async get(query: Knex.QueryBuilder): Promise<Entity | null> {
const row = await this.engine.get(query);
if (!row) {
return null;
}
return this.parse(row);
}
public async all(query: Knex.QueryBuilder): Promise<Entity[]> {
const rows = await this.engine.all(query);
return rows.map((row) => this.parse(row));
}
/**
* SQL helpers
*/
public select() {
return this.knex.select().from(this.table);
}
protected delete() {
return this.knex.delete().from(this.table);
}
public pagination(query: any, pagination?: PaginationParams) {
const paginator = new this.Paginator(pagination, this.jsonQuery);
return paginator.apply(query);
}
/**
* JSON helpers
*/
public filterJson(field: string, key: string): string {
return this.jsonQuery.filter(field, key);
}
protected stringify(data: any): string {
return JSON.stringify(data);
}
protected parse(row: any) {
return JSON.parse(row.data);
}
protected dump(entity: Entity) {
const data = {};
const raw = entity;
for (const field of this.columns) {
const fn = this.metadata.get(field.fieldName);
if (fn) {
data[field.fieldName] = fn(raw);
} else if (field.fieldName == 'data') {
data['data'] = this.stringify(raw);
} else {
data[field.fieldName] = raw[field.fieldName] ?? null;
}
}
return data;
}
}
+54
View File
@@ -0,0 +1,54 @@
import { Field, Index, Schema } from '@waha/core/storage/Schema';
import { Migration } from '@waha/core/storage/sql/SqlKVRepository';
/**
* Session Config
*/
export const SQLSessionConfigSchema = new Schema(
'session_config',
[new Field('id', 'TEXT'), new Field('data', 'TEXT')],
[new Index('session_config_id_index', ['id'])],
);
export const SQLSessionConfigMigrations: Migration[] = [
'CREATE TABLE IF NOT EXISTS session_config (id TEXT PRIMARY KEY, data TEXT)',
'CREATE UNIQUE INDEX IF NOT EXISTS session_config_id_index ON session_config (id)',
];
/**
* Me
*/
export const SQLMeSchema = new Schema(
'me',
[new Field('id', 'TEXT'), new Field('data', 'TEXT')],
[new Index('me_id_index', ['id'])],
);
export const SQLMeMigrations: Migration[] = [
'CREATE TABLE IF NOT EXISTS me (id TEXT PRIMARY KEY, data TEXT)',
'CREATE UNIQUE INDEX IF NOT EXISTS me_id_index ON me (id)',
];
/**
* Worker
*/
export const SQLSessionWorkerSchema = new Schema(
'session_worker',
[
new Field('id', 'TEXT'),
new Field('worker', 'TEXT'),
new Field('data', 'TEXT'),
],
[
new Index('session_worker_id_idx', ['id']),
new Index('session_worker_worker_idx', ['worker']),
],
);
export const SQLSessionWorkerMigrations: Migration[] = [
'CREATE TABLE IF NOT EXISTS session_worker (id TEXT, worker TEXT, data TEXT)',
// Session can have only one record
'CREATE UNIQUE INDEX IF NOT EXISTS session_worker_id_idx ON session_worker (id)',
// Worker can have multiple records
'CREATE INDEX IF NOT EXISTS session_worker_worker_idx ON session_worker (worker)',
];
+38
View File
@@ -0,0 +1,38 @@
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);
}
}
@@ -0,0 +1,13 @@
import { IJsonQuery } from '@waha/core/storage/sql/IJsonQuery';
export class Sqlite3JsonQuery implements IJsonQuery {
filter(field: string, key: string): string {
const jsonPath = `$.${key}`;
return `${field}->'${jsonPath}' = ?`;
}
sortBy(field: string, sortBy: string, direction: string): string {
const jsonPath = `$.${sortBy}`;
return `${field}->'${jsonPath}' ${direction}`;
}
}
+11 -167
View File
@@ -1,177 +1,21 @@
import { PaginationParams } from '@waha/structures/pagination.dto';
import { KnexPaginator } from '@waha/utils/Paginator';
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 Knex, { QueryBuilder } from 'knex';
import { Field, Schema } from './Schema';
import Knex from 'knex';
/**
* Key value repository with extra metadata
*/
export class Sqlite3KVRepository<Entity> {
private UPSERT_BATCH_SIZE = 100;
export class Sqlite3KVRepository<Entity> extends SqlKVRepository<Entity> {
protected db: Database;
protected jsonQuery = new Sqlite3JsonQuery();
private readonly metadata: Map<string, (entity: Entity) => any>;
protected readonly table: string;
private readonly columns: Field[];
private knex: Knex.Knex;
protected Paginator = KnexPaginator;
constructor(
db: Database,
schema: Schema,
metadata: Map<string, (entity: Entity) => any> | null = null,
) {
this.db = db;
this.columns = schema.columns;
this.table = schema.name;
this.metadata = metadata || new Map();
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).
this.knex = Knex({ client: 'better-sqlite3', useNullAsDefault: true });
}
getAll(pagination?: PaginationParams) {
let query = this.select();
query = this.pagination(query, pagination);
return this.all(query);
}
getAllByIds(ids: string[]) {
return this.all(this.select().whereIn('id', ids));
}
protected getAllBy(filters: any) {
const query = this.select().where(filters);
return this.all(query);
}
protected async getBy(filters: any) {
const query = this.select().where(filters).limit(1);
return this.get(query);
}
protected dump(entity: Entity) {
const data = {};
const raw = entity;
for (const field of this.columns) {
const fn = this.metadata.get(field.fieldName);
if (fn) {
data[field.fieldName] = fn(raw);
} else if (field.fieldName == 'data') {
data['data'] = this.stringify(raw);
} else {
data[field.fieldName] = raw[field.fieldName];
}
}
return data;
}
save(entity: Entity) {
return this.upsertOne(entity);
}
async getById(id: string): Promise<Entity | null> {
return this.getBy({ id: id });
}
async upsertOne(entity: Entity): Promise<void> {
await this.upsertMany([entity]);
}
async upsertMany(entities: Entity[]): Promise<void> {
if (entities.length === 0) {
return;
}
const batchSize = this.UPSERT_BATCH_SIZE;
for (let i = 0; i < entities.length; i += batchSize) {
const batch = entities.slice(i, i + batchSize);
await this.upsertBatch(batch);
}
}
private async upsertBatch(entities: Entity[]): Promise<void> {
const data = entities.map((entity) => this.dump(entity));
const keys = this.columns.map((c) => c.fieldName);
const values = data.map((d) => Object.values(d)).flat();
try {
this.db
.prepare(
`INSERT INTO ${this.table} (${keys.join(', ')})
VALUES ${data
.map(() => `(${keys.map(() => '?').join(', ')})`)
.join(', ')}
ON CONFLICT(id) DO UPDATE
SET ${keys.map((key) => `${key} = excluded.${key}`).join(', ')}`,
)
.run(values);
} catch (err) {
console.error(`Error upserting data: ${err}, values: ${values}`);
throw err;
}
}
protected async deleteBy(filters: any) {
const query = this.delete().where(filters);
await this.run(query);
}
async deleteAll() {
const query = this.delete();
await this.run(query);
}
async deleteById(id: string) {
await this.deleteBy({ id: id });
}
/**
* SQL helpers
*/
protected select() {
return this.knex.select().from(this.table);
}
protected delete() {
return this.knex.delete().from(this.table);
}
protected async all(query: any) {
const sql = query.toSQL().sql;
const bind = query.toSQL().bindings;
const rows: any[] = this.db.prepare(sql).all(bind);
return rows.map((row) => this.parse(row));
}
protected async get(query: any) {
const sql = query.toSQL().sql;
const bind = query.toSQL().bindings;
const row: any = this.db.prepare(sql).get(bind);
if (!row) {
return null;
}
return this.parse(row);
}
protected async run(query: any) {
const sql = query.toSQL().sql;
const bind = query.toSQL().bindings;
return this.db.prepare(sql).run(bind);
}
protected stringify(data: any): string {
return JSON.stringify(data);
}
protected parse(row: any) {
return JSON.parse(row.data);
}
protected pagination(query: any, pagination?: PaginationParams) {
const paginator = new this.Paginator(pagination);
return paginator.apply(query);
const knex = Knex({ client: 'better-sqlite3', useNullAsDefault: true });
const engine = new Sqlite3Engine(db);
super(engine, knex);
this.db = db;
}
}
@@ -1,32 +1,30 @@
import { Sqlite3SchemaValidation } from '@waha/core/engines/noweb/store/sqlite3/Sqlite3SchemaValidation';
import { ISessionMeRepository } from '@waha/core/storage/ISessionMeRepository';
import { LocalStore } from '@waha/core/storage/LocalStore';
import { Field, Index, Schema } from '@waha/core/storage/sqlite3/Schema';
import { SQLMeMigrations, SQLMeSchema } from '@waha/core/storage/sql/schemas';
import { Sqlite3KVRepository } from '@waha/core/storage/sqlite3/Sqlite3KVRepository';
import { MeInfo } from '@waha/structures/sessions.dto';
import * as path from 'path';
// eslint-disable-next-line @typescript-eslint/no-var-requires
const Database = require('better-sqlite3');
const SCHEMA = new Schema(
'me',
[new Field('id', 'TEXT'), new Field('data', 'TEXT')],
[new Index('me_id_index', ['id'])],
);
class SessionMeInfo {
id: string;
me?: MeInfo;
}
export class LocalSessionMeRepository
export class Sqlite3SessionMeRepository
extends Sqlite3KVRepository<SessionMeInfo>
implements ISessionMeRepository
{
get schema() {
return SQLMeSchema;
}
get migrations() {
return SQLMeMigrations;
}
constructor(store: LocalStore) {
const db = store.getWAHADatabase();
super(db, SCHEMA);
super(db);
}
upsertMe(sessionName: string, me: MeInfo): Promise<void> {
@@ -42,19 +40,8 @@ export class LocalSessionMeRepository
return this.deleteById(sessionName);
}
async init(): Promise<void> {
this.migrations();
this.validateSchema();
}
private migrations() {
this.db.exec(
'CREATE TABLE IF NOT EXISTS me (id TEXT PRIMARY KEY, data TEXT)',
);
this.db.exec('CREATE UNIQUE INDEX IF NOT EXISTS me_id_index ON me (id)');
}
private validateSchema() {
new Sqlite3SchemaValidation(SCHEMA, this.db).validate();
protected async validateSchema() {
const validation = new Sqlite3SchemaValidation(this.schema, this.db);
validation.validate();
}
}
@@ -0,0 +1,51 @@
import { Sqlite3SchemaValidation } from '@waha/core/engines/noweb/store/sqlite3/Sqlite3SchemaValidation';
import {
ISessionWorkerRepository,
SessionWorkerInfo,
} from '@waha/core/storage/ISessionWorkerRepository';
import { LocalStore } from '@waha/core/storage/LocalStore';
import {
SQLSessionWorkerMigrations,
SQLSessionWorkerSchema,
} from '@waha/core/storage/sql/schemas';
import { Sqlite3KVRepository } from '@waha/core/storage/sqlite3/Sqlite3KVRepository';
export class Sqlite3SessionWorkerRepository
extends Sqlite3KVRepository<SessionWorkerInfo>
implements ISessionWorkerRepository
{
get schema() {
return SQLSessionWorkerSchema;
}
get migrations() {
return SQLSessionWorkerMigrations;
}
constructor(store: LocalStore) {
const db = store.getWAHADatabase();
super(db);
}
assign(session: string, worker: string): Promise<void> {
return this.upsertOne({ id: session, worker: worker });
}
unassign(session: string, worker: string): Promise<void> {
return this.deleteBy({ id: session, worker: worker });
}
remove(session: string) {
return this.deleteById(session);
}
async getSessionsByWorker(worker: string): Promise<string[]> {
const data = await this.getAllBy({ worker: worker });
return data.map((d) => d.id);
}
protected async validateSchema() {
const validation = new Sqlite3SchemaValidation(this.schema, this.db);
validation.validate();
}
}
+1
View File
@@ -67,6 +67,7 @@ async function bootstrap() {
logger: getNestJSLogLevels(),
httpsOptions: httpsOptions,
bufferLogs: true,
forceCloseConnections: true,
});
app.useLogger(app.get(NestJSPinoLogger));
+2 -2
View File
@@ -26,7 +26,7 @@ export class WebsocketHeartbeatJob {
}
client.isAlive = false;
this.logger.debug(`Sending heartbeat (ping) to ${client.id}`);
this.logger.verbose(`Sending heartbeat (ping) to ${client.id}`);
client.ping();
});
}, this.intervalTime);
@@ -40,7 +40,7 @@ export class WebsocketHeartbeatJob {
private onPong(ws: WebSocket) {
return (event: any) => {
ws.isAlive = true;
this.logger.debug(`Heartbeat (pong) received from ${ws.id}`);
this.logger.verbose(`Heartbeat (pong) received from ${ws.id}`);
};
}
}
+2 -1
View File
@@ -1,4 +1,4 @@
import { IsNotEmpty } from 'class-validator';
import { IsNotEmpty, IsString } from 'class-validator';
export const WHATSAPP_DEFAULT_SESSION_NAME = 'default';
@@ -7,6 +7,7 @@ export const WHATSAPP_DEFAULT_SESSION_NAME = 'default';
*/
export class SessionQuery {
@IsNotEmpty()
@IsString()
session: string = WHATSAPP_DEFAULT_SESSION_NAME;
}
+37 -2
View File
@@ -1,6 +1,9 @@
import { ApiProperty } from '@nestjs/swagger';
import { BooleanString } from '@waha/nestjs/validation/BooleanString';
import { PaginationParams } from '@waha/structures/pagination.dto';
import {
LimitOffsetParams,
PaginationParams,
} from '@waha/structures/pagination.dto';
import { ChatIdProperty } from '@waha/structures/properties.dto';
import { Transform, Type } from 'class-transformer';
import { IsBoolean, IsEnum, IsNumber, IsOptional } from 'class-validator';
@@ -38,6 +41,23 @@ export class GetChatMessagesFilter {
'filter.fromMe'?: boolean;
}
export class ChatPictureQuery {
@Transform(BooleanString)
@IsBoolean()
@IsOptional()
@ApiProperty({
example: false,
required: false,
description:
'Refresh the picture from the server (24h cache by default). Do not refresh if not needed, you can get rate limit error',
})
refresh?: boolean = false;
}
export class ChatPictureResponse {
url: string;
}
export class GetChatMessagesQuery {
@IsNumber()
@IsOptional()
@@ -50,7 +70,7 @@ export class GetChatMessagesQuery {
offset?: number;
@ApiProperty({
example: true,
example: false,
required: false,
description: 'Download media for messages',
})
@@ -105,6 +125,21 @@ export class PinMessageRequest {
duration: number;
}
export class OverviewPaginationParams extends LimitOffsetParams {
@IsNumber()
@IsOptional()
@Type(() => Number)
limit?: number = 20;
}
export class ChatSummary {
id: string;
name: string | null;
picture: string | null;
lastMessage: any;
_chat: any;
}
/**
* Events
*/
+16 -1
View File
@@ -1,7 +1,9 @@
import { ApiProperty } from '@nestjs/swagger';
import { BooleanString } from '@waha/nestjs/validation/BooleanString';
import { PaginationParams } from '@waha/structures/pagination.dto';
import { ChatIdProperty } from '@waha/structures/properties.dto';
import { IsEnum, IsOptional, IsString } from 'class-validator';
import { Transform } from 'class-transformer';
import { IsBoolean, IsEnum, IsOptional, IsString } from 'class-validator';
import { SessionBaseRequest, SessionQuery } from './base.dto';
@@ -15,6 +17,19 @@ export class ContactQuery extends SessionQuery {
contactId: string;
}
export class ContactProfilePictureQuery extends ContactQuery {
@Transform(BooleanString)
@IsBoolean()
@IsOptional()
@ApiProperty({
example: false,
required: false,
description:
'Refresh the picture from the server (24h cache by default). Do not refresh if not needed, you can get rate limit error',
})
refresh?: boolean = false;
}
enum ContactSortField {
ID = 'id',
NAME = 'name',
+1
View File
@@ -41,6 +41,7 @@ export enum WAHASessionStatus {
export enum WAHAEngine {
WEBJS = 'WEBJS',
NOWEB = 'NOWEB',
GOWS = 'GOWS',
}
export enum WAHAPresenceStatus {
+17
View File
@@ -57,6 +57,23 @@ export class CreateGroupRequest {
participants: Array<Participant>;
}
export class JoinGroupRequest {
@ApiProperty({
description: 'Group code (123) or url (https://chat.whatsapp.com/123)',
example: 'https://chat.whatsapp.com/1234567890abcdef',
})
@IsString()
code: string;
}
export class JoinGroupResponse {
@ApiProperty({
description: 'Group ID',
example: '123@g.us',
})
id: string;
}
export enum GroupSortField {
ID = 'id',
SUBJECT = 'subject',
+3 -2
View File
@@ -6,8 +6,7 @@ export enum SortOrder {
DESC = 'desc',
ASC = 'asc',
}
export class PaginationParams {
export class LimitOffsetParams {
@IsNumber()
@IsOptional()
@Type(() => Number)
@@ -17,7 +16,9 @@ export class PaginationParams {
@IsOptional()
@Type(() => Number)
offset?: number;
}
export class PaginationParams extends LimitOffsetParams {
@ApiProperty({
description: 'Sort by field',
})
+4 -3
View File
@@ -166,8 +166,9 @@ export class SessionDetailedInfo extends SessionInfo {
engine?: any;
}
const MONGO_DB_NAME_LIMIT = 64;
const MONGO_DB_NAME_PREFIX_LEN = 'waha_noweb'.length;
// Affect almost all Databases - Sqlite, MongoDB, Postgres.
const DB_NAME_LIMIT = 64;
const DB_NAME_MAX_PREFIX_LEN = 'waha_noweb'.length;
export class SessionCreateRequest {
@ApiProperty({
@@ -177,7 +178,7 @@ export class SessionCreateRequest {
})
@IsString()
@IsOptional()
@MaxLength(MONGO_DB_NAME_LIMIT - MONGO_DB_NAME_PREFIX_LEN)
@MaxLength(DB_NAME_LIMIT - DB_NAME_MAX_PREFIX_LEN)
name: string | undefined;
@ValidateNested()
+2
View File
@@ -32,6 +32,8 @@ export class WAMessageAckBody {
fromMe: boolean;
ack: WAMessageAck;
ackName: string;
_data?: any;
}
export class WAGroupPayload {
+13 -4
View File
@@ -1,4 +1,5 @@
import { PaginationParams } from '@waha/structures/pagination.dto';
import { IJsonQuery } from '@waha/core/storage/sql/IJsonQuery';
import { PaginationParams, SortOrder } from '@waha/structures/pagination.dto';
import * as lodash from 'lodash';
export abstract class Paginator {
@@ -42,19 +43,27 @@ export class KnexPaginator extends Paginator {
indexes: string[] = [];
dataField: string = 'data';
constructor(
pagination: PaginationParams,
protected jsonQuery: IJsonQuery,
) {
super(pagination);
}
protected sort(query: any) {
if (!this.pagination?.sortBy) {
return query;
}
const sortBy = this.pagination.sortBy;
const direction = this.pagination.sortOrder || 'asc';
const direction = this.pagination.sortOrder || SortOrder.ASC;
if (this.indexes.includes(sortBy)) {
return query.orderBy(sortBy, direction);
}
// Use data->"$.{field}"
// Make sure to sanitize sortBy to prevent SQL injection
// sqlite3 doesn't support binding for column names and direction
return query.orderByRaw(`${this.dataField}->'$.${sortBy}' ${direction}`);
const sql = this.jsonQuery.sortBy(this.dataField, sortBy, direction);
return query.orderByRaw(sql);
}
protected limit(query: any) {
+5 -1
View File
@@ -25,10 +25,14 @@ function getNestJSLogLevels(): LogLevel[] {
export function getPinoLogLevel(debug: boolean = false): Level {
const enableDebug = process.env.DEBUG != undefined || debug;
const level = getDefaultPinoLogLevel();
if (level === 'trace') {
return 'trace';
}
if (enableDebug) {
return 'debug';
}
return getDefaultPinoLogLevel();
return level;
}
export function isDebugEnabled() {
+14
View File
@@ -0,0 +1,14 @@
import { filter, pipe } from 'rxjs';
import { map } from 'rxjs/operators';
interface EventValue<T> {
event: string;
data: T;
}
export function onlyEvent<T>(event: any) {
return pipe(
filter<EventValue<T>>((obj) => obj.event === event),
map((event) => event.data),
);
}
+1 -1
View File
@@ -33,7 +33,7 @@ export function getEngineName(): string {
}
export const VERSION: WAHAEnvironment = {
version: '2024.12.3',
version: '2025.1.4',
engine: getEngineName(),
tier: getWAHAVersion(),
browser: getBrowserExecutablePath(),
+376 -11
View File
@@ -7,7 +7,7 @@ __metadata:
"@adiwajshing/baileys@github:devlikeapro/Baileys#fork-master-channels-678":
version: 6.7.8
resolution: "@adiwajshing/baileys@https://github.com/devlikeapro/Baileys.git#commit=ea7f0f4365508d272809458b3e9094c7bb56cf3a"
resolution: "@adiwajshing/baileys@https://github.com/devlikeapro/Baileys.git#commit=04738f528321857efe837e3f93c73059878ecdcb"
dependencies:
"@adiwajshing/keyed-db": ^0.2.4
"@hapi/boom": ^9.1.3
@@ -39,7 +39,7 @@ __metadata:
optional: true
sharp:
optional: true
checksum: c765057d3978dae6e25218324f2e1d969a0173822b3a62f4677342cb94a08feaefa936d0ed6effb34bc2a5a340a938c51d7ac9dff8e6ecaf121d8e6dfa950119
checksum: 0f7b2031eeb25137c65aee3151028f7a8637cfed0763c7426817415ec818ca71f55267b9de88511905bd266f4006301a26206dac9bdb1821d13f1a0cbf075e32
languageName: node
linkType: hard
@@ -1242,6 +1242,30 @@ __metadata:
languageName: node
linkType: hard
"@grpc/grpc-js@npm:^1.12.4":
version: 1.12.4
resolution: "@grpc/grpc-js@npm:1.12.4"
dependencies:
"@grpc/proto-loader": ^0.7.13
"@js-sdsl/ordered-map": ^4.4.2
checksum: 6dab69c70173735b775717361662ed5a931d2ca1bc9f3ed5f7095a05f26eb59489b1805b0a8f78b77f34d48a896f358ca951f0ce38d8053d18b0d5d0fd92837f
languageName: node
linkType: hard
"@grpc/proto-loader@npm:^0.7.13":
version: 0.7.13
resolution: "@grpc/proto-loader@npm:0.7.13"
dependencies:
lodash.camelcase: ^4.3.0
long: ^5.0.0
protobufjs: ^7.2.5
yargs: ^17.7.2
bin:
proto-loader-gen-types: build/bin/proto-loader-gen-types.js
checksum: 399c1b8a4627f93dc31660d9636ea6bf58be5675cc7581e3df56a249369e5be02c6cd0d642c5332b0d5673bc8621619bc06fb045aa3e8f57383737b5d35930dc
languageName: node
linkType: hard
"@hapi/boom@npm:^9.1.3":
version: 9.1.4
resolution: "@hapi/boom@npm:9.1.4"
@@ -1780,6 +1804,13 @@ __metadata:
languageName: node
linkType: hard
"@js-sdsl/ordered-map@npm:^4.4.2":
version: 4.4.2
resolution: "@js-sdsl/ordered-map@npm:4.4.2"
checksum: a927ae4ff8565ecb75355cc6886a4f8fadbf2af1268143c96c0cce3ba01261d241c3f4ba77f21f3f017a00f91dfe9e0673e95f830255945c80a0e96c6d30508a
languageName: node
linkType: hard
"@lukeed/csprng@npm:^1.0.0":
version: 1.1.0
resolution: "@lukeed/csprng@npm:1.1.0"
@@ -1787,6 +1818,25 @@ __metadata:
languageName: node
linkType: hard
"@mapbox/node-pre-gyp@npm:^1.0.5":
version: 1.0.11
resolution: "@mapbox/node-pre-gyp@npm:1.0.11"
dependencies:
detect-libc: ^2.0.0
https-proxy-agent: ^5.0.0
make-dir: ^3.1.0
node-fetch: ^2.6.7
nopt: ^5.0.0
npmlog: ^5.0.1
rimraf: ^3.0.2
semver: ^7.3.5
tar: ^6.1.11
bin:
node-pre-gyp: bin/node-pre-gyp
checksum: b848f6abc531a11961d780db813cc510ca5a5b6bf3184d72134089c6875a91c44d571ba6c1879470020803f7803609e7b2e6e429651c026fe202facd11d444b8
languageName: node
linkType: hard
"@microsoft/tsdoc@npm:^0.14.2":
version: 0.14.2
resolution: "@microsoft/tsdoc@npm:0.14.2"
@@ -3675,6 +3725,13 @@ __metadata:
languageName: node
linkType: hard
"abbrev@npm:1":
version: 1.1.1
resolution: "abbrev@npm:1.1.1"
checksum: a4a97ec07d7ea112c517036882b2ac22f3109b7b19077dc656316d07d308438aac28e4d9746dc4d84bf6b1e75b4a7b0a5f3cb30592419f128ca9a8cee3bcfa17
languageName: node
linkType: hard
"abbrev@npm:^2.0.0":
version: 2.0.0
resolution: "abbrev@npm:2.0.0"
@@ -3753,6 +3810,15 @@ __metadata:
languageName: node
linkType: hard
"agent-base@npm:6":
version: 6.0.2
resolution: "agent-base@npm:6.0.2"
dependencies:
debug: 4
checksum: f52b6872cc96fd5f622071b71ef200e01c7c4c454ee68bc9accca90c98cfb39f2810e3e9aa330435835eedc8c23f4f8a15267f67c6e245d2b33757575bdac49d
languageName: node
linkType: hard
"agent-base@npm:^7.0.2, agent-base@npm:^7.1.0, agent-base@npm:^7.1.1":
version: 7.1.1
resolution: "agent-base@npm:7.1.1"
@@ -3935,6 +4001,13 @@ __metadata:
languageName: node
linkType: hard
"aproba@npm:^1.0.3 || ^2.0.0":
version: 2.0.0
resolution: "aproba@npm:2.0.0"
checksum: 5615cadcfb45289eea63f8afd064ab656006361020e1735112e346593856f87435e02d8dcc7ff0d11928bc7d425f27bc7c2a84f6c0b35ab0ff659c814c138a24
languageName: node
linkType: hard
"archiver-utils@npm:^2.1.0":
version: 2.1.0
resolution: "archiver-utils@npm:2.1.0"
@@ -3986,6 +4059,16 @@ __metadata:
languageName: node
linkType: hard
"are-we-there-yet@npm:^2.0.0":
version: 2.0.0
resolution: "are-we-there-yet@npm:2.0.0"
dependencies:
delegates: ^1.0.0
readable-stream: ^3.6.0
checksum: 6c80b4fd04ecee6ba6e737e0b72a4b41bdc64b7d279edfc998678567ff583c8df27e27523bc789f2c99be603ffa9eaa612803da1d886962d2086e7ff6fa90c7c
languageName: node
linkType: hard
"arg@npm:^4.1.0":
version: 4.1.3
resolution: "arg@npm:4.1.3"
@@ -5077,6 +5160,15 @@ __metadata:
languageName: node
linkType: hard
"color-support@npm:^1.1.2":
version: 1.1.3
resolution: "color-support@npm:1.1.3"
bin:
color-support: bin.js
checksum: 9b7356817670b9a13a26ca5af1c21615463b500783b739b7634a0c2047c16cef4b2865d7576875c31c3cddf9dd621fa19285e628f20198b233a5cfdda6d0793b
languageName: node
linkType: hard
"color@npm:^4.2.3":
version: 4.2.3
resolution: "color@npm:4.2.3"
@@ -5176,6 +5268,13 @@ __metadata:
languageName: node
linkType: hard
"console-control-strings@npm:^1.0.0, console-control-strings@npm:^1.1.0":
version: 1.1.0
resolution: "console-control-strings@npm:1.1.0"
checksum: 8755d76787f94e6cf79ce4666f0c5519906d7f5b02d4b884cf41e11dcd759ed69c57da0670afd9236d229a46e0f9cf519db0cd829c6dca820bb5a5c3def584ed
languageName: node
linkType: hard
"content-disposition@npm:0.5.4":
version: 0.5.4
resolution: "content-disposition@npm:0.5.4"
@@ -5572,6 +5671,13 @@ __metadata:
languageName: node
linkType: hard
"delegates@npm:^1.0.0":
version: 1.0.0
resolution: "delegates@npm:1.0.0"
checksum: a51744d9b53c164ba9c0492471a1a2ffa0b6727451bdc89e31627fdf4adda9d51277cfcbfb20f0a6f08ccb3c436f341df3e92631a3440226d93a8971724771fd
languageName: node
linkType: hard
"depd@npm:2.0.0":
version: 2.0.0
resolution: "depd@npm:2.0.0"
@@ -6873,6 +6979,23 @@ __metadata:
languageName: node
linkType: hard
"gauge@npm:^3.0.0":
version: 3.0.2
resolution: "gauge@npm:3.0.2"
dependencies:
aproba: ^1.0.3 || ^2.0.0
color-support: ^1.1.2
console-control-strings: ^1.0.0
has-unicode: ^2.0.1
object-assign: ^4.1.1
signal-exit: ^3.0.0
string-width: ^4.2.3
strip-ansi: ^6.0.1
wide-align: ^1.1.2
checksum: 81296c00c7410cdd48f997800155fbead4f32e4f82109be0719c63edc8560e6579946cc8abd04205297640691ec26d21b578837fd13a4e96288ab4b40b1dc3e9
languageName: node
linkType: hard
"gensync@npm:^1.0.0-beta.2":
version: 1.0.0-beta.2
resolution: "gensync@npm:1.0.0-beta.2"
@@ -7065,6 +7188,13 @@ __metadata:
languageName: node
linkType: hard
"google-protobuf@npm:^3.21.4":
version: 3.21.4
resolution: "google-protobuf@npm:3.21.4"
checksum: 048fa2cb579f5f88c977774b2ae36851807379d9329a6895fe3685df69ba6c927e2ff463d08d5eecd56becd9c65bca406f34f90e27984a3077e8629bb3a2a766
languageName: node
linkType: hard
"gopd@npm:^1.0.1":
version: 1.0.1
resolution: "gopd@npm:1.0.1"
@@ -7081,6 +7211,18 @@ __metadata:
languageName: node
linkType: hard
"grpc-tools@npm:^1.12.4":
version: 1.12.4
resolution: "grpc-tools@npm:1.12.4"
dependencies:
"@mapbox/node-pre-gyp": ^1.0.5
bin:
grpc_tools_node_protoc: bin/protoc.js
grpc_tools_node_protoc_plugin: bin/protoc_plugin.js
checksum: 5ea946c2d231e2b58be48b14e12f2982b44845f62d05327cf84f67fae30355c7a781d8443b0988f9db77d4eb219f15e5f98e869f6a14c4276084726e48dc8ec0
languageName: node
linkType: hard
"has-bigints@npm:^1.0.1, has-bigints@npm:^1.0.2":
version: 1.0.2
resolution: "has-bigints@npm:1.0.2"
@@ -7134,6 +7276,13 @@ __metadata:
languageName: node
linkType: hard
"has-unicode@npm:^2.0.1":
version: 2.0.1
resolution: "has-unicode@npm:2.0.1"
checksum: 1eab07a7436512db0be40a710b29b5dc21fa04880b7f63c9980b706683127e3c1b57cb80ea96d47991bdae2dfe479604f6a1ba410106ee1046a41d1bd0814400
languageName: node
linkType: hard
"hasown@npm:^2.0.0, hasown@npm:^2.0.1, hasown@npm:^2.0.2":
version: 2.0.2
resolution: "hasown@npm:2.0.2"
@@ -7199,6 +7348,16 @@ __metadata:
languageName: node
linkType: hard
"https-proxy-agent@npm:^5.0.0":
version: 5.0.1
resolution: "https-proxy-agent@npm:5.0.1"
dependencies:
agent-base: 6
debug: 4
checksum: 571fccdf38184f05943e12d37d6ce38197becdd69e58d03f43637f7fa1269cf303a7d228aa27e5b27bbd3af8f09fd938e1c91dcfefff2df7ba77c20ed8dfc765
languageName: node
linkType: hard
"https-proxy-agent@npm:^7.0.0, https-proxy-agent@npm:^7.0.1, https-proxy-agent@npm:^7.0.2":
version: 7.0.4
resolution: "https-proxy-agent@npm:7.0.4"
@@ -8474,13 +8633,13 @@ __metadata:
languageName: node
linkType: hard
"libsignal@github:WhiskeySockets/libsignal-node":
"libsignal@github:devlikeapro/libsignal-node#fork-master":
version: 2.0.1
resolution: "libsignal@https://github.com/WhiskeySockets/libsignal-node.git#commit=83a3e3a3864511cb74df1b796373f0d49d071134"
resolution: "libsignal@https://github.com/devlikeapro/libsignal-node.git#commit=83bc9236ee3d96ae1747cf3ca2e505b10cf97250"
dependencies:
curve25519-js: ^0.0.4
protobufjs: 6.8.8
checksum: bc8a8c4d54395e6b3f7224e72fe0c447859eed688cc28da3a56d76dcd5bf1ea71b25b3ff0da96646b636f2bb679898ca38eff909cb83ef3e3b017fe81e06ed50
checksum: 3ee39bc7cbd123fa2c1f84ff53ab4e3dacfb5a00d10944960fd72156b24a5eb5af1fa5ca31b9f25b26aab107c712d8c295c85b0bed26ad6eba6d0e3f26f38927
languageName: node
linkType: hard
@@ -8537,6 +8696,13 @@ __metadata:
languageName: node
linkType: hard
"lodash.camelcase@npm:^4.3.0":
version: 4.3.0
resolution: "lodash.camelcase@npm:4.3.0"
checksum: cb9227612f71b83e42de93eccf1232feeb25e705bdb19ba26c04f91e885bfd3dd5c517c4a97137658190581d3493ea3973072ca010aab7e301046d90740393d1
languageName: node
linkType: hard
"lodash.clonedeep@npm:^4.5.0":
version: 4.5.0
resolution: "lodash.clonedeep@npm:4.5.0"
@@ -8663,6 +8829,15 @@ __metadata:
languageName: node
linkType: hard
"make-dir@npm:^3.1.0":
version: 3.1.0
resolution: "make-dir@npm:3.1.0"
dependencies:
semver: ^6.0.0
checksum: 484200020ab5a1fdf12f393fe5f385fc8e4378824c940fba1729dcd198ae4ff24867bc7a5646331e50cead8abff5d9270c456314386e629acec6dff4b8016b78
languageName: node
linkType: hard
"make-dir@npm:^4.0.0":
version: 4.0.0
resolution: "make-dir@npm:4.0.0"
@@ -9206,7 +9381,7 @@ __metadata:
languageName: node
linkType: hard
"node-fetch@npm:^2.6.1, node-fetch@npm:^2.6.9":
"node-fetch@npm:^2.6.1, node-fetch@npm:^2.6.7, node-fetch@npm:^2.6.9":
version: 2.7.0
resolution: "node-fetch@npm:2.7.0"
dependencies:
@@ -9279,6 +9454,17 @@ __metadata:
languageName: node
linkType: hard
"nopt@npm:^5.0.0":
version: 5.0.0
resolution: "nopt@npm:5.0.0"
dependencies:
abbrev: 1
bin:
nopt: bin/nopt.js
checksum: d35fdec187269503843924e0114c0c6533fb54bbf1620d0f28b4b60ba01712d6687f62565c55cc20a504eff0fbe5c63e22340c3fad549ad40469ffb611b04f2f
languageName: node
linkType: hard
"nopt@npm:^7.0.0":
version: 7.2.1
resolution: "nopt@npm:7.2.1"
@@ -9306,6 +9492,18 @@ __metadata:
languageName: node
linkType: hard
"npmlog@npm:^5.0.1":
version: 5.0.1
resolution: "npmlog@npm:5.0.1"
dependencies:
are-we-there-yet: ^2.0.0
console-control-strings: ^1.1.0
gauge: ^3.0.0
set-blocking: ^2.0.0
checksum: 516b2663028761f062d13e8beb3f00069c5664925871a9b57989642ebe09f23ab02145bf3ab88da7866c4e112cafff72401f61a672c7c8a20edc585a7016ef5f
languageName: node
linkType: hard
"nth-check@npm:^2.0.1":
version: 2.1.1
resolution: "nth-check@npm:2.1.1"
@@ -9729,6 +9927,13 @@ __metadata:
languageName: node
linkType: hard
"pg-cloudflare@npm:^1.1.1":
version: 1.1.1
resolution: "pg-cloudflare@npm:1.1.1"
checksum: 32aac06b5dc4588bbf78801b6267781bc7e13be672009df949d08e9627ba9fdc26924916665d4de99d47f9b0495301930547488dad889d826856976c7b3f3731
languageName: node
linkType: hard
"pg-connection-string@npm:2.6.2":
version: 2.6.2
resolution: "pg-connection-string@npm:2.6.2"
@@ -9736,6 +9941,80 @@ __metadata:
languageName: node
linkType: hard
"pg-connection-string@npm:^2.7.0":
version: 2.7.0
resolution: "pg-connection-string@npm:2.7.0"
checksum: 68015a8874b7ca5dad456445e4114af3d2602bac2fdb8069315ecad0ff9660ec93259b9af7186606529ac4f6f72a06831e6f20897a689b16cc7fda7ca0e247fd
languageName: node
linkType: hard
"pg-int8@npm:1.0.1":
version: 1.0.1
resolution: "pg-int8@npm:1.0.1"
checksum: a1e3a05a69005ddb73e5f324b6b4e689868a447c5fa280b44cd4d04e6916a344ac289e0b8d2695d66e8e89a7fba023affb9e0e94778770ada5df43f003d664c9
languageName: node
linkType: hard
"pg-pool@npm:^3.7.0":
version: 3.7.0
resolution: "pg-pool@npm:3.7.0"
peerDependencies:
pg: ">=8.0"
checksum: 66fc1a5ad0e17b72671b9a2cd4c7a856fb08d3cb82da7af0b322590ada23127ac591111e855740405fde4f06c9de888abe9f3aa685ed6038c3232578e1fce8cf
languageName: node
linkType: hard
"pg-protocol@npm:^1.7.0":
version: 1.7.0
resolution: "pg-protocol@npm:1.7.0"
checksum: 2dba740f6fc4b7f9761682c4c42d183b444292cdc7638b373f5247ec995c8199c369953343479281da3c41611fe34130a80c8668348d49a399c164f802f76be2
languageName: node
linkType: hard
"pg-types@npm:^2.1.0":
version: 2.2.0
resolution: "pg-types@npm:2.2.0"
dependencies:
pg-int8: 1.0.1
postgres-array: ~2.0.0
postgres-bytea: ~1.0.0
postgres-date: ~1.0.4
postgres-interval: ^1.1.0
checksum: bf4ec3f594743442857fb3a8dfe5d2478a04c98f96a0a47365014557cbc0b4b0cee01462c79adca863b93befbf88f876299b75b72c665b5fb84a2c94fbd10316
languageName: node
linkType: hard
"pg@npm:^8.13.1":
version: 8.13.1
resolution: "pg@npm:8.13.1"
dependencies:
pg-cloudflare: ^1.1.1
pg-connection-string: ^2.7.0
pg-pool: ^3.7.0
pg-protocol: ^1.7.0
pg-types: ^2.1.0
pgpass: 1.x
peerDependencies:
pg-native: ">=3.0.1"
dependenciesMeta:
pg-cloudflare:
optional: true
peerDependenciesMeta:
pg-native:
optional: true
checksum: 22cb97fcbee3348d5ee0b195071cc572f9c88eb40cbb61fe6726af68d55d5962121b2d630509bb907703e1c8bdc33de775462029c5399e2a841fa9e6c9da0242
languageName: node
linkType: hard
"pgpass@npm:1.x":
version: 1.0.5
resolution: "pgpass@npm:1.0.5"
dependencies:
split2: ^4.1.0
checksum: 947ac096c031eebdf08d989de2e9f6f156b8133d6858c7c2c06c041e1e71dda6f5f3bad3c0ec1e96a09497bbc6ef89e762eefe703b5ef9cb2804392ec52ec400
languageName: node
linkType: hard
"picocolors@npm:^1.0.0, picocolors@npm:^1.0.1":
version: 1.0.1
resolution: "picocolors@npm:1.0.1"
@@ -9899,6 +10178,36 @@ __metadata:
languageName: node
linkType: hard
"postgres-array@npm:~2.0.0":
version: 2.0.0
resolution: "postgres-array@npm:2.0.0"
checksum: 0e1e659888147c5de579d229a2d95c0d83ebdbffc2b9396d890a123557708c3b758a0a97ed305ce7f58edfa961fa9f0bbcd1ea9f08b6e5df73322e683883c464
languageName: node
linkType: hard
"postgres-bytea@npm:~1.0.0":
version: 1.0.0
resolution: "postgres-bytea@npm:1.0.0"
checksum: d844ae4ca7a941b70e45cac1261a73ee8ed39d72d3d74ab1d645248185a1b7f0ac91a3c63d6159441020f4e1f7fe64689ac56536a307b31cef361e5187335090
languageName: node
linkType: hard
"postgres-date@npm:~1.0.4":
version: 1.0.7
resolution: "postgres-date@npm:1.0.7"
checksum: 5745001d47e51cd767e46bcb1710649cd705d91a24d42fa661c454b6dcbb7353c066a5047983c90a626cd3bbfea9e626cc6fa84a35ec57e5bbb28b49f78e13ed
languageName: node
linkType: hard
"postgres-interval@npm:^1.1.0":
version: 1.2.0
resolution: "postgres-interval@npm:1.2.0"
dependencies:
xtend: ^4.0.0
checksum: 746b71f93805ae33b03528e429dc624706d1f9b20ee81bf743263efb6a0cd79ae02a642a8a480dbc0f09547b4315ab7df6ce5ec0be77ed700bac42730f5c76b2
languageName: node
linkType: hard
"prebuild-install@npm:^7.1.1":
version: 7.1.2
resolution: "prebuild-install@npm:7.1.2"
@@ -10087,6 +10396,35 @@ __metadata:
languageName: node
linkType: hard
"protobufjs@npm:^7.2.5":
version: 7.4.0
resolution: "protobufjs@npm:7.4.0"
dependencies:
"@protobufjs/aspromise": ^1.1.2
"@protobufjs/base64": ^1.1.2
"@protobufjs/codegen": ^2.0.4
"@protobufjs/eventemitter": ^1.1.0
"@protobufjs/fetch": ^1.1.0
"@protobufjs/float": ^1.0.2
"@protobufjs/inquire": ^1.1.0
"@protobufjs/path": ^1.1.2
"@protobufjs/pool": ^1.1.0
"@protobufjs/utf8": ^1.1.0
"@types/node": ">=13.7.0"
long: ^5.0.0
checksum: ba0e6b60541bbf818bb148e90f5eb68bd99004e29a6034ad9895a381cbd352be8dce5376e47ae21b2e05559f2505b4a5f4a3c8fa62402822c6ab4dcdfb89ffb3
languageName: node
linkType: hard
"protoc-gen-ts@npm:^0.8.7":
version: 0.8.7
resolution: "protoc-gen-ts@npm:0.8.7"
bin:
protoc-gen-ts: protoc-gen-ts.js
checksum: 556d89ef43b152b9f457dab79d51f649f5108fb1ef447da41f0c3b6f33df1a5bd8ef3392c0ce9b25730e68ccb0c972a52ca37d50775ce4602a94b502aeab1e88
languageName: node
linkType: hard
"proxy-addr@npm:~2.0.7":
version: 2.0.7
resolution: "proxy-addr@npm:2.0.7"
@@ -10923,7 +11261,7 @@ __metadata:
languageName: node
linkType: hard
"signal-exit@npm:^3.0.2, signal-exit@npm:^3.0.3, signal-exit@npm:^3.0.7":
"signal-exit@npm:^3.0.0, signal-exit@npm:^3.0.2, signal-exit@npm:^3.0.3, signal-exit@npm:^3.0.7":
version: 3.0.7
resolution: "signal-exit@npm:3.0.7"
checksum: a2f098f247adc367dffc27845853e9959b9e88b01cb301658cfe4194352d8d2bb32e18467c786a7fe15f1d44b233ea35633d076d5e737870b7139949d1ab6318
@@ -11085,7 +11423,7 @@ __metadata:
languageName: node
linkType: hard
"split2@npm:^4.0.0":
"split2@npm:^4.0.0, split2@npm:^4.1.0":
version: 4.2.0
resolution: "split2@npm:4.2.0"
checksum: 05d54102546549fe4d2455900699056580cca006c0275c334611420f854da30ac999230857a85fdd9914dc2109ae50f80fda43d2a445f2aa86eccdc1dfce779d
@@ -11185,7 +11523,7 @@ __metadata:
languageName: node
linkType: hard
"string-width-cjs@npm:string-width@^4.2.0, string-width@npm:^4.0.0, string-width@npm:^4.1.0, string-width@npm:^4.2.0, string-width@npm:^4.2.2, string-width@npm:^4.2.3":
"string-width-cjs@npm:string-width@^4.2.0, string-width@npm:^1.0.2 || 2 || 3 || 4, string-width@npm:^4.0.0, string-width@npm:^4.1.0, string-width@npm:^4.2.0, string-width@npm:^4.2.2, string-width@npm:^4.2.3":
version: 4.2.3
resolution: "string-width@npm:4.2.3"
dependencies:
@@ -12225,6 +12563,8 @@ __metadata:
"@adiwajshing/keyed-db": ^0.2.4
"@aws-sdk/client-s3": ^3.633.0
"@aws-sdk/s3-request-presigner": ^3.633.0
"@grpc/grpc-js": ^1.12.4
"@grpc/proto-loader": ^0.7.13
"@nestjs/axios": ^3.0.2
"@nestjs/cli": ^9.0.0
"@nestjs/common": ^9.0.9
@@ -12267,11 +12607,14 @@ __metadata:
express-basic-auth: ^1.2.1
file-type: 16.5.4
fs-extra: ^11.2.0
google-protobuf: ^3.21.4
grpc-tools: ^1.12.4
https-proxy-agent: ^7.0.0
jest: ^29.7.0
joi: ^17.13.3
knex: ^3.1.0
libphonenumber-js: ^1.10.36
libsignal: "github:devlikeapro/libsignal-node#fork-master"
link-preview-js: ^3.0.4
lodash: ^4.17.21
mime-types: ^2.1.27
@@ -12279,11 +12622,13 @@ __metadata:
nestjs-pino: ^4.1.0
passport: ^0.6.0
passport-headerapikey: ^1.2.2
pg: ^8.13.1
pino-http: ^10.2.0
pino-pretty: ^11.2.1
prettier: ^1.19.1
pretty-bytes: 5.6.0
promise-retry: ^2.0.1
protoc-gen-ts: ^0.8.7
puppeteer: ^23.6.0
qrcode: ^1.5.1
qrcode-terminal: ^0.12.0
@@ -12300,6 +12645,7 @@ __metadata:
typescript: ^4.8.4
ulid: ^2.3.0
whatsapp-web.js: "github:devlikeapro/whatsapp-web.js#fork-main-channels"
write-file-atomic: ^6.0.0
dependenciesMeta:
bufferutil:
optional: true
@@ -12394,7 +12740,7 @@ __metadata:
"whatsapp-web.js@github:devlikeapro/whatsapp-web.js#fork-main-channels":
version: 1.26.0
resolution: "whatsapp-web.js@https://github.com/devlikeapro/whatsapp-web.js.git#commit=73288b43a3a346520867ffcbf0de0867dd07b343"
resolution: "whatsapp-web.js@https://github.com/devlikeapro/whatsapp-web.js.git#commit=a8683b0d1a5b8025ac1e7deb3476042ceadb4c07"
dependencies:
"@pedroslopez/moduleraid": ^5.0.2
archiver: ^5.3.1
@@ -12412,7 +12758,7 @@ __metadata:
optional: true
unzipper:
optional: true
checksum: 2513fe75b213a8c78650c61abd1671a2f671ba4f5423fb949fda2d45cd2c24cf414dfdde5222adb6582cda264983f15a3701d0dfd8a6cf7918d70cfdfd5a7d58
checksum: 637e53dffdaa80813a5cdd72e519f8f68978e00db17b39b471f39c8e2b98bc85643fe74168255a1dfdc655c71803e3d01b4ca6635bc6159c429dd59232ac07d4
languageName: node
linkType: hard
@@ -12492,6 +12838,15 @@ __metadata:
languageName: node
linkType: hard
"wide-align@npm:^1.1.2":
version: 1.1.5
resolution: "wide-align@npm:1.1.5"
dependencies:
string-width: ^1.0.2 || 2 || 3 || 4
checksum: d5fc37cd561f9daee3c80e03b92ed3e84d80dde3365a8767263d03dacfc8fa06b065ffe1df00d8c2a09f731482fcacae745abfbb478d4af36d0a891fad4834d3
languageName: node
linkType: hard
"widest-line@npm:^3.1.0":
version: 3.1.0
resolution: "widest-line@npm:3.1.0"
@@ -12567,6 +12922,16 @@ __metadata:
languageName: node
linkType: hard
"write-file-atomic@npm:^6.0.0":
version: 6.0.0
resolution: "write-file-atomic@npm:6.0.0"
dependencies:
imurmurhash: ^0.1.4
signal-exit: ^4.0.1
checksum: 35f1303b0229c89c36d0817de9912b43a242f775cb0f386fecf97bac735013e1fde5f464c2ce9f63288d2c91b1ec5bc18d55347b0e37c0e4dbc64b60dc220629
languageName: node
linkType: hard
"write@npm:1.0.3":
version: 1.0.3
resolution: "write@npm:1.0.3"