Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b7abeed54a | ||
|
|
d53646eef2 | ||
|
|
c54743de6c | ||
|
|
e1fdc41132 | ||
|
|
060aa82294 | ||
|
|
2df08eac3f | ||
|
|
0ecdb9d3d0 | ||
|
|
6a7fe1bd1f | ||
|
|
87c69cb3d0 | ||
|
|
c654855c22 | ||
|
|
11bb006801 | ||
|
|
e5551f3f64 | ||
|
|
0de148005f | ||
|
|
27e4ce628e | ||
|
|
19b752bf15 | ||
|
|
6528e5bba9 | ||
|
|
37e1183e87 | ||
|
|
824512cf8e | ||
|
|
82f296b17d | ||
|
|
8302530954 | ||
|
|
6c271829aa | ||
|
|
3b30d339ee | ||
|
|
c5a268bdbd | ||
|
|
8457ea8e47 | ||
|
|
72d69eeab5 | ||
|
|
31885e41af | ||
|
|
e63726d18b | ||
|
|
3d2ab45d25 | ||
|
|
246478e1d8 | ||
|
|
9418de3b23 | ||
|
|
c6b68cad37 | ||
|
|
ef36e4d3ac | ||
|
|
ba2ddd62ad | ||
|
|
6a03e1488c | ||
|
|
cabd2ed562 | ||
|
|
4a9296276d | ||
|
|
1fd84d824c | ||
|
|
faa55ffe2d | ||
|
|
d45af4191f | ||
|
|
9f8dead8b8 | ||
|
|
564037cd53 | ||
|
|
434de6292c | ||
|
|
a12e6990e5 | ||
|
|
ce0c4a094b | ||
|
|
e36728ec08 | ||
|
|
2c1f3090a0 | ||
|
|
0c6d4b3581 | ||
|
|
1d5b9eb241 | ||
|
|
b0567dafe3 | ||
|
|
518f78f067 | ||
|
|
47a2c21824 | ||
|
|
aca5547965 | ||
|
|
0e07876630 | ||
|
|
a43901b4a4 | ||
|
|
6515fac16d | ||
|
|
fdaa7e9f48 | ||
|
|
cd779850de | ||
|
|
ef939f4357 | ||
|
|
bfb9dd7009 | ||
|
|
f282f14eff | ||
|
|
992b289500 | ||
|
|
9dcdebcab2 | ||
|
|
c0b8bacab0 | ||
|
|
f96e6420fc | ||
|
|
d7f97480cc | ||
|
|
26df41b571 | ||
|
|
4ca336017d | ||
|
|
94252ea54c | ||
|
|
f8220fec1f | ||
|
|
a5ded396c9 | ||
|
|
c457cf8064 | ||
|
|
3079f90118 | ||
|
|
4fbb331015 | ||
|
|
640c0d305c | ||
|
|
0b044c67bb | ||
|
|
7a37ba8145 | ||
|
|
b447863d9f | ||
|
|
6dd7e14f6f | ||
|
|
b09ef98dce | ||
|
|
ff1cf8a288 | ||
|
|
603793662a | ||
|
|
4cdab0dbc7 | ||
|
|
d745279fec | ||
|
|
df34c9f2fd | ||
|
|
37da9aabe3 | ||
|
|
3a2205147c | ||
|
|
5c6acaf10a | ||
|
|
d916f2dea5 | ||
|
|
254a1aedcb | ||
|
|
dce3175388 | ||
|
|
e2b6b45418 | ||
|
|
e210d1e9fc | ||
|
|
df0f449e13 | ||
|
|
895105c900 | ||
|
|
209538709b | ||
|
|
ef7c35286b | ||
|
|
70af0828cb | ||
|
|
ef18e311b1 | ||
|
|
68de84973a | ||
|
|
ef6d4f9998 | ||
|
|
9f5413963b | ||
|
|
ec7c0a5cb7 | ||
|
|
f82bd9eadf | ||
|
|
843c064ebe | ||
|
|
8a52e1d1d0 | ||
|
|
8ec220dc9a | ||
|
|
9c4b9f4433 | ||
|
|
6059a29660 | ||
|
|
e65e6764fa | ||
|
|
1e77cb234a | ||
|
|
4a357087d1 | ||
|
|
6a3823cdff | ||
|
|
c2372ce96e | ||
|
|
0f5e79b163 | ||
|
|
fa4026c83f | ||
|
|
3e4d66ca27 | ||
|
|
2ec94dc13c | ||
|
|
6b11e08f3d | ||
|
|
e884e56b47 | ||
|
|
2d74affed4 | ||
|
|
3572b17049 | ||
|
|
4ee7091cd1 | ||
|
|
ff5e89d97c | ||
|
|
c4e82afc0c | ||
|
|
52c5d85487 | ||
|
|
fd0438f80b | ||
|
|
cb5781ca65 | ||
|
|
4d8aad9dfe | ||
|
|
39429a2e44 | ||
|
|
0a3514d81e | ||
|
|
4e5e0a7c34 | ||
|
|
f6cc6a9bef | ||
|
|
2e545e9fb9 | ||
|
|
d49ef64e7e | ||
|
|
b1e6eaef90 | ||
|
|
3420d0199b | ||
|
|
671db35571 | ||
|
|
d724f87726 | ||
|
|
5ab8565205 | ||
|
|
660c567d1b | ||
|
|
2c68d13c56 | ||
|
|
b9fc155b4f | ||
|
|
8dd5aefc81 | ||
|
|
f9aaf5dcef | ||
|
|
34a6e773df | ||
|
|
f0beb03344 | ||
|
|
1ff9a81ab3 | ||
|
|
9b1771c693 | ||
|
|
823e604737 | ||
|
|
97a31bdf17 | ||
|
|
ee7a198ae6 | ||
|
|
f143a51010 | ||
|
|
e483185b2c | ||
|
|
40e1c75746 | ||
|
|
456bd4693c | ||
|
|
5ac9c0f760 | ||
|
|
f621d84b83 | ||
|
|
0dd001e95a | ||
|
|
69b5235db9 | ||
|
|
a673d63b33 | ||
|
|
36485e10f2 | ||
|
|
8a1a062127 | ||
|
|
5961e0de08 | ||
|
|
60bda521a0 | ||
|
|
49f63c0f0d | ||
|
|
33910aedb8 | ||
|
|
3359a81a14 | ||
|
|
e27f850e68 | ||
|
|
07340b8f53 | ||
|
|
2f9dd7faa8 | ||
|
|
3f81fd4a87 | ||
|
|
4dd961fc66 | ||
|
|
0bd70bfb32 | ||
|
|
3ab845d2d6 | ||
|
|
443f09d5a2 | ||
|
|
3500952bb7 | ||
|
|
e98182c4b5 | ||
|
|
2337827744 | ||
|
|
556dd66af3 | ||
|
|
8bdad3f4f9 | ||
|
|
6346d4143d | ||
|
|
fd8dc88760 | ||
|
|
b2810f390e | ||
|
|
23d3cd0f4b | ||
|
|
4a3c76773d | ||
|
|
d23e7604f0 | ||
|
|
0b3f6c6eff | ||
|
|
f2869c892b | ||
|
|
3755a9529b | ||
|
|
890f0593ee | ||
|
|
e6d666b5b0 | ||
|
|
f440acf145 | ||
|
|
1ad0e0c6dd | ||
|
|
bb8691ddc0 | ||
|
|
cf3041d07b | ||
|
|
313b3d5d6b | ||
|
|
122e7336be | ||
|
|
e954453f6b | ||
|
|
55ef3d09f3 | ||
|
|
88b9cc5ac9 | ||
|
|
f07cb7470d | ||
|
|
bd874a0232 | ||
|
|
7cc4cccb9c | ||
|
|
e85a3bacb7 | ||
|
|
530dfa758f | ||
|
|
8bc3489060 | ||
|
|
6914cb171a | ||
|
|
176c2bbd4c | ||
|
|
7771954455 | ||
|
|
a66743bb20 | ||
|
|
851c7b75f9 | ||
|
|
ed0b406027 | ||
|
|
71985972a2 | ||
|
|
2dbeef9290 | ||
|
|
a79453270d | ||
|
|
ae71808c61 | ||
|
|
5c195ba37b | ||
|
|
607ee17a86 | ||
|
|
a15aacc5b3 | ||
|
|
78fb4b1a65 | ||
|
|
8e566cba72 | ||
|
|
36184288de | ||
|
|
6fc5bf38a7 | ||
|
|
e146ecde70 | ||
|
|
72b796019a | ||
|
|
0f66d15ba8 | ||
|
|
4695e933b8 | ||
|
|
5e1da6cd6f | ||
|
|
0c3bbc0980 | ||
|
|
d2bb5bea63 | ||
|
|
c76e191d4d | ||
|
|
171594d0ae | ||
|
|
4e5f499b48 | ||
|
|
bb75ad8b8e | ||
|
|
482cc7028b | ||
|
|
735bdb3854 | ||
|
|
1ee1bd78af | ||
|
|
00463c729d | ||
|
|
dac636b3bb | ||
|
|
ad87121506 | ||
|
|
55d802f005 | ||
|
|
f17164a3d9 | ||
|
|
0b9f9b2335 | ||
|
|
71d3e24241 | ||
|
|
ccd4512e8f | ||
|
|
fe6b77d67d | ||
|
|
58d12f4e70 | ||
|
|
17303039b8 | ||
|
|
f70d5203a3 | ||
|
|
efd6b1db65 | ||
|
|
dcca0fa921 | ||
|
|
e5bc447807 | ||
|
|
cd6b4b5ab1 | ||
|
|
fb79ae6138 | ||
|
|
17801b0b13 | ||
|
|
9010388de0 | ||
|
|
2da901f994 | ||
|
|
7832cc7a5d | ||
|
|
13ab2e8d0e | ||
|
|
868d23a278 | ||
|
|
eaf2bca44e | ||
|
|
af8f537f58 | ||
|
|
bc7ff472be | ||
|
|
9b9a2b5203 | ||
|
|
347259394b | ||
|
|
ead61b5e37 | ||
|
|
c9f7814f5d | ||
|
|
3ee3355e81 | ||
|
|
ce8f8b0ce9 | ||
|
|
cae35cf3b9 | ||
|
|
900e3c23bc | ||
|
|
aaeffeb983 | ||
|
|
15600324be | ||
|
|
da4913edb5 | ||
|
|
e8cc1e4906 | ||
|
|
801fc3f9fd | ||
|
|
703b0b4cde | ||
|
|
d55895389a | ||
|
|
56009e482b | ||
|
|
767d1bb3e3 | ||
|
|
05132ac0f5 | ||
|
|
fb372f8c28 | ||
|
|
a2dc863b7a | ||
|
|
15671c6781 | ||
|
|
6296115adb |
No files matched your search
@@ -1,4 +1,5 @@
|
||||
Dockerfile
|
||||
src/core/engines/gows/proto
|
||||
.*sessions
|
||||
sessions
|
||||
files
|
||||
|
||||
+37
-22
@@ -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
|
||||
@@ -21,9 +23,9 @@
|
||||
#
|
||||
# https://waha.devlike.pro/docs/how-to/security/
|
||||
#
|
||||
WHATSAPP_API_KEY=321
|
||||
WAHA_DASHBOARD_USERNAME=waha
|
||||
WAHA_DASHBOARD_PASSWORD=waha
|
||||
WHATSAPP_API_KEY=admin
|
||||
WAHA_DASHBOARD_USERNAME=admin
|
||||
WAHA_DASHBOARD_PASSWORD=admin
|
||||
#WHATSAPP_SWAGGER_USERNAME=admin
|
||||
#WHATSAPP_SWAGGER_PASSWORD=admin
|
||||
|
||||
@@ -33,8 +35,8 @@ WAHA_DASHBOARD_PASSWORD=waha
|
||||
# https://waha.devlike.pro/docs/how-to/waha-dashboard/#configuration
|
||||
# Enabled by default
|
||||
#WAHA_DASHBOARD_ENABLED=true
|
||||
#WAHA_DASHBOARD_USERNAME=waha
|
||||
#WAHA_DASHBOARD_PASSWORD=waha
|
||||
#WAHA_DASHBOARD_USERNAME=admin
|
||||
#WAHA_DASHBOARD_PASSWORD=admin
|
||||
|
||||
#
|
||||
# Logging
|
||||
@@ -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
|
||||
#
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
src/core/engines/gows/grpc/*
|
||||
src/core/engines/gows/proto/*
|
||||
@@ -1,9 +1,10 @@
|
||||
---
|
||||
name: Bug report
|
||||
about: Create a report to help us improve
|
||||
title: '[BUG][WEBJS] - '
|
||||
title: '[WEBJS] - '
|
||||
labels: bug
|
||||
assignees: ''
|
||||
type: 'BUG'
|
||||
---
|
||||
|
||||
### Describe the bug
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
---
|
||||
name: Feature request
|
||||
about: Suggest an idea for this project
|
||||
title: '[Feature Request]'
|
||||
title: ''
|
||||
labels: ''
|
||||
assignees: ''
|
||||
type: 'Feature'
|
||||
---
|
||||
|
||||
**Is your feature request related to a problem? Please describe.** I'm always
|
||||
|
||||
@@ -8,7 +8,9 @@ on:
|
||||
jobs:
|
||||
docker-build:
|
||||
runs-on: ${{ matrix.runner }}
|
||||
name: ${{ matrix.platform }} - ${{ matrix.browser }} - ${{ matrix.tag }}
|
||||
name:
|
||||
${{ matrix.engine }} - ${{ matrix.browser }} - ${{ matrix.platform }} -
|
||||
${{ matrix.tag }}
|
||||
strategy:
|
||||
matrix:
|
||||
include:
|
||||
@@ -17,6 +19,7 @@ jobs:
|
||||
tag: 'latest'
|
||||
platform: 'amd64'
|
||||
browser: 'chromium'
|
||||
engine: 'WEBJS'
|
||||
goss: 'goss-linux-amd64'
|
||||
|
||||
# Chromium - ARM
|
||||
@@ -24,6 +27,7 @@ jobs:
|
||||
tag: 'arm'
|
||||
platform: 'linux/arm64'
|
||||
browser: 'chromium'
|
||||
engine: 'WEBJS'
|
||||
goss: 'goss-linux-arm'
|
||||
|
||||
# Chrome - x86
|
||||
@@ -31,6 +35,7 @@ jobs:
|
||||
tag: 'chrome'
|
||||
platform: 'amd64'
|
||||
browser: 'chrome'
|
||||
engine: 'WEBJS'
|
||||
goss: 'goss-linux-amd64'
|
||||
|
||||
# Chrome - ARM (Chrome is not available for ARM)
|
||||
@@ -40,18 +45,36 @@ jobs:
|
||||
# browser: "chrome"
|
||||
# goss: 'goss-linux-arm'
|
||||
|
||||
# No browser - x86
|
||||
# NOWEB - x86
|
||||
- runner: 'buildjet-4vcpu-ubuntu-2204'
|
||||
tag: 'noweb'
|
||||
platform: 'amd64'
|
||||
browser: 'none'
|
||||
engine: 'NOWEB'
|
||||
goss: 'goss-linux-amd64'
|
||||
|
||||
# No browser - ARM
|
||||
# NOWEB - ARM
|
||||
- runner: 'buildjet-4vcpu-ubuntu-2204-arm'
|
||||
tag: 'noweb-arm'
|
||||
platform: 'linux/arm64'
|
||||
browser: 'none'
|
||||
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:
|
||||
@@ -85,6 +108,7 @@ jobs:
|
||||
platforms: ${{ matrix.platform }}
|
||||
build-args: |
|
||||
USE_BROWSER=${{ matrix.browser }}
|
||||
WHATSAPP_DEFAULT_ENGINE=${{ matrix.engine }}
|
||||
push: false
|
||||
load: true
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
@@ -116,6 +140,7 @@ jobs:
|
||||
platforms: ${{ matrix.platform }}
|
||||
build-args: |
|
||||
USE_BROWSER=${{ matrix.browser }}
|
||||
WHATSAPP_DEFAULT_ENGINE=${{ matrix.engine }}
|
||||
push: ${{ github.ref_type == 'tag' }}
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
@@ -1,3 +1,4 @@
|
||||
src/core/engines/gows/proto
|
||||
.env
|
||||
*.heapsnapshot
|
||||
.yarn
|
||||
|
||||
@@ -2,6 +2,8 @@ exclude: |
|
||||
(?x)^(
|
||||
^src/dashboard/.*|
|
||||
^src/core/engines/webjs/.*html|
|
||||
^src/core/engines/gows/grpc/.*|
|
||||
^src/core/engines/gows/proto/.*|
|
||||
^src/plus/engines/webjs/.*html|
|
||||
^.yarn/.*
|
||||
)$
|
||||
@@ -73,4 +75,6 @@ repos:
|
||||
.pre-commit-config.yaml|
|
||||
^src/core/engines/webjs/session.webjs.core.ts|
|
||||
^src/core/engines/webjs/_lodash.js|
|
||||
^tests/perf/.*|
|
||||
^scripts/.*|
|
||||
)$
|
||||
+68
-9
@@ -6,7 +6,7 @@ FROM node:${NODE_VERSION} AS build
|
||||
ENV PUPPETEER_SKIP_DOWNLOAD=True
|
||||
|
||||
# npm packages
|
||||
WORKDIR /src
|
||||
WORKDIR /git
|
||||
COPY package.json .
|
||||
COPY yarn.lock .
|
||||
ENV YARN_CHECKSUM_BEHAVIOR=update
|
||||
@@ -15,8 +15,8 @@ RUN yarn set version 3.6.3
|
||||
RUN yarn install
|
||||
|
||||
# App
|
||||
WORKDIR /src
|
||||
ADD . /src
|
||||
WORKDIR /git
|
||||
ADD . /git
|
||||
RUN yarn install
|
||||
RUN yarn build && find ./dist -name "*.d.ts" -delete
|
||||
|
||||
@@ -25,16 +25,51 @@ RUN yarn build && find ./dist -name "*.d.ts" -delete
|
||||
#
|
||||
FROM node:${NODE_VERSION} AS dashboard
|
||||
|
||||
# Download WAHA Dashboard
|
||||
ENV WAHA_DASHBOARD_SHA 45db5b46a944f4320136f2409d8b970483c8bd9d
|
||||
# jq to parse json
|
||||
RUN apt-get update && apt-get install -y jq && rm -rf /var/lib/apt/lists/*
|
||||
|
||||
COPY waha.config.json /tmp/waha.config.json
|
||||
RUN \
|
||||
wget https://github.com/devlikeapro/dashboard/archive/${WAHA_DASHBOARD_SHA}.zip \
|
||||
WAHA_DASHBOARD_GITHUB_REPO=$(jq -r '.waha.dashboard.repo' /tmp/waha.config.json) && \
|
||||
WAHA_DASHBOARD_SHA=$(jq -r '.waha.dashboard.ref' /tmp/waha.config.json) && \
|
||||
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
|
||||
|
||||
# jq to parse json
|
||||
RUN apt-get update && apt-get install -y jq && rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# 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/*
|
||||
|
||||
COPY waha.config.json /tmp/waha.config.json
|
||||
WORKDIR /go/gows
|
||||
RUN \
|
||||
GOWS_GITHUB_REPO=$(jq -r '.waha.gows.repo' /tmp/waha.config.json) && \
|
||||
GOWS_SHA=$(jq -r '.waha.gows.ref' /tmp/waha.config.json) && \
|
||||
ARCH=$(uname -m) && \
|
||||
if [ "$ARCH" = "x86_64" ]; then ARCH="amd64"; \
|
||||
elif [ "$ARCH" = "aarch64" ]; then ARCH="arm64"; \
|
||||
else echo "Unsupported architecture: $ARCH" && exit 1; fi && \
|
||||
mkdir -p /go/gows/bin && \
|
||||
wget -O /go/gows/bin/gows https://github.com/${GOWS_GITHUB_REPO}/releases/download/${GOWS_SHA}/gows-${ARCH} && \
|
||||
chmod +x /go/gows/bin/gows
|
||||
|
||||
|
||||
#
|
||||
# Final
|
||||
#
|
||||
@@ -44,12 +79,18 @@ ENV PUPPETEER_SKIP_DOWNLOAD=True
|
||||
# https://github.com/devlikeapro/waha/issues/347
|
||||
ENV NODE_OPTIONS="--max-old-space-size=16384"
|
||||
ARG USE_BROWSER=chromium
|
||||
ARG WHATSAPP_DEFAULT_ENGINE
|
||||
|
||||
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 \
|
||||
@@ -90,7 +131,7 @@ RUN if [ "$USE_BROWSER" = "chromium" ]; then \
|
||||
# Install Chrome
|
||||
# Available versions:
|
||||
# https://www.ubuntuupdates.org/package/google_chrome/stable/main/base/google-chrome-stable
|
||||
ARG CHROME_VERSION="130.0.6723.69-1"
|
||||
ARG CHROME_VERSION="135.0.7049.114-1"
|
||||
RUN if [ "$USE_BROWSER" = "chrome" ]; then \
|
||||
wget --no-verbose -O /tmp/chrome.deb https://dl.google.com/linux/chrome/deb/pool/main/g/google-chrome-stable/google-chrome-stable_${CHROME_VERSION}_amd64.deb \
|
||||
&& apt-get update \
|
||||
@@ -99,12 +140,28 @@ RUN if [ "$USE_BROWSER" = "chrome" ]; then \
|
||||
&& rm -rf /var/lib/apt/lists/*; \
|
||||
fi
|
||||
|
||||
# GOWS requirements
|
||||
# libc6
|
||||
RUN apt-get update \
|
||||
&& apt-get install -y libc6 \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Install tini for proper init process
|
||||
RUN apt-get update && apt-get install -y tini && rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Set the ENV for docker image
|
||||
ENV WHATSAPP_DEFAULT_ENGINE=$WHATSAPP_DEFAULT_ENGINE
|
||||
|
||||
# Attach sources, install packages
|
||||
WORKDIR /app
|
||||
COPY package.json ./
|
||||
COPY --from=build /src/node_modules ./node_modules
|
||||
COPY --from=build /src/dist ./dist
|
||||
COPY --from=build /git/node_modules ./node_modules
|
||||
COPY --from=build /git/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
|
||||
@@ -116,4 +173,6 @@ ENV WAHA_ZIPPER=ZIPUNZIP
|
||||
|
||||
# Run command, etc
|
||||
EXPOSE 3000
|
||||
# Use tini as init system to handle zombie processes properly
|
||||
ENTRYPOINT ["/usr/bin/tini", "--"]
|
||||
CMD ["/entrypoint.sh"]
|
||||
@@ -5,7 +5,10 @@ build-chrome:
|
||||
docker build . -t devlikeapro/waha-plus:chrome --build-arg USE_BROWSER=chrome
|
||||
|
||||
build-noweb:
|
||||
docker build . -t devlikeapro/waha:noweb --build-arg USE_BROWSER=none
|
||||
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
|
||||
|
||||
@@ -32,10 +35,26 @@ for-swagger:
|
||||
WHATSAPP_SWAGGER_CONFIG_ADVANCED=true . ${NVM_DIR}/nvm.sh && nvm exec yarn start
|
||||
|
||||
up-noweb:
|
||||
. ${NVM_DIR}/nvm.sh && nvm exec yarn up @adiwajshing/baileys@github:devlikeapro/Baileys#fork-master-channels-678
|
||||
. ${NVM_DIR}/nvm.sh && nvm exec yarn up @adiwajshing/baileys@github:devlikeapro/Baileys#fork-master-2025-05-01
|
||||
|
||||
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:
|
||||
node scripts/gows-proto.js build --dir ../gows/proto
|
||||
|
||||
gows:
|
||||
cd ../gows && \
|
||||
export PATH=${HOME}/go/bin:${PATH} && \
|
||||
make all
|
||||
|
||||
copy-dashboard:
|
||||
cd ../waha-hub/ui && \
|
||||
make copy-waha
|
||||
|
||||
@@ -6,22 +6,16 @@
|
||||
|
||||
**WAHA** - **W**hats**A**pp **H**TTP **A**PI (REST API) that you can install on your own server and run in less than 5 minutes!
|
||||
|
||||
[](https://hub.docker.com/r/devlikeapro/waha)
|
||||
[](https://hub.docker.com/r/devlikeapro/waha)
|
||||
|
||||
- Documentation: [https://waha.devlike.pro/](https://waha.devlike.pro/)
|
||||
- Dashboard Example: [https://waha.devlike.pro/dashboard](https://waha.devlike.pro/dashboard)
|
||||
- Swagger Example: [https://waha.devlike.pro/swagger](https://waha.devlike.pro/swagger)
|
||||
|
||||
# Tables of Contents
|
||||
|
||||
<!-- toc -->
|
||||
|
||||
- [Features](#features)
|
||||
* [Messages](#messages)
|
||||
* [Sessions](#sessions)
|
||||
* [Security](#security)
|
||||
* [Updates](#updates)
|
||||
* [Groups](#groups)
|
||||
* [Contacts](#contacts)
|
||||
- [Quick start](#quick-start)
|
||||
* [Requirements](#requirements)
|
||||
* [Send your first message](#send-your-first-message)
|
||||
@@ -32,52 +26,11 @@
|
||||
+ [5. Get the screenshot](#5-get-the-screenshot)
|
||||
+ [6. Send a text message](#6-send-a-text-message)
|
||||
* [What is next?](#what-is-next)
|
||||
- [Development](#development)
|
||||
* [Start the project](#start-the-project)
|
||||
|
||||
<!-- tocstop -->
|
||||
|
||||
# Features
|
||||
|
||||
## Messages
|
||||
- Send messages
|
||||
- Receive messages
|
||||
- Message replies
|
||||
- Send location
|
||||
- Receive location
|
||||
- React to messages
|
||||
- Send media (images/documents/files)
|
||||
- Send voice messages
|
||||
- Receive media (images/audio/video/documents)
|
||||
- Receive messages - webhook retries
|
||||
|
||||
## Sessions
|
||||
- Multi Device
|
||||
- Get the screenshot
|
||||
- Session saving (don't have to scan QR on every restart)
|
||||
- Single WhatsApp account running inside one container
|
||||
- Multiple WhatsApp account running inside one container
|
||||
|
||||
## Security
|
||||
- Swagger panel authentication
|
||||
- API authentication
|
||||
|
||||
## Updates
|
||||
- Bug fixes and updates come as soon as they are implemented.
|
||||
|
||||
## Groups
|
||||
- Create a group
|
||||
- Get invite for group
|
||||
- Modify group info (subject, description)
|
||||
- Add group participants
|
||||
- Kick group participants
|
||||
- Promote/demote group participants
|
||||
|
||||
## Contacts
|
||||
- Mute/unmute chats
|
||||
- Block/unblock contacts
|
||||
- Get contact info
|
||||
- Get profile pictures
|
||||
- Get presence (online\offline\typing) status
|
||||
|
||||
# Quick start
|
||||
|
||||
## Requirements
|
||||
@@ -87,13 +40,6 @@ instruction <a href="https://docs.docker.com/get-docker/" target="_blank" rel="n
|
||||
|
||||
When you are ready - come back and follows the below steps to send the first text message to WhatsApp via HTTP API!
|
||||
|
||||
{{< details "Why Docker?" >}}
|
||||
Docker makes it easy to ship all-in-one solution with the runtime and dependencies. You don't have to worry about
|
||||
language-specific libraries or chrome installation.
|
||||
|
||||
Also Docker makes installation and update processes so simple, just one command!
|
||||
{{< /details >}}
|
||||
|
||||
## Send your first message
|
||||
|
||||
Let's go over steps that allow you to send your first text message via WhatsApp HTTP API!
|
||||
@@ -114,7 +60,7 @@ docker pull devlikeapro/waha-plus
|
||||
docker logout
|
||||
```
|
||||
|
||||
Read more about how to get `PASSWORD` for [Plus Version →]({{< relref "plus-version" >}})
|
||||
Read more about how to get `PASSWORD` for [**➕ WAHA Plus**](https://waha.devlike.pro/docs/how-to/waha-plus/)
|
||||
|
||||
### 2. Run WhatsApp HTTP API
|
||||
|
||||
@@ -195,3 +141,19 @@ curl -d "{\"chatId\": \"${PHONE}@c.us\", \"text\": \"Hello from WhatsApp HTTP AP
|
||||
|
||||
## What is next?
|
||||
[Go and read the full documentation!](https://waha.devlike.pro/docs/overview/introduction/)
|
||||
|
||||
# Development
|
||||
|
||||
## Start the project
|
||||
1. Clone the repository
|
||||
2. Make sure you're using node>=22
|
||||
3. Run the following commands:
|
||||
```bash
|
||||
# Install dependencies
|
||||
yarn install
|
||||
# Fetch and compile proto files
|
||||
yarn gows:proto
|
||||
# Run
|
||||
yarn start
|
||||
# open http://localhost:3000
|
||||
```
|
||||
+43
-4
@@ -8,6 +8,12 @@ services:
|
||||
# WAHA Core
|
||||
# image: devlikeapro/waha:latest
|
||||
|
||||
logging:
|
||||
driver: 'json-file'
|
||||
options:
|
||||
max-size: '100m'
|
||||
max-file: '10'
|
||||
|
||||
ports:
|
||||
- '127.0.0.1:3000:3000/tcp'
|
||||
|
||||
@@ -22,9 +28,32 @@ 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=3000"
|
||||
# logging:
|
||||
# driver: "json-file"
|
||||
# options:
|
||||
# max-size: "100m"
|
||||
# max-file: "10"
|
||||
|
||||
# NOTE: Only if you're using MongoDB
|
||||
# https://waha.devlike.pro/docs/how-to/storages/#sessions---mongodb
|
||||
# Uncomment this block if you're using MongoDB
|
||||
# mongodb:
|
||||
# image: mongo
|
||||
# container_name: mongodb
|
||||
@@ -35,6 +64,11 @@ services:
|
||||
# environment:
|
||||
# - MONGO_INITDB_ROOT_USERNAME=mongouser
|
||||
# - MONGO_INITDB_ROOT_PASSWORD=mongopassword
|
||||
# logging:
|
||||
# driver: "json-file"
|
||||
# options:
|
||||
# max-size: "100m"
|
||||
# max-file: "10"
|
||||
|
||||
# NOTE: Only if you're using S3 to save media files
|
||||
# https://waha.devlike.pro/docs/how-to/storages/#media---s3
|
||||
@@ -53,8 +87,13 @@ services:
|
||||
# volumes:
|
||||
# - minio_data:/data
|
||||
# command: server /data --console-address ":9001"
|
||||
# logging:
|
||||
# driver: "json-file"
|
||||
# options:
|
||||
# max-size: "100m"
|
||||
# max-file: "10"
|
||||
|
||||
volumes:
|
||||
# NOTE: Only if you're using MongoDB
|
||||
mongodb_data: {}
|
||||
minio_data: {}
|
||||
pg_data: {}
|
||||
@@ -11,8 +11,8 @@ services:
|
||||
- './.media:/app/.media'
|
||||
environment:
|
||||
- WHATSAPP_API_KEY=321
|
||||
- WAHA_DASHBOARD_USERNAME=waha
|
||||
- WAHA_DASHBOARD_PASSWORD=waha
|
||||
- WAHA_DASHBOARD_USERNAME=admin
|
||||
- WAHA_DASHBOARD_PASSWORD=admin
|
||||
- WHATSAPP_DEFAULT_ENGINE=WEBJS
|
||||
- WAHA_PRINT_QR=False
|
||||
- WAHA_MEDIA_STORAGE=LOCAL
|
||||
|
||||
@@ -36,8 +36,8 @@ services:
|
||||
# Username and password for WAHA Dashboard
|
||||
# you can use the same username and password for both Swagger and Dashboard
|
||||
# Default - waha / waha
|
||||
- WAHA_DASHBOARD_USERNAME=waha
|
||||
- WAHA_DASHBOARD_PASSWORD=waha
|
||||
- WAHA_DASHBOARD_USERNAME=admin
|
||||
- WAHA_DASHBOARD_PASSWORD=admin
|
||||
# Engine specific environment variables
|
||||
# - WAHA_WEBJS_WEB_VERSION=2.2412.54-videofix
|
||||
restart: always
|
||||
|
||||
+2
-2
@@ -2,10 +2,10 @@
|
||||
|
||||
# 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}"
|
||||
|
||||
# Start your application using yarn
|
||||
# Start your application using node with exec to ensure proper signal handling
|
||||
exec node dist/main
|
||||
+33
-17
@@ -20,30 +20,35 @@
|
||||
"test:watch": "jest --watch",
|
||||
"test:cov": "jest --coverage",
|
||||
"test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand",
|
||||
"test:e2e": "jest --config ./test/jest-e2e.json"
|
||||
"test:e2e": "jest --config ./test/jest-e2e.json",
|
||||
"gows:proto:fetch": "node scripts/gows-proto.js fetch",
|
||||
"gows:proto:build": "node scripts/gows-proto.js build",
|
||||
"gows:proto": "yarn gows:proto:fetch && yarn gows:proto:build"
|
||||
},
|
||||
"dependencies": {
|
||||
"@adiwajshing/baileys": "github:devlikeapro/Baileys#fork-master-channels-678",
|
||||
"@adiwajshing/baileys": "github:devlikeapro/Baileys#fork-master-2025-05-01",
|
||||
"@adiwajshing/keyed-db": "^0.2.4",
|
||||
"@aws-sdk/client-s3": "^3.633.0",
|
||||
"@aws-sdk/s3-request-presigner": "^3.633.0",
|
||||
"@nestjs/axios": "^3.0.2",
|
||||
"@nestjs/common": "^9.0.9",
|
||||
"@nestjs/common": "^11.0.0",
|
||||
"@nestjs/config": "^3.2.3",
|
||||
"@nestjs/core": "^9.0.9",
|
||||
"@nestjs/passport": "^9.0.0",
|
||||
"@nestjs/platform-express": "^9.0.9",
|
||||
"@nestjs/platform-ws": "^9.0.9",
|
||||
"@nestjs/serve-static": "^2.1.3",
|
||||
"@nestjs/core": "^11.0.0",
|
||||
"@nestjs/passport": "^11.0.0",
|
||||
"@nestjs/platform-express": "^11.0.0",
|
||||
"@nestjs/platform-ws": "^11.0.0",
|
||||
"@nestjs/serve-static": "^5.0.0",
|
||||
"@nestjs/swagger": "^7.1.11",
|
||||
"@nestjs/terminus": "^10.2.3",
|
||||
"@nestjs/websockets": "^9.0.9",
|
||||
"@nestjs/websockets": "^11.0.0",
|
||||
"@types/better-sqlite3": "^7.6.10",
|
||||
"@types/lodash": "^4.14.194",
|
||||
"@types/passport": "^1.0.17",
|
||||
"@types/ws": "^8.5.4",
|
||||
"adm-zip": "0.5.10",
|
||||
"agentkeepalive": "^4.5.0",
|
||||
"async-lock": "^1.4.1",
|
||||
"audio-decode": "^2.2.2",
|
||||
"axios": "^1.7.7",
|
||||
"axios-retry": "^4.5.0",
|
||||
"better-sqlite3": "11.3.0",
|
||||
@@ -55,17 +60,21 @@
|
||||
"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",
|
||||
"mongodb": "6.9.0",
|
||||
"nestjs-pino": "^4.1.0",
|
||||
"passport": "^0.6.0",
|
||||
"node-cache": "5.1.2",
|
||||
"passport": "^0.7.0",
|
||||
"passport-headerapikey": "^1.2.2",
|
||||
"pg": "^8.13.1",
|
||||
"pino-http": "^10.2.0",
|
||||
"pino-pretty": "^11.2.1",
|
||||
"pretty-bytes": "5.6.0",
|
||||
@@ -79,7 +88,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"
|
||||
@@ -87,13 +97,16 @@
|
||||
"resolutions": {
|
||||
"bufferutil": "^4.0.8",
|
||||
"ws": "^8.18.0",
|
||||
"puppeteer": "^23.6.0",
|
||||
"whatwg-url": "13.0.0"
|
||||
"puppeteer": "^24.7.2",
|
||||
"whatwg-url": "13.0.0",
|
||||
"libsignal": "github:devlikeapro/libsignal-node#fork-master"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@nestjs/cli": "^9.0.0",
|
||||
"@nestjs/schematics": "^9.0.1",
|
||||
"@nestjs/testing": "^9.0.9",
|
||||
"@grpc/grpc-js": "^1.12.4",
|
||||
"@grpc/proto-loader": "^0.7.13",
|
||||
"@nestjs/cli": "^11.0.0",
|
||||
"@nestjs/schematics": "^11.0.0",
|
||||
"@nestjs/testing": "^11.0.0",
|
||||
"@types/express": "^4.17.3",
|
||||
"@types/jest": "26.0.10",
|
||||
"@types/node": "^18.14.2",
|
||||
@@ -104,14 +117,17 @@
|
||||
"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",
|
||||
"ts-node": "^10.9.1",
|
||||
"tsconfig-paths": "^4.1.0",
|
||||
"typescript": "^4.8.4"
|
||||
"typescript": "^4.8.4",
|
||||
"yargs": "^17.7.2"
|
||||
},
|
||||
"jest": {
|
||||
"moduleFileExtensions": [
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Set source and output file
|
||||
SOURCE_DIR="src"
|
||||
OUTPUT_DIR="$HOME/Downloads/chatgpt"
|
||||
OUTPUT_FILE="$OUTPUT_DIR/waha.ts"
|
||||
|
||||
# Create or clear the output file
|
||||
rm -f "$OUTPUT_FILE"
|
||||
touch "$OUTPUT_FILE"
|
||||
|
||||
# Find all .md files in the source directory and combine them
|
||||
find "$SOURCE_DIR" -type f -name "*.ts" | while read -r file; do
|
||||
# Add file name to OUTPUT_FILE
|
||||
echo -e "\n-------" >> "$OUTPUT_FILE"
|
||||
echo "File: $file" >> "$OUTPUT_FILE"
|
||||
echo -e "-------\n" >> "$OUTPUT_FILE"
|
||||
# Append the content of the file to the output file
|
||||
cat "$file" >> "$OUTPUT_FILE"
|
||||
done
|
||||
|
||||
echo "All .ts files combined into $OUTPUT_FILE with separators."
|
||||
@@ -0,0 +1,144 @@
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
const fs = require('fs');
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
const path = require('path');
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
const axios = require('axios');
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
const { execSync } = require('child_process');
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
const yargs = require('yargs');
|
||||
|
||||
// Defaults
|
||||
const CONFIG_FILE = 'waha.config.json';
|
||||
// Load defaults from package.json
|
||||
gows = (() => {
|
||||
try {
|
||||
const config = JSON.parse(fs.readFileSync(CONFIG_FILE, 'utf8'));
|
||||
return config.waha.gows || {};
|
||||
} catch (error) {
|
||||
return {};
|
||||
}
|
||||
})();
|
||||
|
||||
const DEFAULT_REPO = gows.repo;
|
||||
if (!DEFAULT_REPO) {
|
||||
throw new Error(`Missing default repo in ${CONFIG_FILE}`);
|
||||
}
|
||||
const DEFAULT_REF = gows.ref;
|
||||
if (!DEFAULT_REF) {
|
||||
throw new Error('Missing default ref in ${CONFIG_FILE}');
|
||||
}
|
||||
const DEFAULT_DIR = './src/core/engines/gows/proto';
|
||||
|
||||
const PROTO_FILES = ['gows.proto'];
|
||||
const PROTO_OUTPUT = './src/core/engines/gows/grpc';
|
||||
|
||||
// Helper function to clean directory
|
||||
function cleanDirectory(directory, suffix) {
|
||||
if (!fs.existsSync(directory)) {
|
||||
fs.mkdirSync(directory, { recursive: true });
|
||||
return;
|
||||
}
|
||||
|
||||
const files = fs.readdirSync(directory);
|
||||
for (const file of files) {
|
||||
const filePath = path.join(directory, file);
|
||||
if (file.endsWith(suffix)) {
|
||||
fs.unlinkSync(filePath);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Helper function to download files
|
||||
async function downloadFiles(repo, ref, directory) {
|
||||
for (const file of PROTO_FILES) {
|
||||
const url = `https://github.com/${repo}/releases/download/${ref}/${file}`;
|
||||
const filePath = path.join(directory, file);
|
||||
try {
|
||||
const response = await axios.get(url, { responseType: 'arraybuffer' });
|
||||
fs.writeFileSync(filePath, response.data);
|
||||
console.log(`Downloaded: ${file}`);
|
||||
} catch (error) {
|
||||
console.error(`Failed to download ${file}: ${error.message}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Handler for fetch command
|
||||
async function handleFetch(repo, ref, dir) {
|
||||
console.log(`Fetching .proto files from ${repo}@${ref} to ${dir}...`);
|
||||
cleanDirectory(dir, '.proto');
|
||||
await downloadFiles(repo, ref, dir);
|
||||
}
|
||||
|
||||
// Handler for build command
|
||||
function handleBuild(dir) {
|
||||
console.log('Building gRPC files...');
|
||||
cleanDirectory(PROTO_OUTPUT);
|
||||
|
||||
const command = `grpc_tools_node_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:${PROTO_OUTPUT} \
|
||||
--grpc_out=grpc_js:${PROTO_OUTPUT} \
|
||||
--ts_out=grpc_js:${PROTO_OUTPUT} \
|
||||
-I ${dir} ${dir}/gows.proto`;
|
||||
|
||||
try {
|
||||
execSync(command, { stdio: 'inherit' });
|
||||
console.log('gRPC files built successfully.');
|
||||
} catch (error) {
|
||||
console.error(`Failed to build gRPC files: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// Commands
|
||||
//
|
||||
|
||||
// fetch
|
||||
yargs.command(
|
||||
'fetch',
|
||||
'Fetch .proto files from GitHub',
|
||||
(yargs) => {
|
||||
yargs
|
||||
.option('repo', {
|
||||
describe: 'GitHub repository (owner/repo)',
|
||||
type: 'string',
|
||||
default: DEFAULT_REPO,
|
||||
})
|
||||
.option('ref', {
|
||||
describe: 'Git reference (branch or commit SHA)',
|
||||
type: 'string',
|
||||
default: DEFAULT_REF,
|
||||
})
|
||||
.option('dir', {
|
||||
describe: 'Directory to output .proto files',
|
||||
type: 'string',
|
||||
default: DEFAULT_DIR,
|
||||
});
|
||||
},
|
||||
async (argv) => {
|
||||
await handleFetch(argv.repo, argv.ref, argv.dir);
|
||||
},
|
||||
);
|
||||
|
||||
// build
|
||||
yargs.command(
|
||||
'build',
|
||||
'Build gRPC files from .proto files',
|
||||
(yargs) => {
|
||||
yargs.option('dir', {
|
||||
describe: 'Directory containing .proto files',
|
||||
type: 'string',
|
||||
default: DEFAULT_DIR,
|
||||
});
|
||||
},
|
||||
(argv) => {
|
||||
handleBuild(argv.dir);
|
||||
},
|
||||
);
|
||||
|
||||
// Parse arguments
|
||||
yargs.parse();
|
||||
@@ -1,22 +0,0 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
echo "Starting WAHA..."
|
||||
|
||||
docker run -d \
|
||||
--restart=always \
|
||||
--name waha \
|
||||
-p 3000:3000/tcp \
|
||||
-v ./.sessions:/app/.sessions \
|
||||
-v ./.media:/app/.media \
|
||||
--env WHATSAPP_API_KEY=321 \
|
||||
--env WAHA_DASHBOARD_USERNAME=waha \
|
||||
--env WAHA_DASHBOARD_PASSWORD=waha \
|
||||
--env WAHA_LOG_FORMAT=JSON \
|
||||
--env WAHA_LOG_LEVEL=info \
|
||||
--env WHATSAPP_DEFAULT_ENGINE=WEBJS \
|
||||
--env WAHA_PRINT_QR=False \
|
||||
--env WHATSAPP_FILES_LIFETIME=0 \
|
||||
--env WHATSAPP_FILES_FOLDER=/app/.media \
|
||||
devlikeapro/waha-plus:latest
|
||||
|
||||
echo "WAHA started."
|
||||
@@ -1,21 +0,0 @@
|
||||
set -e
|
||||
echo "Updating WAHA..."
|
||||
echo "Logging in to Docker..."
|
||||
docker login -u devlikeapro # -p {KEY} - you can hard code key here https://portal.devlike.pro/
|
||||
|
||||
echo "Pulling the latest image..."
|
||||
docker pull devlikeapro/waha-plus
|
||||
|
||||
echo "Logging out of Docker..."
|
||||
docker logout
|
||||
|
||||
# Just pull if you're using WAHA Core
|
||||
# docker pull devlikeapro/waha
|
||||
|
||||
echo "Stopping the current container..."
|
||||
docker stop waha || true
|
||||
echo "Removing the current container..."
|
||||
docker rm waha || true
|
||||
|
||||
echo "Running WAHA..."
|
||||
sh ./waha-run.sh
|
||||
@@ -6,7 +6,6 @@ import {
|
||||
Query,
|
||||
UseInterceptors,
|
||||
} from '@nestjs/common';
|
||||
import { UnprocessableEntityException } from '@nestjs/common/exceptions/unprocessable-entity.exception';
|
||||
import { ApiOperation, ApiSecurity, ApiTags } from '@nestjs/swagger';
|
||||
import { ApiFileAcceptHeader } from '@waha/nestjs/ApiFileAcceptHeader';
|
||||
import {
|
||||
@@ -19,8 +18,6 @@ import { SessionManager } from '../core/abc/manager.abc';
|
||||
import { WhatsappSession } from '../core/abc/session.abc';
|
||||
import { BufferResponseInterceptor } from '../nestjs/BufferResponseInterceptor';
|
||||
import {
|
||||
CaptchaBody,
|
||||
OTPRequest,
|
||||
QRCodeFormat,
|
||||
QRCodeQuery,
|
||||
QRCodeValue,
|
||||
@@ -63,42 +60,6 @@ class AuthController {
|
||||
) {
|
||||
return session.requestCode(request.phoneNumber, request.method, request);
|
||||
}
|
||||
|
||||
@Post('authorize-code')
|
||||
@SessionApiParam
|
||||
@ApiOperation({
|
||||
summary: 'Send OTP authentication code.',
|
||||
})
|
||||
authorizeCode(
|
||||
@SessionParam session: WhatsappSession,
|
||||
@Body() request: OTPRequest,
|
||||
) {
|
||||
return session.authorizeCode(request.code);
|
||||
}
|
||||
|
||||
@Get('captcha')
|
||||
@SessionApiParam
|
||||
@ApiFileAcceptHeader()
|
||||
@UseInterceptors(new BufferResponseInterceptor())
|
||||
@ApiOperation({
|
||||
summary: 'Get captcha image.',
|
||||
})
|
||||
async getCaptcha(@SessionParam session: WhatsappSession): Promise<Buffer> {
|
||||
const image = await session.getCaptcha();
|
||||
return image.get();
|
||||
}
|
||||
|
||||
@Post('captcha')
|
||||
@SessionApiParam
|
||||
@ApiOperation({
|
||||
summary: 'Enter captcha code.',
|
||||
})
|
||||
saveCaptcha(
|
||||
@SessionParam session: WhatsappSession,
|
||||
@Body() body: CaptchaBody,
|
||||
) {
|
||||
return session.saveCaptcha(body.code);
|
||||
}
|
||||
}
|
||||
|
||||
export { AuthController };
|
||||
@@ -3,31 +3,51 @@ import {
|
||||
Controller,
|
||||
Delete,
|
||||
Get,
|
||||
HttpCode,
|
||||
HttpStatus,
|
||||
Param,
|
||||
Post,
|
||||
Query,
|
||||
UsePipes,
|
||||
} from '@nestjs/common';
|
||||
import { ApiOperation, ApiSecurity, ApiTags } from '@nestjs/swagger';
|
||||
import { ApiOperation, ApiParam, ApiSecurity, ApiTags } from '@nestjs/swagger';
|
||||
import { ChannelsInfoServiceCore } from '@waha/core/services/ChannelsInfoServiceCore';
|
||||
import { isJidNewsletter } from '@waha/core/utils/jids';
|
||||
import {
|
||||
SessionApiParam,
|
||||
WorkingSessionParam,
|
||||
} from '@waha/nestjs/params/SessionApiParam';
|
||||
import { WAHAValidationPipe } from '@waha/nestjs/pipes/WAHAValidationPipe';
|
||||
import {
|
||||
Channel,
|
||||
ChannelCategory,
|
||||
ChannelCountry,
|
||||
ChannelListResult,
|
||||
ChannelMessage,
|
||||
ChannelSearchByText,
|
||||
ChannelSearchByView,
|
||||
ChannelView,
|
||||
CreateChannelRequest,
|
||||
ListChannelsQuery,
|
||||
NewsletterIdApiParam,
|
||||
NewsletterIdOrInviteCodeApiParam,
|
||||
PreviewChannelMessages,
|
||||
} from '@waha/structures/channels.dto';
|
||||
|
||||
import { SessionManager } from '../core/abc/manager.abc';
|
||||
import { isNewsletter, WhatsappSession } from '../core/abc/session.abc';
|
||||
import {
|
||||
parseChannelInviteLink,
|
||||
WhatsappSession,
|
||||
} from '../core/abc/session.abc';
|
||||
|
||||
@ApiSecurity('api_key')
|
||||
@Controller('api/:session/channels')
|
||||
@ApiTags('📢 Channels')
|
||||
export class ChannelsController {
|
||||
constructor(private manager: SessionManager) {}
|
||||
constructor(
|
||||
private manager: SessionManager,
|
||||
private channelsInfoService: ChannelsInfoServiceCore,
|
||||
) {}
|
||||
|
||||
@Get('')
|
||||
@SessionApiParam
|
||||
@@ -73,15 +93,47 @@ export class ChannelsController {
|
||||
@WorkingSessionParam session: WhatsappSession,
|
||||
@Param('id') id: string,
|
||||
): Promise<Channel> {
|
||||
if (isNewsletter(id)) {
|
||||
if (isJidNewsletter(id)) {
|
||||
return session.channelsGetChannel(id);
|
||||
} else {
|
||||
// https://www.whatsapp.com/channel/123 => 123
|
||||
const inviteCode = id.split('/').pop();
|
||||
const inviteCode = parseChannelInviteLink(id);
|
||||
return session.channelsGetChannelByInviteCode(inviteCode);
|
||||
}
|
||||
}
|
||||
|
||||
@Get(':id/messages/preview')
|
||||
@SessionApiParam
|
||||
@UsePipes(new WAHAValidationPipe())
|
||||
@ApiParam({
|
||||
name: 'id',
|
||||
description: 'Channel id or invite code',
|
||||
required: true,
|
||||
type: 'string',
|
||||
schema: {
|
||||
default: '0029Va4K0PZ5a245NkngBA2M',
|
||||
},
|
||||
})
|
||||
@ApiOperation({
|
||||
summary: 'Preview channel messages',
|
||||
description:
|
||||
'You can use either ' +
|
||||
'invite code (https://www.whatsapp.com/channel/123) or (123)' +
|
||||
'OR' +
|
||||
'Channel ID (123@newsletter).',
|
||||
})
|
||||
async previewChannelMessages(
|
||||
@WorkingSessionParam session: WhatsappSession,
|
||||
@Param('id') code: string,
|
||||
@Query() query: PreviewChannelMessages,
|
||||
): Promise<ChannelMessage[]> {
|
||||
if (isJidNewsletter(code)) {
|
||||
const channel = await session.channelsGetChannel(code);
|
||||
code = parseChannelInviteLink(channel.invite);
|
||||
}
|
||||
const inviteCode = parseChannelInviteLink(code);
|
||||
return session.previewChannelMessages(inviteCode, query);
|
||||
}
|
||||
|
||||
@Post(':id/follow')
|
||||
@SessionApiParam
|
||||
@NewsletterIdApiParam
|
||||
@@ -125,4 +177,49 @@ export class ChannelsController {
|
||||
): Promise<void> {
|
||||
return session.channelsUnmuteChannel(id);
|
||||
}
|
||||
|
||||
@Post('/search/by-view')
|
||||
@HttpCode(HttpStatus.OK)
|
||||
@SessionApiParam
|
||||
@UsePipes(new WAHAValidationPipe())
|
||||
@ApiOperation({ summary: 'Search for channels (by view)' })
|
||||
async searchByView(
|
||||
@WorkingSessionParam session: WhatsappSession,
|
||||
@Body() request: ChannelSearchByView,
|
||||
): Promise<ChannelListResult> {
|
||||
return session.searchChannelsByView(request);
|
||||
}
|
||||
|
||||
@Post('/search/by-text')
|
||||
@HttpCode(HttpStatus.OK)
|
||||
@SessionApiParam
|
||||
@UsePipes(new WAHAValidationPipe())
|
||||
@ApiOperation({ summary: 'Search for channels (by text)' })
|
||||
async searchByText(
|
||||
@WorkingSessionParam session: WhatsappSession,
|
||||
@Body() request: ChannelSearchByText,
|
||||
): Promise<ChannelListResult> {
|
||||
return session.searchChannelsByText(request);
|
||||
}
|
||||
|
||||
@Get('/search/views')
|
||||
@SessionApiParam
|
||||
@ApiOperation({ summary: 'Get list of views for channel search' })
|
||||
getSearchViews(): Promise<ChannelView[]> {
|
||||
return this.channelsInfoService.getViews();
|
||||
}
|
||||
|
||||
@Get('/search/countries')
|
||||
@SessionApiParam
|
||||
@ApiOperation({ summary: 'Get list of countries for channel search' })
|
||||
getSearchCountries(): Promise<ChannelCountry[]> {
|
||||
return this.channelsInfoService.getCountries();
|
||||
}
|
||||
|
||||
@Get('/search/categories')
|
||||
@SessionApiParam
|
||||
@ApiOperation({ summary: 'Get list of categories for channel search' })
|
||||
getSearchCategories(): Promise<ChannelCategory[]> {
|
||||
return this.channelsInfoService.getCategories();
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
import { isJidGroup } from '@adiwajshing/baileys';
|
||||
import {
|
||||
Body,
|
||||
Controller,
|
||||
@@ -21,13 +22,21 @@ 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,
|
||||
OverviewBodyRequest,
|
||||
OverviewFilter,
|
||||
OverviewPaginationParams,
|
||||
PinMessageRequest,
|
||||
ReadChatMessagesQuery,
|
||||
ReadChatMessagesResponse,
|
||||
transformAck,
|
||||
} from '../structures/chats.dto';
|
||||
import { EditMessageRequest } from '../structures/chatting.dto';
|
||||
|
||||
@@ -48,6 +57,35 @@ 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,
|
||||
@Query() filter: OverviewFilter,
|
||||
): Promise<ChatSummary[]> {
|
||||
return session.getChatsOverview(pagination, filter);
|
||||
}
|
||||
|
||||
@Post('overview')
|
||||
@SessionApiParam
|
||||
@ApiOperation({
|
||||
summary:
|
||||
'Get chats overview. Use POST if you have too many "ids" params - GET can limit it',
|
||||
})
|
||||
@UsePipes(new ValidationPipe({ transform: true, whitelist: true }))
|
||||
postChatsOverview(
|
||||
@WorkingSessionParam session: WhatsappSession,
|
||||
@Body() body: OverviewBodyRequest,
|
||||
): Promise<ChatSummary[]> {
|
||||
return session.getChatsOverview(body.pagination, body.filter);
|
||||
}
|
||||
|
||||
@Delete(':chatId')
|
||||
@SessionApiParam
|
||||
@ApiOperation({ summary: 'Deletes the chat' })
|
||||
@@ -59,6 +97,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' })
|
||||
@@ -70,9 +121,23 @@ class ChatsController {
|
||||
@WorkingSessionParam session: WhatsappSession,
|
||||
@Param('chatId') chatId: string,
|
||||
) {
|
||||
filter = transformAck(filter);
|
||||
return session.getChatMessages(chatId, query, filter);
|
||||
}
|
||||
|
||||
@Post(':chatId/messages/read')
|
||||
@SessionApiParam
|
||||
@ApiOperation({ summary: 'Read unread messages in the chat' })
|
||||
@ChatIdApiParam
|
||||
@UsePipes(new ValidationPipe({ transform: true, whitelist: true }))
|
||||
readChatMessages(
|
||||
@Query() query: ReadChatMessagesQuery,
|
||||
@WorkingSessionParam session: WhatsappSession,
|
||||
@Param('chatId') chatId: string,
|
||||
): Promise<ReadChatMessagesResponse> {
|
||||
return session.readChatMessages(chatId, query);
|
||||
}
|
||||
|
||||
@Get(':chatId/messages/:messageId')
|
||||
@SessionApiParam
|
||||
@ApiOperation({ summary: 'Gets message by id' })
|
||||
|
||||
@@ -10,7 +10,11 @@ import {
|
||||
} from '@nestjs/common';
|
||||
import { ApiOperation, ApiSecurity, ApiTags } from '@nestjs/swagger';
|
||||
import { WAHAValidationPipe } from '@waha/nestjs/pipes/WAHAValidationPipe';
|
||||
import { GetChatMessagesFilter } from '@waha/structures/chats.dto';
|
||||
import {
|
||||
GetChatMessagesFilter,
|
||||
ReadChatMessagesQuery,
|
||||
transformAck,
|
||||
} from '@waha/structures/chats.dto';
|
||||
import { SendButtonsRequest } from '@waha/structures/chatting.buttons.dto';
|
||||
|
||||
import { SessionManager } from '../core/abc/manager.abc';
|
||||
@@ -18,10 +22,12 @@ import {
|
||||
ChatRequest,
|
||||
CheckNumberStatusQuery,
|
||||
GetMessageQuery,
|
||||
MessageButtonReply,
|
||||
MessageContactVcardRequest,
|
||||
MessageFileRequest,
|
||||
MessageForwardRequest,
|
||||
MessageImageRequest,
|
||||
MessageLinkCustomPreviewRequest,
|
||||
MessageLinkPreviewRequest,
|
||||
MessageLocationRequest,
|
||||
MessagePollRequest,
|
||||
@@ -94,6 +100,25 @@ export class ChattingController {
|
||||
return whatsapp.sendVideo(request);
|
||||
}
|
||||
|
||||
@Post('/send/link-custom-preview')
|
||||
@ApiOperation({
|
||||
summary: 'Send a text message with a CUSTOM link preview.',
|
||||
description:
|
||||
'You can use regular /api/sendText if you wanna send auto-generated link preview.',
|
||||
})
|
||||
@UsePipes(new WAHAValidationPipe())
|
||||
async sendLinkCustomPreview(
|
||||
@Body() request: MessageLinkCustomPreviewRequest,
|
||||
): Promise<any> {
|
||||
const whatsapp = await this.manager.getWorkingSession(request.session);
|
||||
if (!request.text.includes(request.preview.url)) {
|
||||
throw new Error(
|
||||
'"text" must include the URL provided in the "preview.url"',
|
||||
);
|
||||
}
|
||||
return whatsapp.sendLinkCustomPreview(request);
|
||||
}
|
||||
|
||||
@Post('/sendButtons')
|
||||
@ApiOperation({
|
||||
summary: 'Send buttons (interactive message)',
|
||||
@@ -115,6 +140,15 @@ export class ChattingController {
|
||||
|
||||
@Post('/sendSeen')
|
||||
async sendSeen(@Body() chat: SendSeenRequest) {
|
||||
const hasMessageId = chat.messageIds?.length > 0 || Boolean(chat.messageId);
|
||||
if (!hasMessageId) {
|
||||
const whatsapp = await this.manager.getWorkingSession(chat.session);
|
||||
const query: ReadChatMessagesQuery = {
|
||||
messages: null,
|
||||
days: 7,
|
||||
};
|
||||
return whatsapp.readChatMessages(chat.chatId, query);
|
||||
}
|
||||
const whatsapp = await this.manager.getWorkingSession(chat.session);
|
||||
return whatsapp.sendSeen(chat);
|
||||
}
|
||||
@@ -165,21 +199,20 @@ export class ChattingController {
|
||||
return whatsapp.sendLocation(request);
|
||||
}
|
||||
|
||||
@Post('/sendLinkPreview')
|
||||
async sendLinkPreview(@Body() request: MessageLinkPreviewRequest) {
|
||||
@Post('/sendContactVcard')
|
||||
async sendContactVcard(@Body() request: MessageContactVcardRequest) {
|
||||
const whatsapp = await this.manager.getWorkingSession(request.session);
|
||||
return whatsapp.sendLinkPreview(request);
|
||||
return whatsapp.sendContactVCard(request);
|
||||
}
|
||||
|
||||
@Get('/messages')
|
||||
@ApiOperation({ summary: 'Get messages in a chat' })
|
||||
@Post('/send/buttons/reply')
|
||||
@ApiOperation({
|
||||
summary: 'Reply on a button message',
|
||||
})
|
||||
@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);
|
||||
async sendButtonsReply(@Body() request: MessageButtonReply) {
|
||||
const whatsapp = await this.manager.getWorkingSession(request.session);
|
||||
return whatsapp.sendButtonsReply(request);
|
||||
}
|
||||
|
||||
@Get('/sendText')
|
||||
@@ -192,10 +225,20 @@ 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,
|
||||
) {
|
||||
filter = transformAck(filter);
|
||||
const whatsapp = await this.manager.getWorkingSession(query.session);
|
||||
return whatsapp.getChatMessages(query.chatId, query, filter);
|
||||
}
|
||||
|
||||
@Get('/checkNumberStatus')
|
||||
@@ -221,4 +264,11 @@ export class ChattingController {
|
||||
const whatsapp = await this.manager.getWorkingSession(request.session);
|
||||
return whatsapp.reply(request);
|
||||
}
|
||||
|
||||
@Post('/sendLinkPreview')
|
||||
@ApiOperation({ deprecated: true })
|
||||
async sendLinkPreview_DEPRECATED(@Body() request: MessageLinkPreviewRequest) {
|
||||
const whatsapp = await this.manager.getWorkingSession(request.session);
|
||||
return whatsapp.sendLinkPreview(request);
|
||||
}
|
||||
}
|
||||
@@ -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')
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
import {
|
||||
Body,
|
||||
Controller,
|
||||
Param,
|
||||
Post,
|
||||
UsePipes,
|
||||
ValidationPipe,
|
||||
} from '@nestjs/common';
|
||||
import { ApiOperation, ApiSecurity, ApiTags } from '@nestjs/swagger';
|
||||
import { WhatsappSession } from '@waha/core/abc/session.abc';
|
||||
import {
|
||||
SessionApiParam,
|
||||
WorkingSessionParam,
|
||||
} from '@waha/nestjs/params/SessionApiParam';
|
||||
|
||||
import { SessionManager } from '../core/abc/manager.abc';
|
||||
import {
|
||||
EventCancelRequest,
|
||||
EventMessageRequest,
|
||||
} from '../structures/events.dto';
|
||||
import { WAMessage } from '../structures/responses.dto';
|
||||
|
||||
@ApiSecurity('api_key')
|
||||
@Controller('api/:session/events')
|
||||
@ApiTags('📅 Events')
|
||||
export class EventsController {
|
||||
constructor(private manager: SessionManager) {}
|
||||
|
||||
@Post()
|
||||
@ApiOperation({ summary: 'Send an event message' })
|
||||
@UsePipes(new ValidationPipe())
|
||||
@SessionApiParam
|
||||
async sendEvent(
|
||||
@WorkingSessionParam session: WhatsappSession,
|
||||
@Body() request: EventMessageRequest,
|
||||
): Promise<WAMessage> {
|
||||
return session.sendEvent(request);
|
||||
}
|
||||
|
||||
// @Post(':id/cancel')
|
||||
// @ApiOperation({ summary: 'Cancel an event by ID' })
|
||||
// @UsePipes(new ValidationPipe())
|
||||
// @SessionApiParam
|
||||
// async cancelEvent(
|
||||
// @WorkingSessionParam session: WhatsappSession,
|
||||
// @Param('id') id: string,
|
||||
// ): Promise<WAMessage> {
|
||||
// return session.cancelEvent(id);
|
||||
// }
|
||||
}
|
||||
+172
-57
@@ -3,9 +3,14 @@ import {
|
||||
Controller,
|
||||
Delete,
|
||||
Get,
|
||||
HttpCode,
|
||||
HttpStatus,
|
||||
Param,
|
||||
Post,
|
||||
Put,
|
||||
Query,
|
||||
UsePipes,
|
||||
ValidationPipe,
|
||||
} from '@nestjs/common';
|
||||
import { ApiOperation, ApiSecurity, ApiTags } from '@nestjs/swagger';
|
||||
import { GroupIdApiParam } from '@waha/nestjs/params/ChatIdApiParam';
|
||||
@@ -13,12 +18,23 @@ import {
|
||||
SessionApiParam,
|
||||
WorkingSessionParam,
|
||||
} from '@waha/nestjs/params/SessionApiParam';
|
||||
import { CountResponse, Result } from '@waha/structures/base.dto';
|
||||
import {
|
||||
ChatPictureQuery,
|
||||
ChatPictureResponse,
|
||||
} from '@waha/structures/chats.dto';
|
||||
import { ProfilePictureRequest } from '@waha/structures/profile.dto';
|
||||
|
||||
import { SessionManager } from '../core/abc/manager.abc';
|
||||
import { WhatsappSession } from '../core/abc/session.abc';
|
||||
import { parseGroupInviteLink, WhatsappSession } from '../core/abc/session.abc';
|
||||
import {
|
||||
CreateGroupRequest,
|
||||
DescriptionRequest,
|
||||
GroupField,
|
||||
GroupsListFields,
|
||||
GroupsPaginationParams,
|
||||
JoinGroupRequest,
|
||||
JoinGroupResponse,
|
||||
ParticipantsRequest,
|
||||
SettingsSecurityChangeInfo,
|
||||
SubjectRequest,
|
||||
@@ -40,11 +56,65 @@ 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> {
|
||||
const code = parseGroupInviteLink(query.code);
|
||||
return session.joinInfoGroup(code);
|
||||
}
|
||||
|
||||
@Post('join')
|
||||
@HttpCode(HttpStatus.OK)
|
||||
@SessionApiParam
|
||||
@ApiOperation({ summary: 'Join group via code' })
|
||||
async joinGroup(
|
||||
@WorkingSessionParam session: WhatsappSession,
|
||||
@Body() request: JoinGroupRequest,
|
||||
): Promise<JoinGroupResponse> {
|
||||
const code = parseGroupInviteLink(request.code);
|
||||
const id = await session.joinGroup(code);
|
||||
return { id: id };
|
||||
}
|
||||
|
||||
@Get('')
|
||||
@SessionApiParam
|
||||
@ApiOperation({ summary: 'Get all groups.' })
|
||||
getGroups(@WorkingSessionParam session: WhatsappSession) {
|
||||
return session.getGroups();
|
||||
@UsePipes(new ValidationPipe({ transform: true, whitelist: true }))
|
||||
async getGroups(
|
||||
@WorkingSessionParam session: WhatsappSession,
|
||||
@Query() pagination: GroupsPaginationParams,
|
||||
@Query() fields: GroupsListFields,
|
||||
) {
|
||||
let groups: any = await session.getGroups(pagination);
|
||||
groups = session.filterGroupsFields(groups, fields);
|
||||
return groups;
|
||||
}
|
||||
|
||||
@Get('/count')
|
||||
@SessionApiParam
|
||||
@ApiOperation({ summary: 'Get the number of groups.' })
|
||||
@UsePipes(new ValidationPipe({ transform: true, whitelist: true }))
|
||||
async getGroupsCount(
|
||||
@WorkingSessionParam session: WhatsappSession,
|
||||
): Promise<CountResponse> {
|
||||
const data: any = await session.getGroups({});
|
||||
const groups: any[] = Array.isArray(data) ? data : Object.values(data);
|
||||
return {
|
||||
count: groups.length,
|
||||
};
|
||||
}
|
||||
|
||||
@Post('refresh')
|
||||
@HttpCode(HttpStatus.OK)
|
||||
@SessionApiParam
|
||||
@ApiOperation({ summary: 'Refresh groups from the server.' })
|
||||
@UsePipes(new ValidationPipe({ transform: true, whitelist: true }))
|
||||
async refreshGroups(@WorkingSessionParam session: WhatsappSession) {
|
||||
return { success: await session.refreshGroups() };
|
||||
}
|
||||
|
||||
@Get(':id')
|
||||
@@ -58,6 +128,100 @@ export class GroupsController {
|
||||
return session.getGroup(id);
|
||||
}
|
||||
|
||||
@Delete(':id')
|
||||
@SessionApiParam
|
||||
@GroupIdApiParam
|
||||
@ApiOperation({ summary: 'Delete the group.' })
|
||||
deleteGroup(
|
||||
@WorkingSessionParam session: WhatsappSession,
|
||||
@Param('id') id: string,
|
||||
) {
|
||||
return session.deleteGroup(id);
|
||||
}
|
||||
|
||||
@Post(':id/leave')
|
||||
@HttpCode(HttpStatus.OK)
|
||||
@SessionApiParam
|
||||
@GroupIdApiParam
|
||||
@ApiOperation({ summary: 'Leave the group.' })
|
||||
leaveGroup(
|
||||
@WorkingSessionParam session: WhatsappSession,
|
||||
@Param('id') id: string,
|
||||
) {
|
||||
return session.leaveGroup(id);
|
||||
}
|
||||
|
||||
@Get(':id/picture')
|
||||
@SessionApiParam
|
||||
@GroupIdApiParam
|
||||
@ApiOperation({ summary: 'Get group picture' })
|
||||
@UsePipes(new ValidationPipe({ transform: true, whitelist: true }))
|
||||
async getChatPicture(
|
||||
@WorkingSessionParam session: WhatsappSession,
|
||||
@Param('id') id: string,
|
||||
@Query() query: ChatPictureQuery,
|
||||
): Promise<ChatPictureResponse> {
|
||||
const url = await session.getContactProfilePicture(id, query.refresh);
|
||||
return { url: url };
|
||||
}
|
||||
|
||||
@Put(':id/picture')
|
||||
@SessionApiParam
|
||||
@GroupIdApiParam
|
||||
@ApiOperation({ summary: 'Set group picture' })
|
||||
async setPicture(
|
||||
@Param('id') id: string,
|
||||
@WorkingSessionParam session: WhatsappSession,
|
||||
@Body() request: ProfilePictureRequest,
|
||||
): Promise<Result> {
|
||||
const success = await session.updateGroupPicture(id, request.file);
|
||||
return { success: success ?? true };
|
||||
}
|
||||
|
||||
@Delete(':id/picture')
|
||||
@SessionApiParam
|
||||
@GroupIdApiParam
|
||||
@ApiOperation({ summary: 'Delete group picture' })
|
||||
async deletePicture(
|
||||
@Param('id') id: string,
|
||||
@WorkingSessionParam session: WhatsappSession,
|
||||
): Promise<Result> {
|
||||
const success = await session.updateGroupPicture(id, null);
|
||||
return { success: success || true };
|
||||
}
|
||||
|
||||
@Put(':id/description')
|
||||
@ApiOperation({
|
||||
summary: 'Updates the group description.',
|
||||
description:
|
||||
'Returns "true" if the subject was properly updated. This can return "false" if the user does not have the necessary permissions.',
|
||||
})
|
||||
@SessionApiParam
|
||||
@GroupIdApiParam
|
||||
setDescription(
|
||||
@WorkingSessionParam session: WhatsappSession,
|
||||
@Param('id') id: string,
|
||||
@Body() request: DescriptionRequest,
|
||||
) {
|
||||
return session.setDescription(id, request.description);
|
||||
}
|
||||
|
||||
@Put(':id/subject')
|
||||
@SessionApiParam
|
||||
@GroupIdApiParam
|
||||
@ApiOperation({
|
||||
summary: 'Updates the group subject',
|
||||
description:
|
||||
'Returns "true" if the subject was properly updated. This can return "false" if the user does not have the necessary permissions.',
|
||||
})
|
||||
setSubject(
|
||||
@WorkingSessionParam session: WhatsappSession,
|
||||
@Param('id') id: string,
|
||||
@Body() request: SubjectRequest,
|
||||
) {
|
||||
return session.setSubject(id, request.subject);
|
||||
}
|
||||
|
||||
@Put(':id/settings/security/info-admin-only')
|
||||
@SessionApiParam
|
||||
@GroupIdApiParam
|
||||
@@ -119,60 +283,6 @@ export class GroupsController {
|
||||
return session.getMessagesAdminsOnly(id);
|
||||
}
|
||||
|
||||
@Delete(':id')
|
||||
@SessionApiParam
|
||||
@GroupIdApiParam
|
||||
@ApiOperation({ summary: 'Delete the group.' })
|
||||
deleteGroup(
|
||||
@WorkingSessionParam session: WhatsappSession,
|
||||
@Param('id') id: string,
|
||||
) {
|
||||
return session.deleteGroup(id);
|
||||
}
|
||||
|
||||
@Post(':id/leave')
|
||||
@SessionApiParam
|
||||
@GroupIdApiParam
|
||||
@ApiOperation({ summary: 'Leave the group.' })
|
||||
leaveGroup(
|
||||
@WorkingSessionParam session: WhatsappSession,
|
||||
@Param('id') id: string,
|
||||
) {
|
||||
return session.leaveGroup(id);
|
||||
}
|
||||
|
||||
@Put(':id/description')
|
||||
@ApiOperation({
|
||||
summary: 'Updates the group description.',
|
||||
description:
|
||||
'Returns "true" if the subject was properly updated. This can return "false" if the user does not have the necessary permissions.',
|
||||
})
|
||||
@SessionApiParam
|
||||
@GroupIdApiParam
|
||||
setDescription(
|
||||
@WorkingSessionParam session: WhatsappSession,
|
||||
@Param('id') id: string,
|
||||
@Body() request: DescriptionRequest,
|
||||
) {
|
||||
return session.setDescription(id, request.description);
|
||||
}
|
||||
|
||||
@Put(':id/subject')
|
||||
@SessionApiParam
|
||||
@GroupIdApiParam
|
||||
@ApiOperation({
|
||||
summary: 'Updates the group subject',
|
||||
description:
|
||||
'Returns "true" if the subject was properly updated. This can return "false" if the user does not have the necessary permissions.',
|
||||
})
|
||||
setSubject(
|
||||
@WorkingSessionParam session: WhatsappSession,
|
||||
@Param('id') id: string,
|
||||
@Body() request: SubjectRequest,
|
||||
) {
|
||||
return session.setSubject(id, request.subject);
|
||||
}
|
||||
|
||||
@Get(':id/invite-code')
|
||||
@SessionApiParam
|
||||
@GroupIdApiParam
|
||||
@@ -185,6 +295,7 @@ export class GroupsController {
|
||||
}
|
||||
|
||||
@Post(':id/invite-code/revoke')
|
||||
@HttpCode(HttpStatus.OK)
|
||||
@SessionApiParam
|
||||
@GroupIdApiParam
|
||||
@ApiOperation({
|
||||
@@ -210,6 +321,7 @@ export class GroupsController {
|
||||
}
|
||||
|
||||
@Post(':id/participants/add')
|
||||
@HttpCode(HttpStatus.OK)
|
||||
@SessionApiParam
|
||||
@GroupIdApiParam
|
||||
@ApiOperation({ summary: 'Add participants' })
|
||||
@@ -222,6 +334,7 @@ export class GroupsController {
|
||||
}
|
||||
|
||||
@Post(':id/participants/remove')
|
||||
@HttpCode(HttpStatus.OK)
|
||||
@SessionApiParam
|
||||
@GroupIdApiParam
|
||||
@ApiOperation({
|
||||
@@ -236,6 +349,7 @@ export class GroupsController {
|
||||
}
|
||||
|
||||
@Post(':id/admin/promote')
|
||||
@HttpCode(HttpStatus.OK)
|
||||
@SessionApiParam
|
||||
@GroupIdApiParam
|
||||
@ApiOperation({ summary: 'Promote participants to admin users.' })
|
||||
@@ -248,6 +362,7 @@ export class GroupsController {
|
||||
}
|
||||
|
||||
@Post(':id/admin/demote')
|
||||
@HttpCode(HttpStatus.OK)
|
||||
@SessionApiParam
|
||||
@GroupIdApiParam
|
||||
@ApiOperation({ summary: 'Demotes participants to regular users.' })
|
||||
|
||||
@@ -0,0 +1,102 @@
|
||||
import { isLidUser } from '@adiwajshing/baileys/lib/WABinary/jid-utils';
|
||||
import {
|
||||
Controller,
|
||||
Get,
|
||||
Param,
|
||||
Query,
|
||||
UnprocessableEntityException,
|
||||
UsePipes,
|
||||
ValidationPipe,
|
||||
} from '@nestjs/common';
|
||||
import { ApiOperation, ApiSecurity, ApiTags } from '@nestjs/swagger';
|
||||
import { WhatsappSession } from '@waha/core/abc/session.abc';
|
||||
import {
|
||||
SessionApiParam,
|
||||
WorkingSessionParam,
|
||||
} from '@waha/nestjs/params/SessionApiParam';
|
||||
import { CountResponse } from '@waha/structures/base.dto';
|
||||
import {
|
||||
LidsListQueryParams,
|
||||
LidToPhoneNumber,
|
||||
} from '@waha/structures/lids.dto';
|
||||
import { PaginationParams, SortOrder } from '@waha/structures/pagination.dto';
|
||||
|
||||
import { SessionManager } from '../core/abc/manager.abc';
|
||||
|
||||
@ApiSecurity('api_key')
|
||||
@Controller('api/:session/lids')
|
||||
@ApiTags('👤 Contacts')
|
||||
export class LidsController {
|
||||
constructor(private manager: SessionManager) {}
|
||||
|
||||
@Get('/')
|
||||
@SessionApiParam
|
||||
@ApiOperation({ summary: 'Get all known lids to phone number mapping' })
|
||||
@UsePipes(new ValidationPipe({ transform: true, whitelist: true }))
|
||||
async getAll(
|
||||
@WorkingSessionParam session: WhatsappSession,
|
||||
@Query() params: LidsListQueryParams,
|
||||
): Promise<Array<LidToPhoneNumber>> {
|
||||
// Always lid
|
||||
const pagination: PaginationParams = params;
|
||||
pagination.sortBy = 'lid';
|
||||
pagination.sortOrder = SortOrder.ASC;
|
||||
const lids = await session.getAllLids(pagination);
|
||||
return lids;
|
||||
}
|
||||
|
||||
@Get('/count')
|
||||
@SessionApiParam
|
||||
@ApiOperation({ summary: 'Get the number of known lids' })
|
||||
@UsePipes(new ValidationPipe({ transform: true, whitelist: true }))
|
||||
async getLidsCount(
|
||||
@WorkingSessionParam session: WhatsappSession,
|
||||
): Promise<CountResponse> {
|
||||
const count = await session.getLidsCount();
|
||||
return {
|
||||
count: count,
|
||||
};
|
||||
}
|
||||
|
||||
@Get('/:lid')
|
||||
@SessionApiParam
|
||||
@ApiOperation({ summary: 'Get phone number by lid' })
|
||||
@UsePipes(new ValidationPipe({ transform: true, whitelist: true }))
|
||||
async findPNByLid(
|
||||
@WorkingSessionParam session: WhatsappSession,
|
||||
@Param('lid') lid: string,
|
||||
): Promise<LidToPhoneNumber> {
|
||||
if (!lid.includes('@')) {
|
||||
lid = lid + '@lid';
|
||||
}
|
||||
|
||||
if (!isLidUser(lid)) {
|
||||
throw new UnprocessableEntityException(
|
||||
'Invalid LID - it must end with @lid',
|
||||
);
|
||||
}
|
||||
const result = await session.findPNByLid(lid);
|
||||
result.pn = result.pn || null;
|
||||
return result;
|
||||
}
|
||||
|
||||
@Get('/pn/:phoneNumber')
|
||||
@SessionApiParam
|
||||
@ApiOperation({ summary: 'Get lid by phone number (chat id)' })
|
||||
@UsePipes(new ValidationPipe({ transform: true, whitelist: true }))
|
||||
async findLIDByPhoneNumber(
|
||||
@WorkingSessionParam session: WhatsappSession,
|
||||
@Param('phoneNumber') phoneNumber: string,
|
||||
): Promise<LidToPhoneNumber> {
|
||||
if (isLidUser(phoneNumber)) {
|
||||
return {
|
||||
lid: phoneNumber,
|
||||
pn: null,
|
||||
};
|
||||
}
|
||||
|
||||
const result = await session.findLIDByPhoneNumber(phoneNumber);
|
||||
result.lid = result.lid || null;
|
||||
return result;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
import {
|
||||
Body,
|
||||
Controller,
|
||||
Delete,
|
||||
Get,
|
||||
Put,
|
||||
UnprocessableEntityException,
|
||||
UsePipes,
|
||||
} from '@nestjs/common';
|
||||
import { ApiOperation, ApiSecurity, ApiTags } from '@nestjs/swagger';
|
||||
import { SessionManager } from '@waha/core/abc/manager.abc';
|
||||
import { WhatsappSession } from '@waha/core/abc/session.abc';
|
||||
import {
|
||||
SessionApiParam,
|
||||
WorkingSessionParam,
|
||||
} from '@waha/nestjs/params/SessionApiParam';
|
||||
import { WAHAValidationPipe } from '@waha/nestjs/pipes/WAHAValidationPipe';
|
||||
import { Result } from '@waha/structures/base.dto';
|
||||
import {
|
||||
MyProfile,
|
||||
ProfileNameRequest,
|
||||
ProfilePictureRequest,
|
||||
ProfileStatusRequest,
|
||||
} from '@waha/structures/profile.dto';
|
||||
|
||||
@ApiSecurity('api_key')
|
||||
@Controller('api/:session/profile')
|
||||
@ApiTags('🆔 Profile')
|
||||
export class ProfileController {
|
||||
constructor(private manager: SessionManager) {}
|
||||
|
||||
@Get('')
|
||||
@SessionApiParam
|
||||
@ApiOperation({ summary: 'Get my profile' })
|
||||
async getMyProfile(
|
||||
@WorkingSessionParam session: WhatsappSession,
|
||||
): Promise<MyProfile> {
|
||||
const me = session.getSessionMeInfo();
|
||||
if (!me) {
|
||||
throw new UnprocessableEntityException('No profile found');
|
||||
}
|
||||
const picture = await session.getContactProfilePicture(me.id, false);
|
||||
return {
|
||||
id: me.id,
|
||||
name: me.pushName,
|
||||
picture: picture || null,
|
||||
};
|
||||
}
|
||||
|
||||
@Put('/name')
|
||||
@SessionApiParam
|
||||
@UsePipes(new WAHAValidationPipe())
|
||||
@ApiOperation({ summary: 'Set my profile name' })
|
||||
async setProfileName(
|
||||
@WorkingSessionParam session: WhatsappSession,
|
||||
@Body() request: ProfileNameRequest,
|
||||
): Promise<Result> {
|
||||
const success = await session.setProfileName(request.name);
|
||||
return { success: success ?? true };
|
||||
}
|
||||
|
||||
@Put('/status')
|
||||
@SessionApiParam
|
||||
@UsePipes(new WAHAValidationPipe())
|
||||
@ApiOperation({ summary: 'Set profile status (About)' })
|
||||
async setProfileStatus(
|
||||
@WorkingSessionParam session: WhatsappSession,
|
||||
@Body() request: ProfileStatusRequest,
|
||||
): Promise<Result> {
|
||||
const success = await session.setProfileStatus(request.status);
|
||||
return { success: success ?? true };
|
||||
}
|
||||
|
||||
@Put('/picture')
|
||||
@SessionApiParam
|
||||
@ApiOperation({ summary: 'Set profile picture' })
|
||||
async setProfilePicture(
|
||||
@WorkingSessionParam session: WhatsappSession,
|
||||
@Body() request: ProfilePictureRequest,
|
||||
): Promise<Result> {
|
||||
const success = await session.updateProfilePicture(request.file);
|
||||
return { success: success ?? true };
|
||||
}
|
||||
|
||||
@Delete('/picture')
|
||||
@SessionApiParam
|
||||
@ApiOperation({ summary: 'Delete profile picture' })
|
||||
async deleteProfilePicture(
|
||||
@WorkingSessionParam session: WhatsappSession,
|
||||
): Promise<Result> {
|
||||
const success = await session.updateProfilePicture(null);
|
||||
return { success: success || true };
|
||||
}
|
||||
}
|
||||
@@ -233,6 +233,9 @@ class SessionsController {
|
||||
@Body() request: SessionStartDeprecatedRequest,
|
||||
): Promise<SessionDTO> {
|
||||
const name = request.name;
|
||||
if (!request.name) {
|
||||
throw new UnprocessableEntityException('Session name is required');
|
||||
}
|
||||
if (this.manager.isRunning(name)) {
|
||||
const msg = `Session '${name}' is already started.`;
|
||||
throw new UnprocessableEntityException(msg);
|
||||
@@ -255,6 +258,9 @@ class SessionsController {
|
||||
async DEPRECATED_stop(
|
||||
@Body() request: SessionStopDeprecatedRequest,
|
||||
): Promise<void> {
|
||||
if (!request.name) {
|
||||
throw new UnprocessableEntityException('Session name is required');
|
||||
}
|
||||
const name = request.name;
|
||||
if (request.logout) {
|
||||
// Old API did remove the session complete
|
||||
@@ -283,6 +289,9 @@ class SessionsController {
|
||||
async DEPRECATED_logout(
|
||||
@Body() request: SessionLogoutDeprecatedRequest,
|
||||
): Promise<void> {
|
||||
if (!request.name) {
|
||||
throw new UnprocessableEntityException('Session name is required');
|
||||
}
|
||||
const name = request.name;
|
||||
await this.withLock(name, async () => {
|
||||
await this.manager.unassign(name);
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { Body, Controller, Post } from '@nestjs/common';
|
||||
import { Body, Controller, Get, Post } from '@nestjs/common';
|
||||
import { ApiOperation, ApiSecurity, ApiTags } from '@nestjs/swagger';
|
||||
import {
|
||||
SessionApiParam,
|
||||
WorkingSessionParam,
|
||||
} from '@waha/nestjs/params/SessionApiParam';
|
||||
import { NewMessageIDResponse } from '@waha/structures/chatting.dto';
|
||||
|
||||
import { SessionManager } from '../core/abc/manager.abc';
|
||||
import { WhatsappSession } from '../core/abc/session.abc';
|
||||
@@ -70,6 +71,18 @@ class StatusController {
|
||||
) {
|
||||
return session.deleteStatus(status);
|
||||
}
|
||||
|
||||
@Get('new-message-id')
|
||||
@SessionApiParam
|
||||
@ApiOperation({
|
||||
summary: 'Generate message ID you can use to batch contacts',
|
||||
})
|
||||
async getNewMessageId(
|
||||
@WorkingSessionParam session: WhatsappSession,
|
||||
): Promise<NewMessageIDResponse> {
|
||||
const id = await session.generateNewMessageId();
|
||||
return { id: id };
|
||||
}
|
||||
}
|
||||
|
||||
export { StatusController };
|
||||
+25
-19
@@ -1,13 +1,19 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { Injectable, Logger, OnApplicationBootstrap } from '@nestjs/common';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { WAHAEvents } from '@waha/structures/enums.dto';
|
||||
import { GlobalWebhookConfigConfig } from '@waha/core/config/GlobalWebhookConfig';
|
||||
|
||||
import { parseBool } from './helpers';
|
||||
import { WebhookConfig } from './structures/webhooks.config.dto';
|
||||
|
||||
@Injectable()
|
||||
export class WhatsappConfigService {
|
||||
constructor(private configService: ConfigService) {}
|
||||
export class WhatsappConfigService implements OnApplicationBootstrap {
|
||||
private logger: Logger;
|
||||
private webhookConfig: GlobalWebhookConfigConfig;
|
||||
|
||||
constructor(private configService: ConfigService) {
|
||||
this.logger = new Logger('WhatsappConfigService');
|
||||
this.webhookConfig = new GlobalWebhookConfigConfig(configService);
|
||||
}
|
||||
|
||||
get schema() {
|
||||
return this.configService.get('WHATSAPP_API_SCHEMA', 'http');
|
||||
@@ -113,27 +119,20 @@ export class WhatsappConfigService {
|
||||
}
|
||||
|
||||
getWebhookConfig(): WebhookConfig | undefined {
|
||||
const url = this.getWebhookUrl();
|
||||
const events = this.getWebhookEvents();
|
||||
if (!url || events.length === 0) {
|
||||
return undefined;
|
||||
}
|
||||
return { url: url, events: events };
|
||||
}
|
||||
|
||||
private getWebhookUrl(): string | undefined {
|
||||
return this.get('WHATSAPP_HOOK_URL');
|
||||
}
|
||||
|
||||
private getWebhookEvents(): WAHAEvents[] {
|
||||
const value = this.get('WHATSAPP_HOOK_EVENTS', '');
|
||||
return value ? value.split(',') : [];
|
||||
return this.webhookConfig.config;
|
||||
}
|
||||
|
||||
getSessionMongoUrl(): string | undefined {
|
||||
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);
|
||||
}
|
||||
@@ -175,4 +174,11 @@ export class WhatsappConfigService {
|
||||
const value = this.configService.get('WAHA_DEBUG_MODE', 'false');
|
||||
return parseBool(value);
|
||||
}
|
||||
|
||||
onApplicationBootstrap() {
|
||||
const error = this.webhookConfig.validateConfig();
|
||||
if (error) {
|
||||
throw new Error(`Invalid global webhook config:\n${error}\n`);
|
||||
}
|
||||
}
|
||||
}
|
||||
+8
-5
@@ -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');
|
||||
}
|
||||
}
|
||||
@@ -57,13 +57,11 @@ export class SwaggerConfiguratorCore {
|
||||
.setVersion(VERSION.version)
|
||||
.addTag('🖥️ Sessions', 'Control WhatsApp sessions (accounts)')
|
||||
.addTag('🔑 Auth', 'Authentication')
|
||||
.addTag('🆔 Profile', 'Your profile information')
|
||||
.addTag('🖼️ Screenshot', 'Get screenshot of WhatsApp and show QR code')
|
||||
.addTag('📤 Chatting', 'Chatting methods')
|
||||
.addTag('📢 Channels', 'Channels (newsletters) methods')
|
||||
.addTag(
|
||||
'🟢 Status',
|
||||
'Status (aka stories) methods. <b>NOWEB</b> engine only!',
|
||||
)
|
||||
.addTag('🟢 Status', 'Status (aka stories) methods')
|
||||
.addTag('💬 Chats', `Chats methods`)
|
||||
.addTag(
|
||||
'👤 Contacts',
|
||||
@@ -73,6 +71,7 @@ export class SwaggerConfiguratorCore {
|
||||
)
|
||||
.addTag('👥 Groups', `Groups methods.<br>`)
|
||||
.addTag('✅ Presence', `Presence information`)
|
||||
.addTag('📅 Events', `Event Message`)
|
||||
.addTag(
|
||||
'🏷️ Labels',
|
||||
'Labels - available only for WhatsApp Business accounts',
|
||||
@@ -140,6 +139,7 @@ export class SwaggerConfiguratorCore {
|
||||
webhooks[event] = {
|
||||
post: {
|
||||
summary: eventMetadata.description,
|
||||
deprecated: eventMetadata.deprecated || false,
|
||||
requestBody: {
|
||||
content: {
|
||||
'application/json': {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
@@ -37,14 +46,20 @@ export abstract class SessionManager implements BeforeApplicationShutdown {
|
||||
protected sessionWorkerRepository: ISessionWorkerRepository;
|
||||
private lock: any;
|
||||
|
||||
WAIT_SESSION_RUNNING_INTERVAL = 500;
|
||||
WAIT_SESSION_RUNNING_TIMEOUT = 5_000;
|
||||
WAIT_STATUS_INTERVAL = 500;
|
||||
WAIT_STATUS_TIMEOUT = 5_000;
|
||||
WAIT_STATUS_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,
|
||||
maxExecutionTime: 30_000,
|
||||
});
|
||||
this.log.setContext(SessionManager.name);
|
||||
}
|
||||
|
||||
@@ -134,6 +149,37 @@ export abstract class SessionManager implements BeforeApplicationShutdown {
|
||||
sessionName: string,
|
||||
expected: WAHASessionStatus[],
|
||||
): Promise<WhatsappSession> {
|
||||
if (!sessionName) {
|
||||
throw new UnprocessableEntityException({
|
||||
error: `Session name is required`,
|
||||
session: sessionName,
|
||||
});
|
||||
}
|
||||
const running = await waitUntil(
|
||||
async () => this.isRunning(sessionName),
|
||||
this.WAIT_SESSION_RUNNING_INTERVAL,
|
||||
this.WAIT_SESSION_RUNNING_TIMEOUT,
|
||||
);
|
||||
|
||||
if (!running) {
|
||||
const exists = await this.exists(sessionName);
|
||||
if (!exists) {
|
||||
throw new UnprocessableEntityException({
|
||||
error: `Session "${sessionName}" does not exist`,
|
||||
session: sessionName,
|
||||
});
|
||||
}
|
||||
|
||||
const msg = {
|
||||
error:
|
||||
'Session status is not as expected. Try again later or restart the session',
|
||||
session: sessionName,
|
||||
status: 'STOPPED',
|
||||
expected: expected,
|
||||
};
|
||||
throw new UnprocessableEntityException(msg);
|
||||
}
|
||||
|
||||
const session = this.getSession(sessionName);
|
||||
const valid = await waitUntil(
|
||||
async () => expected.includes(session.status),
|
||||
@@ -156,18 +202,35 @@ 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,
|
||||
) {
|
||||
return (payload: any): WAHAWebhook => {
|
||||
const id = payload._eventId;
|
||||
const timestampMs = payload._timestampMs;
|
||||
const data = { ...payload };
|
||||
delete data._eventId;
|
||||
delete data._timestampMs;
|
||||
const me = session.getSessionMeInfo();
|
||||
return {
|
||||
id: id,
|
||||
timestamp: timestampMs,
|
||||
event: event,
|
||||
session: session.name,
|
||||
metadata: session.sessionConfig?.metadata,
|
||||
|
||||
+338
-22
@@ -1,17 +1,34 @@
|
||||
import { isJidBroadcast } from '@adiwajshing/baileys/lib/WABinary/jid-utils';
|
||||
import { MessagesForRead } from '@waha/core/utils/convertors';
|
||||
import { isJidNewsletter } from '@waha/core/utils/jids';
|
||||
import {
|
||||
Channel,
|
||||
ChannelListResult,
|
||||
ChannelMessage,
|
||||
ChannelSearchByText,
|
||||
ChannelSearchByView,
|
||||
CreateChannelRequest,
|
||||
ListChannelsQuery,
|
||||
PreviewChannelMessages,
|
||||
} from '@waha/structures/channels.dto';
|
||||
import {
|
||||
ChatSummary,
|
||||
GetChatMessageQuery,
|
||||
GetChatMessagesFilter,
|
||||
GetChatMessagesQuery,
|
||||
OverviewFilter,
|
||||
ReadChatMessagesQuery,
|
||||
ReadChatMessagesResponse,
|
||||
} from '@waha/structures/chats.dto';
|
||||
import { SendButtonsRequest } from '@waha/structures/chatting.buttons.dto';
|
||||
import { BinaryFile, RemoteFile } from '@waha/structures/files.dto';
|
||||
import { Label, LabelDTO, LabelID } from '@waha/structures/labels.dto';
|
||||
import { PaginationParams } from '@waha/structures/pagination.dto';
|
||||
import { WAMessage } from '@waha/structures/responses.dto';
|
||||
import { LidToPhoneNumber } from '@waha/structures/lids.dto';
|
||||
import {
|
||||
LimitOffsetParams,
|
||||
PaginationParams,
|
||||
} from '@waha/structures/pagination.dto';
|
||||
import { MessageSource, WAMessage } from '@waha/structures/responses.dto';
|
||||
import { DefaultMap } from '@waha/utils/DefaultMap';
|
||||
import { generatePrefixedId } from '@waha/utils/ids';
|
||||
import { LoggerBuilder } from '@waha/utils/logging';
|
||||
@@ -19,6 +36,7 @@ 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 * as NodeCache from 'node-cache';
|
||||
import { Logger } from 'pino';
|
||||
import {
|
||||
BehaviorSubject,
|
||||
@@ -38,10 +56,12 @@ import {
|
||||
ChatRequest,
|
||||
CheckNumberStatusQuery,
|
||||
EditMessageRequest,
|
||||
MessageButtonReply,
|
||||
MessageContactVcardRequest,
|
||||
MessageFileRequest,
|
||||
MessageForwardRequest,
|
||||
MessageImageRequest,
|
||||
MessageLinkCustomPreviewRequest,
|
||||
MessageLinkPreviewRequest,
|
||||
MessageLocationRequest,
|
||||
MessagePollRequest,
|
||||
@@ -51,6 +71,7 @@ import {
|
||||
MessageTextRequest,
|
||||
MessageVideoRequest,
|
||||
MessageVoiceRequest,
|
||||
SendSeenRequest,
|
||||
} from '../../structures/chatting.dto';
|
||||
import { ContactQuery, ContactRequest } from '../../structures/contacts.dto';
|
||||
import {
|
||||
@@ -59,8 +80,14 @@ import {
|
||||
WAHAPresenceStatus,
|
||||
WAHASessionStatus,
|
||||
} from '../../structures/enums.dto';
|
||||
import {
|
||||
EventCancelRequest,
|
||||
EventMessageRequest,
|
||||
} from '../../structures/events.dto';
|
||||
import {
|
||||
CreateGroupRequest,
|
||||
GroupField,
|
||||
GroupsListFields,
|
||||
ParticipantsRequest,
|
||||
SettingsSecurityChangeInfo,
|
||||
} from '../../structures/groups.dto';
|
||||
@@ -131,6 +158,15 @@ 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
|
||||
});
|
||||
|
||||
// Save sent messages ids in cache so we can determine if a message was sent
|
||||
// via API or APP
|
||||
private sentMessageIds: NodeCache = new NodeCache({
|
||||
stdTTL: 10 * 60, // 10 minutes
|
||||
});
|
||||
|
||||
public constructor({
|
||||
name,
|
||||
@@ -162,6 +198,7 @@ export abstract class WhatsappSession {
|
||||
filter(Boolean),
|
||||
map((data) => {
|
||||
data._eventId = generatePrefixedId('evt');
|
||||
data._timestampMs = Date.now();
|
||||
return data;
|
||||
}),
|
||||
retry(),
|
||||
@@ -234,12 +271,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',
|
||||
@@ -249,13 +287,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',
|
||||
@@ -264,7 +302,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',
|
||||
@@ -273,7 +312,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',
|
||||
@@ -316,27 +355,68 @@ export abstract class WhatsappSession {
|
||||
throw new NotImplementedByEngineError();
|
||||
}
|
||||
|
||||
public authorizeCode(code: string) {
|
||||
throw new NotImplementedByEngineError();
|
||||
}
|
||||
|
||||
abstract getScreenshot(): Promise<Buffer>;
|
||||
|
||||
public getSessionMeInfo(): MeInfo | null {
|
||||
return null;
|
||||
}
|
||||
|
||||
public getCaptcha(): Promise<QR> {
|
||||
/**
|
||||
* Profile methods
|
||||
*/
|
||||
public setProfileName(name: string): Promise<boolean> {
|
||||
throw new NotImplementedByEngineError();
|
||||
}
|
||||
|
||||
public saveCaptcha(code: string) {
|
||||
public setProfileStatus(status: string): Promise<boolean> {
|
||||
throw new NotImplementedByEngineError();
|
||||
}
|
||||
|
||||
public async updateProfilePicture(
|
||||
file: BinaryFile | RemoteFile | null,
|
||||
): Promise<boolean> {
|
||||
if (file) {
|
||||
await this.setProfilePicture(file);
|
||||
} else {
|
||||
await this.deleteProfilePicture();
|
||||
}
|
||||
|
||||
// Refresh profile picture after update
|
||||
setTimeout(() => {
|
||||
this.logger.debug('Refreshing my profile picture after update...');
|
||||
this.refreshMyProfilePicture()
|
||||
.then(() => {
|
||||
this.logger.debug('Refreshed my profile picture after update');
|
||||
})
|
||||
.catch((err) => {
|
||||
this.logger.error('Error refreshing my profile picture after update');
|
||||
this.logger.error(err, err.stack);
|
||||
});
|
||||
}, 3_000);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
protected async refreshMyProfilePicture() {
|
||||
const me = this.getSessionMeInfo();
|
||||
await this.getContactProfilePicture(me.id, true);
|
||||
}
|
||||
|
||||
protected setProfilePicture(file: BinaryFile | RemoteFile): Promise<boolean> {
|
||||
throw new NotImplementedByEngineError();
|
||||
}
|
||||
|
||||
protected deleteProfilePicture(): Promise<boolean> {
|
||||
throw new NotImplementedByEngineError();
|
||||
}
|
||||
|
||||
/**
|
||||
* Other methods
|
||||
*/
|
||||
generateNewMessageId(): Promise<string> {
|
||||
throw new NotImplementedByEngineError();
|
||||
}
|
||||
|
||||
abstract checkNumberStatus(request: CheckNumberStatusQuery);
|
||||
|
||||
abstract sendText(request: MessageTextRequest);
|
||||
@@ -355,6 +435,12 @@ export abstract class WhatsappSession {
|
||||
throw new NotImplementedByEngineError();
|
||||
}
|
||||
|
||||
sendLinkCustomPreview(
|
||||
request: MessageLinkCustomPreviewRequest,
|
||||
): Promise<any> {
|
||||
throw new NotImplementedByEngineError();
|
||||
}
|
||||
|
||||
abstract forwardMessage(request: MessageForwardRequest): Promise<WAMessage>;
|
||||
|
||||
abstract sendImage(request: MessageImageRequest);
|
||||
@@ -371,9 +457,13 @@ export abstract class WhatsappSession {
|
||||
throw new NotImplementedByEngineError();
|
||||
}
|
||||
|
||||
sendButtonsReply(request: MessageButtonReply) {
|
||||
throw new NotImplementedByEngineError();
|
||||
}
|
||||
|
||||
abstract reply(request: MessageReplyRequest);
|
||||
|
||||
abstract sendSeen(chat: ChatRequest);
|
||||
abstract sendSeen(chat: SendSeenRequest);
|
||||
|
||||
abstract startTyping(chat: ChatRequest);
|
||||
|
||||
@@ -385,6 +475,14 @@ export abstract class WhatsappSession {
|
||||
throw new NotImplementedByEngineError();
|
||||
}
|
||||
|
||||
sendEvent(request: EventMessageRequest): Promise<WAMessage> {
|
||||
throw new NotImplementedByEngineError();
|
||||
}
|
||||
|
||||
cancelEvent(eventId: string): Promise<WAMessage> {
|
||||
throw new NotImplementedByEngineError();
|
||||
}
|
||||
|
||||
/**
|
||||
* Chats methods
|
||||
*/
|
||||
@@ -392,6 +490,13 @@ export abstract class WhatsappSession {
|
||||
throw new NotImplementedByEngineError();
|
||||
}
|
||||
|
||||
public getChatsOverview(
|
||||
pagination: PaginationParams,
|
||||
filter?: OverviewFilter,
|
||||
): Promise<ChatSummary[]> {
|
||||
throw new NotImplementedByEngineError();
|
||||
}
|
||||
|
||||
public deleteChat(chatId) {
|
||||
throw new NotImplementedByEngineError();
|
||||
}
|
||||
@@ -400,10 +505,35 @@ export abstract class WhatsappSession {
|
||||
chatId: string,
|
||||
query: GetChatMessagesQuery,
|
||||
filter: GetChatMessagesFilter,
|
||||
) {
|
||||
): Promise<WAMessage[]> {
|
||||
throw new NotImplementedByEngineError();
|
||||
}
|
||||
|
||||
abstract readChatMessages(
|
||||
chatId: string,
|
||||
request: ReadChatMessagesQuery,
|
||||
): Promise<ReadChatMessagesResponse>;
|
||||
|
||||
protected async readChatMessagesWSImpl(
|
||||
chatId: string,
|
||||
request: ReadChatMessagesQuery,
|
||||
): Promise<ReadChatMessagesResponse> {
|
||||
const { query, filter } = MessagesForRead(chatId, request);
|
||||
const messages = await this.getChatMessages(chatId, query, filter);
|
||||
this.logger.debug(`Found ${messages.length} messages to read`);
|
||||
if (messages.length === 0) {
|
||||
return { ids: [] };
|
||||
}
|
||||
const ids = messages.map((m) => m.id);
|
||||
const seen: SendSeenRequest = {
|
||||
chatId: chatId,
|
||||
messageIds: ids,
|
||||
session: '',
|
||||
};
|
||||
await this.sendSeen(seen);
|
||||
return { ids: ids };
|
||||
}
|
||||
|
||||
public getChatMessage(
|
||||
chatId: string,
|
||||
messageId: string,
|
||||
@@ -499,14 +629,71 @@ export abstract class WhatsappSession {
|
||||
throw new NotImplementedByEngineError();
|
||||
}
|
||||
|
||||
public getContactAbout(query: ContactQuery) {
|
||||
public getContactAbout(query: ContactQuery): Promise<{ about: string }> {
|
||||
throw new NotImplementedByEngineError();
|
||||
}
|
||||
|
||||
public getContactProfilePicture(query: ContactQuery) {
|
||||
/**
|
||||
* Lid to Phone Number methods
|
||||
*/
|
||||
public async getAllLids(
|
||||
pagination: PaginationParams,
|
||||
): Promise<Array<LidToPhoneNumber>> {
|
||||
throw new NotImplementedByEngineError();
|
||||
}
|
||||
|
||||
public async getLidsCount(): Promise<number> {
|
||||
throw new NotImplementedByEngineError();
|
||||
}
|
||||
|
||||
public async findPNByLid(lid: string): Promise<LidToPhoneNumber> {
|
||||
throw new NotImplementedByEngineError();
|
||||
}
|
||||
|
||||
public async findLIDByPhoneNumber(
|
||||
phoneNumber: string,
|
||||
): Promise<LidToPhoneNumber> {
|
||||
throw new NotImplementedByEngineError();
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch the latest profile picture of the contact (group, newsletter, etc.)
|
||||
* @param id
|
||||
*/
|
||||
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) {
|
||||
let fn: Promise<string>;
|
||||
if (isJidBroadcast(id)) {
|
||||
return null;
|
||||
} else if (isJidNewsletter(id)) {
|
||||
fn = this.channelsGetChannel(id).then(
|
||||
(channel: Channel) => channel.picture || channel.preview,
|
||||
);
|
||||
} else {
|
||||
fn = this.fetchContactProfilePicture(id);
|
||||
}
|
||||
this.profilePictures.del(id);
|
||||
const url = await fn.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) {
|
||||
throw new NotImplementedByEngineError();
|
||||
}
|
||||
@@ -522,7 +709,31 @@ export abstract class WhatsappSession {
|
||||
throw new NotImplementedByEngineError();
|
||||
}
|
||||
|
||||
public getGroups() {
|
||||
public joinGroup(code: string): Promise<string> {
|
||||
throw new NotImplementedByEngineError();
|
||||
}
|
||||
|
||||
public joinInfoGroup(code: string): Promise<any> {
|
||||
throw new NotImplementedByEngineError();
|
||||
}
|
||||
|
||||
public getGroups(pagination: PaginationParams): Promise<any> {
|
||||
throw new NotImplementedByEngineError();
|
||||
}
|
||||
|
||||
public filterGroupsFields(data: any, fields: GroupsListFields) {
|
||||
const groups: any[] = Array.isArray(data) ? data : Object.values(data);
|
||||
if (fields.exclude?.includes(GroupField.PARTICIPANTS)) {
|
||||
groups.forEach((group) => this.removeGroupsFieldParticipant(group));
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
protected removeGroupsFieldParticipant(group: any) {
|
||||
return;
|
||||
}
|
||||
|
||||
public refreshGroups(): Promise<boolean> {
|
||||
throw new NotImplementedByEngineError();
|
||||
}
|
||||
|
||||
@@ -558,6 +769,43 @@ export abstract class WhatsappSession {
|
||||
throw new NotImplementedByEngineError();
|
||||
}
|
||||
|
||||
public async updateGroupPicture(
|
||||
id: string,
|
||||
file: BinaryFile | RemoteFile | null,
|
||||
): Promise<boolean> {
|
||||
if (file) {
|
||||
await this.setGroupPicture(id, file);
|
||||
} else {
|
||||
await this.deleteGroupPicture(id);
|
||||
}
|
||||
|
||||
// Refresh picture after update
|
||||
setTimeout(() => {
|
||||
this.logger.debug('Refreshing group profile picture after update...');
|
||||
this.refreshProfilePicture(id)
|
||||
.then(() => {
|
||||
this.logger.debug('Refreshed group profile picture after update');
|
||||
})
|
||||
.catch((err) => {
|
||||
this.logger.error('Error refreshing my profile picture after update');
|
||||
this.logger.error(err, err.stack);
|
||||
});
|
||||
}, 3_000);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
protected setGroupPicture(
|
||||
id: string,
|
||||
file: BinaryFile | RemoteFile,
|
||||
): Promise<boolean> {
|
||||
throw new NotImplementedByEngineError();
|
||||
}
|
||||
|
||||
protected deleteGroupPicture(id: string): Promise<boolean> {
|
||||
throw new NotImplementedByEngineError();
|
||||
}
|
||||
|
||||
public setSubject(id, description) {
|
||||
throw new NotImplementedByEngineError();
|
||||
}
|
||||
@@ -602,13 +850,32 @@ export abstract class WhatsappSession {
|
||||
throw new NotImplementedByEngineError();
|
||||
}
|
||||
|
||||
public subscribePresence(id: string): Promise<void> {
|
||||
public subscribePresence(id: string): Promise<any> {
|
||||
throw new NotImplementedByEngineError();
|
||||
}
|
||||
|
||||
/**
|
||||
* Channels methods
|
||||
*/
|
||||
public searchChannelsByView(
|
||||
query: ChannelSearchByView,
|
||||
): Promise<ChannelListResult> {
|
||||
throw new NotImplementedByEngineError();
|
||||
}
|
||||
|
||||
public searchChannelsByText(
|
||||
query: ChannelSearchByText,
|
||||
): Promise<ChannelListResult> {
|
||||
throw new NotImplementedByEngineError();
|
||||
}
|
||||
|
||||
public async previewChannelMessages(
|
||||
inviteCode: string,
|
||||
query: PreviewChannelMessages,
|
||||
): Promise<ChannelMessage[]> {
|
||||
throw new NotImplementedByEngineError();
|
||||
}
|
||||
|
||||
public channelsList(query: ListChannelsQuery): Promise<Channel[]> {
|
||||
throw new NotImplementedByEngineError();
|
||||
}
|
||||
@@ -670,6 +937,9 @@ export abstract class WhatsappSession {
|
||||
throw new NotImplementedByEngineError();
|
||||
}
|
||||
|
||||
/**
|
||||
* Engine methods
|
||||
*/
|
||||
public async getEngineInfo(): Promise<any> {
|
||||
return {};
|
||||
}
|
||||
@@ -711,12 +981,58 @@ export abstract class WhatsappSession {
|
||||
);
|
||||
qrcode.generate(qr.raw, { small: true });
|
||||
}
|
||||
|
||||
protected saveSentMessageId(id: string) {
|
||||
this.sentMessageIds.set(id, true);
|
||||
}
|
||||
|
||||
protected getMessageSource(id: string): MessageSource {
|
||||
if (!id) {
|
||||
return MessageSource.APP;
|
||||
}
|
||||
const api = this.sentMessageIds.has(id);
|
||||
return api ? MessageSource.API : MessageSource.APP;
|
||||
}
|
||||
}
|
||||
|
||||
export function isNewsletter(jid: string) {
|
||||
return jid.endsWith('@newsletter');
|
||||
export function getGroupInviteLink(code: string) {
|
||||
if (code.startsWith('https://')) {
|
||||
return code;
|
||||
}
|
||||
return `https://chat.whatsapp.com/${code}`;
|
||||
}
|
||||
|
||||
export function parseGroupInviteLink(link: string) {
|
||||
// https://chat.whatsapp.com/123 => 123
|
||||
return link.split('/').pop();
|
||||
}
|
||||
|
||||
export function getChannelInviteLink(code: string) {
|
||||
return `https://whatsapp.com/channel/${code}`;
|
||||
}
|
||||
|
||||
export function parseChannelInviteLink(link: string): string {
|
||||
// https://www.whatsapp.com/channel/123 => 123
|
||||
const code = link.split('/').pop();
|
||||
return code;
|
||||
}
|
||||
|
||||
export function getPublicUrlFromDirectPath(directPath: string) {
|
||||
return `https://pps.whatsapp.net${directPath}`;
|
||||
}
|
||||
|
||||
const deviceRegexp = /^.*:(\d+)@.*$/;
|
||||
|
||||
/**
|
||||
* Extracts the device ID from a JID string.
|
||||
*
|
||||
* @param jid - The JID string (e.g., "123123:12@c.us")
|
||||
* @return The extracted device ID (e.g., "12") or null if the format is invalid.
|
||||
*/
|
||||
export function extractDeviceId(jid: string): string | null {
|
||||
if (!jid) {
|
||||
return null;
|
||||
}
|
||||
const match = jid.match(deviceRegexp);
|
||||
return match ? match[1] : null;
|
||||
}
|
||||
@@ -32,7 +32,7 @@ export class WebsocketGatewayCore
|
||||
OnGatewayDisconnect,
|
||||
BeforeApplicationShutdown
|
||||
{
|
||||
HEARTBEAT_INTERVAL = 10_000;
|
||||
HEARTBEAT_INTERVAL = 60_000;
|
||||
|
||||
@WebSocketServer()
|
||||
server: Server;
|
||||
@@ -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,14 +5,18 @@ import { PassportModule } from '@nestjs/passport';
|
||||
import { ServeStaticModule } from '@nestjs/serve-static';
|
||||
import { TerminusModule } from '@nestjs/terminus';
|
||||
import { ChannelsController } from '@waha/api/channels.controller';
|
||||
import { LidsController } from '@waha/api/lids.controller';
|
||||
import { ProfileController } from '@waha/api/profile.controller';
|
||||
import {
|
||||
ServerController,
|
||||
ServerDebugController,
|
||||
} from '@waha/api/server.controller';
|
||||
import { WebsocketGatewayCore } from '@waha/core/api/websocket.gateway.core';
|
||||
import { GowsEngineConfigService } from '@waha/core/config/GowsEngineConfigService';
|
||||
import { WebJSEngineConfigService } from '@waha/core/config/WebJSEngineConfigService';
|
||||
import { MediaLocalStorageModule } from '@waha/core/media/local/media.local.storage.module';
|
||||
import { MediaLocalStorageConfig } from '@waha/core/media/local/MediaLocalStorageConfig';
|
||||
import { ChannelsInfoServiceCore } from '@waha/core/services/ChannelsInfoServiceCore';
|
||||
import { BufferJsonReplacerInterceptor } from '@waha/nestjs/BufferJsonReplacerInterceptor';
|
||||
import {
|
||||
getPinoHttpUseLevel,
|
||||
@@ -28,6 +32,7 @@ import { AuthController } from '../api/auth.controller';
|
||||
import { ChatsController } from '../api/chats.controller';
|
||||
import { ChattingController } from '../api/chatting.controller';
|
||||
import { ContactsController } from '../api/contacts.controller';
|
||||
import { EventsController } from '../api/events.controller';
|
||||
import { GroupsController } from '../api/groups.controller';
|
||||
import { HealthController } from '../api/health.controller';
|
||||
import { LabelsController } from '../api/labels.controller';
|
||||
@@ -106,7 +111,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,
|
||||
@@ -117,15 +124,18 @@ const IMPORTS = [...IMPORTS_CORE, ...IMPORTS_MEDIA];
|
||||
export const CONTROLLERS = [
|
||||
AuthController,
|
||||
SessionsController,
|
||||
ProfileController,
|
||||
ChattingController,
|
||||
ChatsController,
|
||||
ChannelsController,
|
||||
StatusController,
|
||||
LabelsController,
|
||||
ContactsController,
|
||||
LidsController,
|
||||
GroupsController,
|
||||
PresenceController,
|
||||
ScreenshotController,
|
||||
EventsController,
|
||||
PingController,
|
||||
HealthController,
|
||||
ServerController,
|
||||
@@ -145,9 +155,11 @@ const PROVIDERS = [
|
||||
provide: APP_INTERCEPTOR,
|
||||
useClass: BufferJsonReplacerInterceptor,
|
||||
},
|
||||
ChannelsInfoServiceCore,
|
||||
DashboardConfigServiceCore,
|
||||
SwaggerConfigServiceCore,
|
||||
WebJSEngineConfigService,
|
||||
GowsEngineConfigService,
|
||||
WhatsappConfigService,
|
||||
EngineConfigService,
|
||||
WebsocketGatewayCore,
|
||||
|
||||
@@ -0,0 +1,167 @@
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { WAHAEvents } from '@waha/structures/enums.dto';
|
||||
import {
|
||||
CustomHeader,
|
||||
HmacConfiguration,
|
||||
RetriesConfiguration,
|
||||
WebhookConfig,
|
||||
} from '@waha/structures/webhooks.config.dto';
|
||||
import { plainToInstance } from 'class-transformer';
|
||||
import { validateSync, ValidationError } from 'class-validator';
|
||||
|
||||
enum Env {
|
||||
WHATSAPP_HOOK_URL = 'WHATSAPP_HOOK_URL',
|
||||
WHATSAPP_HOOK_EVENTS = 'WHATSAPP_HOOK_EVENTS',
|
||||
WHATSAPP_HOOK_RETRIES_POLICY = 'WHATSAPP_HOOK_RETRIES_POLICY',
|
||||
WHATSAPP_HOOK_RETRIES_DELAY_SECONDS = 'WHATSAPP_HOOK_RETRIES_DELAY_SECONDS',
|
||||
WHATSAPP_HOOK_RETRIES_ATTEMPTS = 'WHATSAPP_HOOK_RETRIES_ATTEMPTS',
|
||||
WHATSAPP_HOOK_HMAC_KEY = 'WHATSAPP_HOOK_HMAC_KEY',
|
||||
WHATSAPP_HOOK_CUSTOM_HEADERS = 'WHATSAPP_HOOK_CUSTOM_HEADERS',
|
||||
}
|
||||
|
||||
export class GlobalWebhookConfigConfig {
|
||||
protected _config: WebhookConfig | null = null;
|
||||
|
||||
constructor(private configService: ConfigService) {}
|
||||
|
||||
private getUrl(): string | undefined {
|
||||
return this.configService.get(Env.WHATSAPP_HOOK_URL);
|
||||
}
|
||||
|
||||
private getEvents(): WAHAEvents[] {
|
||||
const value = this.configService.get(Env.WHATSAPP_HOOK_EVENTS, '');
|
||||
return value ? value.split(',') : [];
|
||||
}
|
||||
|
||||
private getHmac(): HmacConfiguration | null {
|
||||
const key = this.configService.get(Env.WHATSAPP_HOOK_HMAC_KEY, '');
|
||||
if (!key) {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
key: key,
|
||||
};
|
||||
}
|
||||
|
||||
private getRetries(): RetriesConfiguration | null {
|
||||
const policy: any = this.configService.get(
|
||||
Env.WHATSAPP_HOOK_RETRIES_POLICY,
|
||||
null,
|
||||
);
|
||||
const delaySeconds: any = this.configService.get(
|
||||
Env.WHATSAPP_HOOK_RETRIES_DELAY_SECONDS,
|
||||
null,
|
||||
);
|
||||
const attempts: any = this.configService.get(
|
||||
Env.WHATSAPP_HOOK_RETRIES_ATTEMPTS,
|
||||
null,
|
||||
);
|
||||
if (!policy && !delaySeconds && !attempts) {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
policy: policy,
|
||||
delaySeconds: delaySeconds,
|
||||
attempts: attempts,
|
||||
};
|
||||
}
|
||||
|
||||
private getCustomHeaders(): CustomHeader[] {
|
||||
// key:value;key2:value
|
||||
const value = this.configService.get(Env.WHATSAPP_HOOK_CUSTOM_HEADERS, '');
|
||||
if (!value) {
|
||||
return [];
|
||||
}
|
||||
const headers = value.split(';');
|
||||
return headers.map((header: string) => {
|
||||
const parts = header.split(':');
|
||||
if (parts.length !== 2) {
|
||||
throw new Error(
|
||||
`${Env.WHATSAPP_HOOK_CUSTOM_HEADERS} - Invalid custom header, no ':' found in '${header}'`,
|
||||
);
|
||||
}
|
||||
return {
|
||||
name: parts[0],
|
||||
value: parts[1],
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
get config() {
|
||||
if (!this._config) {
|
||||
this._config = this.parseWebhookConfig();
|
||||
}
|
||||
return this._config;
|
||||
}
|
||||
|
||||
validateConfig(): string | null {
|
||||
const config = this.parseWebhookConfig();
|
||||
if (!config) {
|
||||
return null;
|
||||
}
|
||||
const errors = validateSync(config);
|
||||
if (errors.length > 0) {
|
||||
return this.formatErrors([], errors, []).join('\n');
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
private getEnv(key: string): string | null {
|
||||
const keys = {
|
||||
url: Env.WHATSAPP_HOOK_URL,
|
||||
events: Env.WHATSAPP_HOOK_EVENTS,
|
||||
'retries.policy': Env.WHATSAPP_HOOK_RETRIES_POLICY,
|
||||
'retries.delaySeconds': Env.WHATSAPP_HOOK_RETRIES_DELAY_SECONDS,
|
||||
'retries.attempts': Env.WHATSAPP_HOOK_RETRIES_ATTEMPTS,
|
||||
'hmac.key': Env.WHATSAPP_HOOK_HMAC_KEY,
|
||||
};
|
||||
return keys[key] || null;
|
||||
}
|
||||
|
||||
private formatErrors(
|
||||
lines: string[],
|
||||
errors: ValidationError[],
|
||||
path: string[],
|
||||
) {
|
||||
for (const err of errors) {
|
||||
path = [...path, err.property];
|
||||
for (const key in err.constraints) {
|
||||
if (!err.constraints.hasOwnProperty(key)) {
|
||||
continue;
|
||||
}
|
||||
const property = path.join('.');
|
||||
const env = this.getEnv(property);
|
||||
if (env) {
|
||||
const value = this.configService.get(env, '');
|
||||
const line = `- ${env} (${property}): '${value}' => '${err.value}' - ${err.constraints[key]}`;
|
||||
lines.push(line);
|
||||
} else {
|
||||
const line = `- ${property}: '${err.value}' - ${err.constraints[key]}`;
|
||||
lines.push(line);
|
||||
}
|
||||
}
|
||||
if (err.children) {
|
||||
this.formatErrors(lines, err.children, path);
|
||||
}
|
||||
}
|
||||
return lines;
|
||||
}
|
||||
|
||||
private parseWebhookConfig(): WebhookConfig | null {
|
||||
const url = this.getUrl();
|
||||
const events = this.getEvents();
|
||||
if (!url || events.length === 0) {
|
||||
return null;
|
||||
}
|
||||
const data: WebhookConfig = {
|
||||
url: url,
|
||||
events: events,
|
||||
hmac: this.getHmac(),
|
||||
retries: this.getRetries(),
|
||||
customHeaders: this.getCustomHeaders(),
|
||||
};
|
||||
return plainToInstance(WebhookConfig, data, {
|
||||
enableImplicitConversion: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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,3 @@
|
||||
export enum Jid {
|
||||
BROADCAST = 'status@broadcast',
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
export function isFromFullSync(event: any): boolean {
|
||||
return event.FromFullSync;
|
||||
}
|
||||
@@ -0,0 +1,191 @@
|
||||
import {
|
||||
GOWSGroupParticipant,
|
||||
GroupInfoEvent,
|
||||
GroupInfoFull,
|
||||
JoinedGroupEvent,
|
||||
} from '@waha/core/engines/gows/types.group';
|
||||
import { toJID } from '@waha/core/utils/jids';
|
||||
import {
|
||||
GroupId,
|
||||
GroupInfo,
|
||||
GroupParticipant,
|
||||
GroupParticipantRole,
|
||||
} from '@waha/structures/groups.dto';
|
||||
import {
|
||||
GroupParticipantType,
|
||||
GroupV2JoinEvent,
|
||||
GroupV2LeaveEvent,
|
||||
GroupV2ParticipantsEvent,
|
||||
GroupV2UpdateEvent,
|
||||
} from '@waha/structures/groups.events.dto';
|
||||
import { MeInfo } from '@waha/structures/sessions.dto';
|
||||
|
||||
export function ToGroupV2JoinEvent(event: JoinedGroupEvent): GroupV2JoinEvent {
|
||||
return {
|
||||
timestamp: Date.now(),
|
||||
group: ToGroupInfo(event),
|
||||
_data: event,
|
||||
};
|
||||
}
|
||||
|
||||
export function ToGroupV2LeaveEvent(
|
||||
me: MeInfo | null,
|
||||
event: GroupInfoEvent,
|
||||
): GroupV2LeaveEvent | null {
|
||||
if (!event.Leave || event.Leave.length === 0) {
|
||||
return null;
|
||||
}
|
||||
if (!me?.id) {
|
||||
return null;
|
||||
}
|
||||
const jid = toJID(me.id);
|
||||
const left = event.Leave;
|
||||
if (!left.includes(jid)) {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
timestamp: Date.now(),
|
||||
group: {
|
||||
id: event.JID,
|
||||
},
|
||||
_data: event,
|
||||
};
|
||||
}
|
||||
|
||||
export function ToGroupV2UpdateEvent(
|
||||
event: GroupInfoEvent,
|
||||
): GroupV2UpdateEvent | null {
|
||||
const group = ToGroupInfoPartial(event);
|
||||
if (!group) {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
timestamp: Date.now(),
|
||||
group: group,
|
||||
_data: event,
|
||||
};
|
||||
}
|
||||
|
||||
export function ToGroupV2ParticipantsEvents(
|
||||
event: GroupInfoEvent,
|
||||
): GroupV2ParticipantsEvent[] {
|
||||
const id: GroupId = {
|
||||
id: event.JID,
|
||||
};
|
||||
const events: GroupV2ParticipantsEvent[] = [];
|
||||
const maps = [
|
||||
{ participants: event.Leave, type: GroupParticipantType.LEAVE },
|
||||
{ participants: event.Join, type: GroupParticipantType.JOIN },
|
||||
{ participants: event.Promote, type: GroupParticipantType.PROMOTE },
|
||||
{ participants: event.Demote, type: GroupParticipantType.DEMOTE },
|
||||
];
|
||||
for (const map of maps) {
|
||||
const participants = getParticipants(map.participants, map.type);
|
||||
if (participants) {
|
||||
events.push({
|
||||
group: id,
|
||||
type: map.type,
|
||||
timestamp: Date.now(),
|
||||
participants: participants,
|
||||
_data: event,
|
||||
});
|
||||
}
|
||||
}
|
||||
return events;
|
||||
}
|
||||
|
||||
function getParticipants(jids: string[] | null, type: GroupParticipantType) {
|
||||
if (!jids) {
|
||||
return;
|
||||
}
|
||||
let role: GroupParticipantRole;
|
||||
switch (type) {
|
||||
case GroupParticipantType.JOIN:
|
||||
role = GroupParticipantRole.PARTICIPANT;
|
||||
break;
|
||||
case GroupParticipantType.LEAVE:
|
||||
role = GroupParticipantRole.LEFT;
|
||||
break;
|
||||
case GroupParticipantType.PROMOTE:
|
||||
role = GroupParticipantRole.ADMIN;
|
||||
break;
|
||||
case GroupParticipantType.DEMOTE:
|
||||
role = GroupParticipantRole.PARTICIPANT;
|
||||
break;
|
||||
}
|
||||
const participants: GroupParticipant[] = [];
|
||||
for (const jid of jids) {
|
||||
participants.push({
|
||||
id: jid,
|
||||
role: role,
|
||||
});
|
||||
}
|
||||
return participants;
|
||||
}
|
||||
|
||||
function ToGroupInfo(group: GroupInfoFull): GroupInfo {
|
||||
const participants: GroupParticipant[] = extractParticipants(
|
||||
group.Participants,
|
||||
);
|
||||
return {
|
||||
id: group.JID,
|
||||
subject: group.Name,
|
||||
description: group.Topic,
|
||||
invite: undefined,
|
||||
participants: participants,
|
||||
membersCanAddNewMember: group.MemberAddMode === 'all_member_add',
|
||||
membersCanSendMessages: !!group.IsAnnounce,
|
||||
newMembersApprovalRequired: group.IsJoinApprovalRequired,
|
||||
};
|
||||
}
|
||||
|
||||
function ToGroupInfoPartial(group: GroupInfoEvent): GroupInfo | null {
|
||||
const announce: boolean | undefined = group.Announce?.IsAnnounce;
|
||||
let membersCanSendMessages: boolean | undefined;
|
||||
// revert announce or undefined
|
||||
if (announce === undefined) {
|
||||
membersCanSendMessages = undefined;
|
||||
} else {
|
||||
membersCanSendMessages = !announce;
|
||||
}
|
||||
|
||||
const info = {
|
||||
id: group.JID,
|
||||
subject: group.Name?.Name,
|
||||
description: group.Topic?.Topic,
|
||||
invite: group.NewInviteLink || undefined,
|
||||
membersCanAddNewMember: undefined,
|
||||
membersCanSendMessages: membersCanSendMessages,
|
||||
newMembersApprovalRequired:
|
||||
group.MembershipApprovalMode?.IsJoinApprovalRequired,
|
||||
participants: undefined,
|
||||
};
|
||||
// check if all fields are undefined but id - return null
|
||||
for (const key in info) {
|
||||
if (key !== 'id' && info[key] !== undefined) {
|
||||
return info;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function extractParticipants(
|
||||
participants: GOWSGroupParticipant[],
|
||||
): GroupParticipant[] {
|
||||
const result: GroupParticipant[] = [];
|
||||
for (const participant of participants) {
|
||||
let role: GroupParticipantRole;
|
||||
if (participant.IsSuperAdmin) {
|
||||
role = GroupParticipantRole.SUPERADMIN;
|
||||
} else if (participant.IsAdmin) {
|
||||
role = GroupParticipantRole.ADMIN;
|
||||
} else {
|
||||
role = GroupParticipantRole.PARTICIPANT;
|
||||
}
|
||||
result.push({
|
||||
id: participant.JID,
|
||||
role: role,
|
||||
});
|
||||
}
|
||||
return result;
|
||||
}
|
||||
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,42 @@
|
||||
import { messages } from '@waha/core/engines/gows/grpc/gows';
|
||||
import { MessageStatus } from '@waha/core/engines/gows/types';
|
||||
import { WAMessageAck } from '@waha/structures/enums.dto';
|
||||
|
||||
export function optional(value: any, type) {
|
||||
if (value === null || value === undefined) {
|
||||
return null;
|
||||
}
|
||||
return new type({ value: value });
|
||||
}
|
||||
|
||||
export function parseJson(value: messages.Json | { data?: string }) {
|
||||
if (value instanceof messages.Json) {
|
||||
value = value.toObject();
|
||||
}
|
||||
if (value.data === undefined) {
|
||||
return undefined;
|
||||
}
|
||||
return JSON.parse(value.data);
|
||||
}
|
||||
|
||||
export function parseJsonList(value: messages.JsonList) {
|
||||
return value.toObject().elements.map((value) => parseJson(value));
|
||||
}
|
||||
|
||||
export function statusToAck(status: MessageStatus): WAMessageAck | null {
|
||||
switch (status) {
|
||||
case MessageStatus.Error:
|
||||
return WAMessageAck.ERROR;
|
||||
case MessageStatus.Pending:
|
||||
return WAMessageAck.PENDING;
|
||||
case MessageStatus.ServerAck:
|
||||
return WAMessageAck.SERVER;
|
||||
case MessageStatus.DeliveryAck:
|
||||
return WAMessageAck.DEVICE;
|
||||
case MessageStatus.Read:
|
||||
return WAMessageAck.READ;
|
||||
case MessageStatus.Played:
|
||||
return WAMessageAck.PLAYED;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
import { toCusFormat } from '@waha/core/engines/noweb/session.noweb.core';
|
||||
import { Label, LabelChatAssociation } from '@waha/structures/labels.dto';
|
||||
|
||||
import * as gows from './types';
|
||||
|
||||
/**
|
||||
* Converts a GOWS LabelEdit event to a Label DTO
|
||||
*/
|
||||
export function eventToLabelDTO(labelEdit: gows.LabelEdit): Label {
|
||||
return {
|
||||
id: labelEdit.LabelID,
|
||||
name: labelEdit.Action.name,
|
||||
color: labelEdit.Action.color,
|
||||
colorHex: Label.toHex(labelEdit.Action.color),
|
||||
} as Label;
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a GOWS LabelAssociationChat event to a LabelChatAssociation DTO
|
||||
*/
|
||||
export function eventToLabelChatAssociationDTO(
|
||||
labelAssoc: gows.LabelAssociationChat,
|
||||
): LabelChatAssociation {
|
||||
return {
|
||||
labelId: labelAssoc.LabelID,
|
||||
label: null, // We don't have the label info here
|
||||
chatId: toCusFormat(labelAssoc.JID),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a LabelEdit event is for a label creation or update (not deletion)
|
||||
*/
|
||||
export function isLabelUpsertEvent(labelEdit: gows.LabelEdit): boolean {
|
||||
return !labelEdit.Action.deleted;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a LabelAssociationChat event is for adding a label to a chat
|
||||
*/
|
||||
export function isLabelChatAddedEvent(
|
||||
labelAssoc: gows.LabelAssociationChat,
|
||||
): boolean {
|
||||
return labelAssoc.Action.labeled === true;
|
||||
}
|
||||
File diff suppressed because it is too large.
Load diff
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
// JID just a string alias, we dump it as String in golang
|
||||
type JID = string;
|
||||
|
||||
interface GroupName {
|
||||
Name: string;
|
||||
NameSetAt: string; // ISO string format for time
|
||||
NameSetBy: JID;
|
||||
}
|
||||
|
||||
interface GroupTopic {
|
||||
Topic: string;
|
||||
TopicID: string;
|
||||
TopicSetAt: string;
|
||||
TopicSetBy: JID;
|
||||
TopicDeleted: boolean;
|
||||
}
|
||||
|
||||
interface GroupLocked {
|
||||
IsLocked: boolean;
|
||||
}
|
||||
|
||||
interface GroupAnnounce {
|
||||
IsAnnounce: boolean;
|
||||
AnnounceVersionID: string;
|
||||
}
|
||||
|
||||
interface GroupEphemeral {
|
||||
IsEphemeral: boolean;
|
||||
DisappearingTimer: number;
|
||||
}
|
||||
|
||||
interface GroupMembershipApprovalMode {
|
||||
IsJoinApprovalRequired: boolean;
|
||||
}
|
||||
|
||||
interface GroupDelete {
|
||||
Deleted: boolean;
|
||||
DeleteReason: string;
|
||||
}
|
||||
|
||||
interface GroupLinkTarget {
|
||||
JID: JID;
|
||||
GroupName: GroupName;
|
||||
GroupIsDefaultSub: GroupIsDefaultSub;
|
||||
}
|
||||
|
||||
interface GroupLinkChange {
|
||||
Type: string; // Enum in Go: GroupLinkChangeType
|
||||
UnlinkReason: string; // Enum in Go: GroupUnlinkReason
|
||||
Group: GroupLinkTarget;
|
||||
}
|
||||
|
||||
interface GroupIsDefaultSub {
|
||||
IsDefaultSubGroup: boolean;
|
||||
}
|
||||
|
||||
export interface GroupInfoEvent {
|
||||
JID: JID;
|
||||
Notify: string;
|
||||
Sender?: JID;
|
||||
Timestamp: string;
|
||||
|
||||
Name?: GroupName;
|
||||
Topic?: GroupTopic;
|
||||
Locked?: GroupLocked;
|
||||
Announce?: GroupAnnounce;
|
||||
Ephemeral?: GroupEphemeral;
|
||||
|
||||
MembershipApprovalMode?: GroupMembershipApprovalMode;
|
||||
Delete?: GroupDelete;
|
||||
|
||||
Link?: GroupLinkChange;
|
||||
Unlink?: GroupLinkChange;
|
||||
NewInviteLink?: string;
|
||||
|
||||
PrevParticipantVersionID: string;
|
||||
ParticipantVersionID: string;
|
||||
|
||||
JoinReason: string;
|
||||
|
||||
Join: JID[];
|
||||
Leave: JID[];
|
||||
Promote: JID[];
|
||||
Demote: JID[];
|
||||
}
|
||||
|
||||
export interface GOWSGroupParticipant {
|
||||
JID: JID;
|
||||
IsAdmin: boolean;
|
||||
IsSuperAdmin: boolean;
|
||||
}
|
||||
|
||||
export interface GroupInfoFull {
|
||||
JID: JID;
|
||||
OwnerJID: JID;
|
||||
Name: string; // Name has additional properties here
|
||||
Topic: string; // Topic has additional properties here
|
||||
IsLocked: boolean; // specifies whether the group info can only be edited by admins.
|
||||
IsAnnounce: boolean; // specifies whether only admins can send messages in the group
|
||||
Participants: GOWSGroupParticipant[];
|
||||
MemberAddMode: string; // all_member_add | admin_add
|
||||
IsJoinApprovalRequired: boolean;
|
||||
}
|
||||
|
||||
export interface JoinedGroupEvent extends GroupInfoFull {
|
||||
Reason: string;
|
||||
Type: string;
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* StatusError Status = 0
|
||||
* StatusPending Status = 1
|
||||
* StatusServerAck Status = 2
|
||||
* StatusDeliveryAck Status = 3
|
||||
* StatusRead Status = 4
|
||||
* StatusPlayed Status = 5
|
||||
*/
|
||||
export enum MessageStatus {
|
||||
Error = 0,
|
||||
Pending = 1,
|
||||
ServerAck = 2,
|
||||
DeliveryAck = 3,
|
||||
Read = 4,
|
||||
Played = 5,
|
||||
}
|
||||
|
||||
export interface LabelEditAction {
|
||||
name: string;
|
||||
color: number;
|
||||
deleted: boolean;
|
||||
orderIndex: number;
|
||||
isActive: boolean;
|
||||
type: number;
|
||||
}
|
||||
|
||||
export interface LabelEdit {
|
||||
Timestamp: string;
|
||||
LabelID: string;
|
||||
Action: LabelEditAction;
|
||||
FromFullSync: boolean;
|
||||
}
|
||||
|
||||
export interface LabelAssociationChatAction {
|
||||
labeled: boolean;
|
||||
}
|
||||
|
||||
export interface LabelAssociationChat {
|
||||
JID: string;
|
||||
Timestamp: string;
|
||||
LabelID: string;
|
||||
Action: LabelAssociationChatAction;
|
||||
FromFullSync: boolean;
|
||||
}
|
||||
@@ -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 };
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
import * as NodeCache from 'node-cache';
|
||||
|
||||
export class PresenceProxy {
|
||||
private cache: NodeCache;
|
||||
|
||||
public proxy: Record<string, any>;
|
||||
|
||||
constructor() {
|
||||
this.cache = new NodeCache({
|
||||
stdTTL: 60, // 1 minute
|
||||
checkperiod: 60, // 1 minute
|
||||
useClones: false,
|
||||
});
|
||||
|
||||
this.proxy = new Proxy(
|
||||
{},
|
||||
{
|
||||
get: (_, prop: string) => {
|
||||
return this.cache.get(prop);
|
||||
},
|
||||
|
||||
set: (_, prop: string, value) => {
|
||||
this.cache.set(prop, value);
|
||||
return true;
|
||||
},
|
||||
|
||||
deleteProperty: (_, prop: string) => {
|
||||
return this.cache.del(prop) > 0;
|
||||
},
|
||||
|
||||
has: (_, prop: string) => {
|
||||
return this.cache.has(prop);
|
||||
},
|
||||
|
||||
ownKeys: () => {
|
||||
return this.cache.keys();
|
||||
},
|
||||
|
||||
getOwnPropertyDescriptor: (_, prop: string) => {
|
||||
if (this.cache.has(prop)) {
|
||||
return {
|
||||
enumerable: true,
|
||||
configurable: true,
|
||||
};
|
||||
}
|
||||
return undefined;
|
||||
},
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,143 @@
|
||||
import { GroupMetadata, jidNormalizedUser } from '@adiwajshing/baileys';
|
||||
import type { Contact } from '@adiwajshing/baileys/lib/Types/Contact';
|
||||
import {
|
||||
GroupParticipant as NOWEBGroupParticipant,
|
||||
ParticipantAction,
|
||||
} from '@adiwajshing/baileys/lib/Types/GroupMetadata';
|
||||
import { getGroupInviteLink } from '@waha/core/abc/session.abc';
|
||||
import { toCusFormat } from '@waha/core/engines/noweb/session.noweb.core';
|
||||
import {
|
||||
GroupInfo,
|
||||
GroupParticipant,
|
||||
GroupParticipantRole,
|
||||
} from '@waha/structures/groups.dto';
|
||||
import {
|
||||
GroupParticipantType,
|
||||
GroupV2JoinEvent,
|
||||
GroupV2LeaveEvent,
|
||||
GroupV2ParticipantsEvent,
|
||||
GroupV2UpdateEvent,
|
||||
} from '@waha/structures/groups.events.dto';
|
||||
|
||||
export function ToGroupInfo(group: Partial<GroupMetadata>): GroupInfo {
|
||||
let participants: GroupParticipant[] = undefined;
|
||||
if (group.participants && group.participants.length > 0) {
|
||||
participants = group.participants.map(ToGroupParticipant);
|
||||
}
|
||||
return {
|
||||
id: group.id,
|
||||
subject: group.subject,
|
||||
description: group.desc,
|
||||
invite: group.inviteCode ? getGroupInviteLink(group.inviteCode) : undefined,
|
||||
participants: participants,
|
||||
membersCanAddNewMember: group.memberAddMode,
|
||||
membersCanSendMessages: group.announce,
|
||||
newMembersApprovalRequired: group.joinApprovalMode,
|
||||
};
|
||||
}
|
||||
|
||||
export function ToGroupV2JoinEvent(group: GroupMetadata): GroupV2JoinEvent {
|
||||
return {
|
||||
timestamp: Date.now(),
|
||||
group: ToGroupInfo(group),
|
||||
_data: group,
|
||||
};
|
||||
}
|
||||
|
||||
export function ToGroupParticipant(
|
||||
participant: NOWEBGroupParticipant,
|
||||
): GroupParticipant {
|
||||
let role: GroupParticipantRole = GroupParticipantRole.PARTICIPANT;
|
||||
if (participant.admin === 'admin') {
|
||||
role = GroupParticipantRole.ADMIN;
|
||||
} else if (participant.admin === 'superadmin') {
|
||||
role = GroupParticipantRole.SUPERADMIN;
|
||||
}
|
||||
return {
|
||||
id: toCusFormat(participant.id),
|
||||
role: role,
|
||||
};
|
||||
}
|
||||
|
||||
interface GroupParticipantUpdate {
|
||||
id: string;
|
||||
author: string;
|
||||
participants: string[];
|
||||
action: ParticipantAction;
|
||||
}
|
||||
|
||||
export function ToGroupV2Participants(
|
||||
update: GroupParticipantUpdate,
|
||||
): GroupV2ParticipantsEvent {
|
||||
let role: GroupParticipantRole;
|
||||
let type: GroupParticipantType;
|
||||
switch (update.action) {
|
||||
case 'add':
|
||||
role = GroupParticipantRole.PARTICIPANT;
|
||||
type = GroupParticipantType.JOIN;
|
||||
break;
|
||||
case 'remove':
|
||||
role = GroupParticipantRole.LEFT;
|
||||
type = GroupParticipantType.LEAVE;
|
||||
break;
|
||||
case 'promote':
|
||||
role = GroupParticipantRole.ADMIN;
|
||||
type = GroupParticipantType.PROMOTE;
|
||||
break;
|
||||
case 'demote':
|
||||
role = GroupParticipantRole.ADMIN;
|
||||
type = GroupParticipantType.DEMOTE;
|
||||
break;
|
||||
}
|
||||
|
||||
const participants: GroupParticipant[] = update.participants.map((id) => {
|
||||
return {
|
||||
id: toCusFormat(id),
|
||||
role: role,
|
||||
};
|
||||
});
|
||||
|
||||
return {
|
||||
group: {
|
||||
id: toCusFormat(update.id),
|
||||
},
|
||||
type: type,
|
||||
timestamp: Date.now(),
|
||||
participants: participants,
|
||||
_data: update,
|
||||
};
|
||||
}
|
||||
|
||||
export function ToGroupV2UpdateEvent(
|
||||
group: Partial<GroupMetadata>,
|
||||
): GroupV2UpdateEvent {
|
||||
return {
|
||||
timestamp: Date.now(),
|
||||
group: ToGroupInfo(group),
|
||||
_data: group,
|
||||
};
|
||||
}
|
||||
|
||||
export function ToGroupV2LeaveEvent(
|
||||
me: Contact,
|
||||
update: GroupParticipantUpdate,
|
||||
): GroupV2LeaveEvent | null {
|
||||
if (update.action !== 'remove') {
|
||||
return null;
|
||||
}
|
||||
if (!me) {
|
||||
return null;
|
||||
}
|
||||
const meId = jidNormalizedUser(me.id);
|
||||
if (!update.participants.includes(meId)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
timestamp: Date.now(),
|
||||
group: {
|
||||
id: toCusFormat(update.id),
|
||||
},
|
||||
_data: update,
|
||||
};
|
||||
}
|
||||
File diff suppressed because it is too large.
Load diff
@@ -1,4 +1,5 @@
|
||||
import { Chat } from '@adiwajshing/baileys';
|
||||
import { OverviewFilter } from '@waha/structures/chats.dto';
|
||||
import { PaginationParams } from '@waha/structures/pagination.dto';
|
||||
|
||||
export interface IChatRepository {
|
||||
@@ -6,7 +7,11 @@ export interface IChatRepository {
|
||||
|
||||
getAllByIds(ids: string[]): Promise<Chat[]>;
|
||||
|
||||
getAllWithMessages(pagination: PaginationParams): Promise<Chat[]>;
|
||||
getAllWithMessages(
|
||||
pagination: PaginationParams,
|
||||
broadcast: boolean,
|
||||
filter?: OverviewFilter,
|
||||
): Promise<Chat[]>;
|
||||
|
||||
getById(id: string): Promise<Chat | null>;
|
||||
|
||||
@@ -15,4 +20,6 @@ export interface IChatRepository {
|
||||
deleteById(id: string): Promise<void>;
|
||||
|
||||
save(chat: Chat): Promise<void>;
|
||||
|
||||
upsertMany(chats: Chat[]): Promise<void>;
|
||||
}
|
||||
@@ -11,4 +11,8 @@ export interface IContactRepository {
|
||||
deleteById(id: string): Promise<void>;
|
||||
|
||||
save(contact: Contact): Promise<void>;
|
||||
|
||||
upsertMany(contacts: Contact[]): Promise<void>;
|
||||
|
||||
getEntitiesByIds(ids: string[]): Promise<Map<string, Contact | null>>;
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
import { GroupMetadata } from '@adiwajshing/baileys';
|
||||
import { PaginationParams } from '@waha/structures/pagination.dto';
|
||||
|
||||
export interface IGroupRepository {
|
||||
getAll(pagination?: PaginationParams): Promise<GroupMetadata[]>;
|
||||
|
||||
getById(id: string): Promise<GroupMetadata | null>;
|
||||
|
||||
deleteAll(): Promise<void>;
|
||||
|
||||
deleteById(id: string): Promise<void>;
|
||||
|
||||
save(group: GroupMetadata): Promise<void>;
|
||||
}
|
||||
@@ -8,6 +8,8 @@ export interface IMessagesRepository {
|
||||
|
||||
upsertOne(message: any): Promise<void>;
|
||||
|
||||
getById(id: string): Promise<any | null>;
|
||||
|
||||
getAllByJid(
|
||||
jid: string,
|
||||
filter: GetChatMessagesFilter,
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
import { LimitOffsetParams } from '@waha/structures/pagination.dto';
|
||||
|
||||
export class LidToPN {
|
||||
id: string;
|
||||
pn: string;
|
||||
}
|
||||
|
||||
export interface INowebLidPNRepository {
|
||||
/**
|
||||
* Save lid to phone number mapping
|
||||
*/
|
||||
saveLids(lids: LidToPN[]): Promise<void>;
|
||||
|
||||
/**
|
||||
* Get all lids with pagination
|
||||
*/
|
||||
getAllLids(pagination?: LimitOffsetParams): Promise<LidToPN[]>;
|
||||
|
||||
/**
|
||||
* Get a total count of lids
|
||||
*/
|
||||
getLidsCount(): Promise<number>;
|
||||
|
||||
/**
|
||||
* Find a phone number by lid
|
||||
*/
|
||||
findPNByLid(lid: string): Promise<string | null>;
|
||||
|
||||
/**
|
||||
* Find lid by phone number
|
||||
*/
|
||||
findLidByPN(pn: string): Promise<string | null>;
|
||||
}
|
||||
@@ -1,11 +1,11 @@
|
||||
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';
|
||||
|
||||
import { IChatRepository } from './IChatRepository';
|
||||
import { IContactRepository } from './IContactRepository';
|
||||
import { IMessagesRepository } from './IMessagesRepository';
|
||||
import { INowebLidPNRepository } from './INowebLidPNRepository';
|
||||
|
||||
export abstract class INowebStorage {
|
||||
abstract init(): Promise<void>;
|
||||
@@ -16,25 +16,13 @@ export abstract class INowebStorage {
|
||||
|
||||
abstract getChatRepository(): IChatRepository;
|
||||
|
||||
abstract getGroupRepository(): IGroupRepository;
|
||||
|
||||
abstract getMessagesRepository(): IMessagesRepository;
|
||||
|
||||
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}`,
|
||||
);
|
||||
}
|
||||
abstract getLidPNRepository(): INowebLidPNRepository;
|
||||
}
|
||||
@@ -4,9 +4,17 @@ import {
|
||||
Contact,
|
||||
proto,
|
||||
} from '@adiwajshing/baileys';
|
||||
import { GroupMetadata } from '@adiwajshing/baileys/lib/Types/GroupMetadata';
|
||||
import { Label } from '@adiwajshing/baileys/lib/Types/Label';
|
||||
import { GetChatMessagesFilter } from '@waha/structures/chats.dto';
|
||||
import { PaginationParams } from '@waha/structures/pagination.dto';
|
||||
import {
|
||||
GetChatMessagesFilter,
|
||||
OverviewFilter,
|
||||
} from '@waha/structures/chats.dto';
|
||||
import { LidToPhoneNumber } from '@waha/structures/lids.dto';
|
||||
import {
|
||||
LimitOffsetParams,
|
||||
PaginationParams,
|
||||
} from '@waha/structures/pagination.dto';
|
||||
|
||||
export interface INowebStore {
|
||||
presences: any;
|
||||
@@ -27,7 +35,13 @@ export interface INowebStore {
|
||||
|
||||
getMessageById(chatId: string, messageId: string): Promise<any>;
|
||||
|
||||
getChats(pagination: PaginationParams): Promise<Chat[]>;
|
||||
getChats(
|
||||
pagination: PaginationParams,
|
||||
broadcast: boolean,
|
||||
filter?: OverviewFilter,
|
||||
): Promise<Chat[]>;
|
||||
|
||||
getChat(jid: string): Promise<Chat | null>;
|
||||
|
||||
getContacts(pagination: PaginationParams): Promise<Contact[]>;
|
||||
|
||||
@@ -40,4 +54,17 @@ export interface INowebStore {
|
||||
getChatsByLabelId(labelId: string): Promise<Chat[]>;
|
||||
|
||||
getChatLabels(chatId: string): Promise<Label[]>;
|
||||
|
||||
getGroups(pagination: PaginationParams): Promise<GroupMetadata[]>;
|
||||
|
||||
resetGroupsCache(): void;
|
||||
|
||||
// Lid Repository methods
|
||||
getAllLids(pagination?: LimitOffsetParams): Promise<LidToPhoneNumber[]>;
|
||||
|
||||
getLidsCount(): Promise<number>;
|
||||
|
||||
findPNByLid(lid: string): Promise<string | null>;
|
||||
|
||||
findLidByPN(pn: string): Promise<string | null>;
|
||||
}
|
||||
@@ -1,15 +1,28 @@
|
||||
import { Chat, Contact, makeInMemoryStore, proto } from '@adiwajshing/baileys';
|
||||
import makeWASocket, {
|
||||
Chat,
|
||||
Contact,
|
||||
GroupMetadata,
|
||||
proto,
|
||||
} from '@adiwajshing/baileys';
|
||||
import { Label } from '@adiwajshing/baileys/lib/Types/Label';
|
||||
import { BadRequestException } from '@nestjs/common';
|
||||
import { GetChatMessagesFilter } from '@waha/structures/chats.dto';
|
||||
import { PaginationParams } from '@waha/structures/pagination.dto';
|
||||
import { LidToPhoneNumber } from '@waha/structures/lids.dto';
|
||||
import {
|
||||
LimitOffsetParams,
|
||||
PaginationParams,
|
||||
} from '@waha/structures/pagination.dto';
|
||||
import { PaginatorInMemory } from '@waha/utils/Paginator';
|
||||
|
||||
import { INowebStore } from './INowebStore';
|
||||
import makeInMemoryStore from './memory/make-in-memory-store';
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
const logger = require('pino')();
|
||||
|
||||
export class NowebInMemoryStore implements INowebStore {
|
||||
private socket: ReturnType<typeof makeWASocket>;
|
||||
|
||||
private store: ReturnType<typeof makeInMemoryStore>;
|
||||
errorMessage =
|
||||
'Enable NOWEB store "config.noweb.store.enabled=True" and "config.noweb.store.full_sync=True" when starting a new session. ' +
|
||||
@@ -17,6 +30,10 @@ export class NowebInMemoryStore implements INowebStore {
|
||||
|
||||
constructor() {
|
||||
this.store = makeInMemoryStore({ logger: logger });
|
||||
const presences = {};
|
||||
this.store.presences = presences;
|
||||
// Adjust inline even handler
|
||||
this.store.setPresences(presences);
|
||||
}
|
||||
|
||||
init(): Promise<void> {
|
||||
@@ -33,6 +50,7 @@ export class NowebInMemoryStore implements INowebStore {
|
||||
|
||||
bind(ev: any, socket: any) {
|
||||
this.store.bind(ev);
|
||||
this.socket = socket;
|
||||
}
|
||||
|
||||
loadMessage(jid: string, id: string): Promise<proto.IWebMessageInfo> {
|
||||
@@ -51,10 +69,14 @@ 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);
|
||||
}
|
||||
|
||||
getChat(jid: string): Promise<Chat | null> {
|
||||
return null;
|
||||
}
|
||||
|
||||
getContacts(pagination: PaginationParams): Promise<Contact[]> {
|
||||
throw new BadRequestException(this.errorMessage);
|
||||
}
|
||||
@@ -78,4 +100,34 @@ export class NowebInMemoryStore implements INowebStore {
|
||||
getChatLabels(chatId: string): Promise<Label[]> {
|
||||
throw new BadRequestException(this.errorMessage);
|
||||
}
|
||||
|
||||
async getGroups(pagination: PaginationParams): Promise<GroupMetadata[]> {
|
||||
const response = await this.socket?.groupFetchAllParticipating();
|
||||
const groups: any[] = Object.values(response);
|
||||
const paginator = new PaginatorInMemory(pagination);
|
||||
return paginator.apply(groups);
|
||||
}
|
||||
|
||||
resetGroupsCache() {
|
||||
return;
|
||||
}
|
||||
|
||||
//
|
||||
// Lids methods
|
||||
//
|
||||
getAllLids(pagination?: LimitOffsetParams): Promise<LidToPhoneNumber[]> {
|
||||
throw new BadRequestException(this.errorMessage);
|
||||
}
|
||||
|
||||
findLidByPN(pn: string): Promise<string | null> {
|
||||
throw new BadRequestException(this.errorMessage);
|
||||
}
|
||||
|
||||
findPNByLid(lid: string): Promise<string | null> {
|
||||
throw new BadRequestException(this.errorMessage);
|
||||
}
|
||||
|
||||
getLidsCount(): Promise<number> {
|
||||
throw new BadRequestException(this.errorMessage);
|
||||
}
|
||||
}
|
||||
@@ -1,45 +1,81 @@
|
||||
import {
|
||||
import makeWASocket, {
|
||||
areJidsSameUser,
|
||||
BaileysEventEmitter,
|
||||
Chat,
|
||||
ChatUpdate,
|
||||
Contact,
|
||||
GroupParticipant,
|
||||
isJidUser,
|
||||
isRealMessage,
|
||||
jidNormalizedUser,
|
||||
ParticipantAction,
|
||||
proto,
|
||||
updateMessageWithReaction,
|
||||
updateMessageWithReceipt,
|
||||
} from '@adiwajshing/baileys';
|
||||
import { GroupMetadata } from '@adiwajshing/baileys/lib/Types/GroupMetadata';
|
||||
import { Label } from '@adiwajshing/baileys/lib/Types/Label';
|
||||
import {
|
||||
LabelAssociation,
|
||||
LabelAssociationType,
|
||||
} from '@adiwajshing/baileys/lib/Types/LabelAssociation';
|
||||
import { isLidUser } from '@adiwajshing/baileys/lib/WABinary/jid-utils';
|
||||
import { IGroupRepository } from '@waha/core/engines/noweb/store/IGroupRepository';
|
||||
import { ILabelAssociationRepository } from '@waha/core/engines/noweb/store/ILabelAssociationsRepository';
|
||||
import { ILabelsRepository } from '@waha/core/engines/noweb/store/ILabelsRepository';
|
||||
import { GetChatMessagesFilter } from '@waha/structures/chats.dto';
|
||||
import { PaginationParams, SortOrder } from '@waha/structures/pagination.dto';
|
||||
import {
|
||||
GetChatMessagesFilter,
|
||||
OverviewFilter,
|
||||
} from '@waha/structures/chats.dto';
|
||||
import { LidToPhoneNumber } from '@waha/structures/lids.dto';
|
||||
import {
|
||||
LimitOffsetParams,
|
||||
PaginationParams,
|
||||
SortOrder,
|
||||
} from '@waha/structures/pagination.dto';
|
||||
import { DefaultMap } from '@waha/utils/DefaultMap';
|
||||
import { waitUntil } from '@waha/utils/promiseTimeout';
|
||||
import * as lodash from 'lodash';
|
||||
import { toNumber } from 'lodash';
|
||||
import { Logger } from 'pino';
|
||||
|
||||
import { toJID } from '../session.noweb.core';
|
||||
import { IChatRepository } from './IChatRepository';
|
||||
import { IContactRepository } from './IContactRepository';
|
||||
import { IMessagesRepository } from './IMessagesRepository';
|
||||
import { INowebLidPNRepository, LidToPN } from './INowebLidPNRepository';
|
||||
import { INowebStorage } from './INowebStorage';
|
||||
import { INowebStore } from './INowebStore';
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
const AsyncLock = require('async-lock');
|
||||
|
||||
type ms = number;
|
||||
const HOUR: ms = 60 * 60 * 1000;
|
||||
|
||||
export class NowebPersistentStore implements INowebStore {
|
||||
private socket: any;
|
||||
private socket: ReturnType<typeof makeWASocket>;
|
||||
private chatRepo: IChatRepository;
|
||||
private groupRepo: IGroupRepository;
|
||||
private contactRepo: IContactRepository;
|
||||
private messagesRepo: IMessagesRepository;
|
||||
private labelsRepo: ILabelsRepository;
|
||||
private labelAssociationsRepo: ILabelAssociationRepository;
|
||||
private lidRepo: INowebLidPNRepository;
|
||||
public presences: any;
|
||||
private lock: any;
|
||||
|
||||
private lock: any = new AsyncLock({
|
||||
maxPending: Infinity,
|
||||
maxExecutionTime: 60_000,
|
||||
});
|
||||
|
||||
private groupsFetchLock: any = new AsyncLock({
|
||||
maxPending: Infinity,
|
||||
maxExecutionTime: 60_000,
|
||||
});
|
||||
|
||||
private lastTimeGroupUpdate: Date = new Date(0);
|
||||
private lastTimeGroupFetch: Date = new Date(0);
|
||||
private GROUP_METADATA_CACHE_TIME = 24 * HOUR;
|
||||
|
||||
constructor(
|
||||
private logger: Logger,
|
||||
@@ -47,12 +83,13 @@ export class NowebPersistentStore implements INowebStore {
|
||||
) {
|
||||
this.socket = null;
|
||||
this.chatRepo = storage.getChatRepository();
|
||||
this.groupRepo = storage.getGroupRepository();
|
||||
this.contactRepo = storage.getContactsRepository();
|
||||
this.messagesRepo = storage.getMessagesRepository();
|
||||
this.labelsRepo = storage.getLabelsRepository();
|
||||
this.labelAssociationsRepo = storage.getLabelAssociationRepository();
|
||||
this.lidRepo = storage.getLidPNRepository();
|
||||
this.presences = {};
|
||||
this.lock = new AsyncLock({ maxPending: Infinity });
|
||||
}
|
||||
|
||||
init(): Promise<void> {
|
||||
@@ -88,13 +125,45 @@ export class NowebPersistentStore implements INowebStore {
|
||||
ev.on('chats.delete', (data) =>
|
||||
this.withLock('chats', () => this.onChatDelete(data)),
|
||||
);
|
||||
// Groups
|
||||
ev.on('groups.upsert', (data) =>
|
||||
this.withLock('groups', () => this.onGroupUpsert(data)),
|
||||
);
|
||||
ev.on('groups.update', (data) => {
|
||||
this.withLock('groups', () => this.onGroupUpdate(data));
|
||||
this.withLock('lids', async () => {
|
||||
const participants = lodash.flatMap(data, (g) => g?.participants || []);
|
||||
const lids = await this.handleLidPNUpdates(participants);
|
||||
this.logger.debug(
|
||||
`groups.update - '${lids.length}' synced lid to pn mapping`,
|
||||
);
|
||||
});
|
||||
});
|
||||
ev.on('group-participants.update', (data) =>
|
||||
this.withLock(`group-${data.id}`, () =>
|
||||
this.onGroupParticipantsUpdate(data),
|
||||
),
|
||||
);
|
||||
|
||||
// Contacts
|
||||
ev.on('contacts.upsert', (data) =>
|
||||
this.withLock('contacts', () => this.onContactsUpsert(data)),
|
||||
);
|
||||
ev.on('contacts.update', (data) =>
|
||||
this.withLock('contacts', () => this.onContactUpdate(data)),
|
||||
);
|
||||
ev.on('contacts.upsert', (data) => {
|
||||
this.withLock('contacts', () => this.onContactsUpsert(data));
|
||||
this.withLock('lids', async () => {
|
||||
const lids = await this.handleLidPNUpdates(data);
|
||||
this.logger.debug(
|
||||
`contacts.upsert - '${lids.length}' synced lid to pn mapping`,
|
||||
);
|
||||
});
|
||||
});
|
||||
ev.on('contacts.update', (data) => {
|
||||
this.withLock('contacts', () => this.onContactUpdate(data));
|
||||
this.withLock('lids', async () => {
|
||||
const lids = await this.handleLidPNUpdates(data);
|
||||
this.logger.debug(
|
||||
`contacts.update - '${lids.length}' synced lid to pn mapping`,
|
||||
);
|
||||
});
|
||||
});
|
||||
ev.on('labels.edit', (data) => this.onLabelsEdit(data));
|
||||
ev.on('labels.association', ({ association, type }) =>
|
||||
this.onLabelsAssociation(association, type),
|
||||
@@ -129,6 +198,12 @@ export class NowebPersistentStore implements INowebStore {
|
||||
await this.onContactsUpsert(contacts);
|
||||
this.logger.info(`history sync - '${contacts.length}' synced contacts`);
|
||||
}),
|
||||
this.withLock('lids', async () => {
|
||||
const lids = await this.handleLidPNUpdates(contacts);
|
||||
this.logger.info(
|
||||
`history sync - '${lids.length}' synced lid to pn mapping`,
|
||||
);
|
||||
}),
|
||||
this.withLock('chats', () => this.onChatUpsert(chats)),
|
||||
this.withLock('messages', () => this.syncMessagesHistory(messages)),
|
||||
]);
|
||||
@@ -159,11 +234,31 @@ export class NowebPersistentStore implements INowebStore {
|
||||
for (const update of updates) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
const jid = jidNormalizedUser(update.key.remoteJid!);
|
||||
if (!update.key.id) {
|
||||
continue;
|
||||
}
|
||||
const message = await this.messagesRepo.getByJidById(jid, update.key.id);
|
||||
if (!message) {
|
||||
this.logger.warn(
|
||||
`got update for non-existent message. update: '${JSON.stringify(
|
||||
update,
|
||||
)}'`,
|
||||
);
|
||||
continue;
|
||||
}
|
||||
const fields = { ...update.update };
|
||||
// check if fields has only "status" field
|
||||
const onlyStatusField =
|
||||
Object.keys(fields).length === 1 &&
|
||||
'status' in fields &&
|
||||
fields.status !== null;
|
||||
if (onlyStatusField) {
|
||||
// if so, check the message don't have a newer status
|
||||
if (message.status >= fields.status) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// It can overwrite the key, so we need to delete it
|
||||
delete fields['key'];
|
||||
Object.assign(message, fields);
|
||||
@@ -192,17 +287,97 @@ export class NowebPersistentStore implements INowebStore {
|
||||
private async onChatUpsert(chats: Chat[]) {
|
||||
for (const chat of chats) {
|
||||
delete chat['messages'];
|
||||
chat.conversationTimestamp = toNumber(chat.conversationTimestamp);
|
||||
await this.chatRepo.save(chat);
|
||||
chat.conversationTimestamp = toNumber(chat.conversationTimestamp) || null;
|
||||
}
|
||||
await this.chatRepo.upsertMany(chats);
|
||||
this.logger.info(`store sync - '${chats.length}' synced chats`);
|
||||
}
|
||||
|
||||
private async onGroupUpsert(groups: GroupMetadata[]) {
|
||||
for (const group of groups) {
|
||||
await this.groupRepo.save(group);
|
||||
}
|
||||
this.logger.info(`store sync - '${groups.length}' synced groups`);
|
||||
}
|
||||
|
||||
private async onGroupUpdate(groups: Partial<GroupMetadata>[]) {
|
||||
for (const update of groups) {
|
||||
let group = await this.groupRepo.getById(update.id);
|
||||
group = Object.assign(group || {}, update) as GroupMetadata;
|
||||
await this.groupRepo.save(group);
|
||||
}
|
||||
this.logger.info(`store sync - '${groups.length}' updated groups`);
|
||||
this.lastTimeGroupUpdate = new Date();
|
||||
}
|
||||
|
||||
private async onGroupParticipantsUpdate(data) {
|
||||
const id: string = data.id;
|
||||
const participants: string[] = data.participants;
|
||||
const action: ParticipantAction = data.action;
|
||||
|
||||
if (action == 'remove') {
|
||||
// Remove the group if the current user is removed
|
||||
const myJid = this.socket?.authState?.creds?.me?.id;
|
||||
const participantsIncludesMe = lodash.find(participants, (p) =>
|
||||
areJidsSameUser(p, myJid),
|
||||
);
|
||||
if (participantsIncludesMe) {
|
||||
await this.groupRepo.deleteById(id);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
let group = await this.groupRepo.getById(id);
|
||||
if (!group) {
|
||||
group = { id: id, participants: [] } as GroupMetadata;
|
||||
}
|
||||
if (!group.participants) {
|
||||
group.participants = [];
|
||||
}
|
||||
|
||||
const participantsById = new DefaultMap<string, GroupParticipant>((key) => {
|
||||
return { id: key, admin: null } as GroupParticipant;
|
||||
});
|
||||
for (const participant of group.participants) {
|
||||
participantsById.set(participant.id, participant);
|
||||
}
|
||||
|
||||
for (const participant of participants) {
|
||||
this.participantUpdate(participantsById, participant, action);
|
||||
}
|
||||
group.participants = Array.from(participantsById.values());
|
||||
await this.groupRepo.save(group);
|
||||
}
|
||||
|
||||
private participantUpdate(
|
||||
participantsById: DefaultMap<string, GroupParticipant>,
|
||||
participant: string,
|
||||
action: ParticipantAction,
|
||||
) {
|
||||
switch (action) {
|
||||
case 'add':
|
||||
// if there's no participant - add it (by id)
|
||||
participantsById.get(participant);
|
||||
break;
|
||||
case 'remove':
|
||||
// remove the participant (by id)
|
||||
participantsById.delete(participant);
|
||||
break;
|
||||
case 'promote':
|
||||
// set admin: admin
|
||||
participantsById.get(participant).admin = 'admin';
|
||||
break;
|
||||
case 'demote':
|
||||
participantsById.get(participant).admin = null;
|
||||
break;
|
||||
}
|
||||
this.logger.info(`history sync - '${chats.length}' synced chats`);
|
||||
}
|
||||
|
||||
private async onChatUpdate(updates: ChatUpdate[]) {
|
||||
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);
|
||||
}
|
||||
@@ -220,20 +395,24 @@ export class NowebPersistentStore implements INowebStore {
|
||||
}
|
||||
|
||||
private async onContactsUpsert(contacts: Contact[]) {
|
||||
const upserts = [];
|
||||
const ids = contacts.map((c) => c.id);
|
||||
const contactById = await this.contactRepo.getEntitiesByIds(ids);
|
||||
for (const update of contacts) {
|
||||
const contact = await this.contactRepo.getById(update.id);
|
||||
const contact = contactById.get(update.id) || {};
|
||||
// remove undefined from data
|
||||
Object.keys(update).forEach(
|
||||
(key) => update[key] === undefined && delete update[key],
|
||||
);
|
||||
const result = { ...(contact || {}), ...update };
|
||||
await this.contactRepo.save(result);
|
||||
const result = { ...contact, ...update };
|
||||
upserts.push(result);
|
||||
}
|
||||
await this.contactRepo.upsertMany(upserts);
|
||||
}
|
||||
|
||||
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(
|
||||
@@ -241,7 +420,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
|
||||
@@ -250,7 +429,12 @@ export class NowebPersistentStore implements INowebStore {
|
||||
|
||||
if (update.imgUrl === 'changed') {
|
||||
contact.imgUrl = this.socket
|
||||
? await this.socket?.profilePictureUrl(contact.id)
|
||||
? await this.socket?.profilePictureUrl(contact.id).catch((error) => {
|
||||
this.logger.warn(
|
||||
`failed to get profile picture for contact '${contact.id}': ${error}`,
|
||||
);
|
||||
return undefined;
|
||||
})
|
||||
: undefined;
|
||||
} else if (update.imgUrl === 'removed') {
|
||||
delete contact.imgUrl;
|
||||
@@ -317,7 +501,12 @@ export class NowebPersistentStore implements INowebStore {
|
||||
}
|
||||
|
||||
async loadMessage(jid: string, id: string) {
|
||||
const data = await this.messagesRepo.getByJidById(jid, id);
|
||||
let data;
|
||||
if (!jid) {
|
||||
data = await this.messagesRepo.getById(id);
|
||||
} else {
|
||||
data = await this.messagesRepo.getByJidById(jid, id);
|
||||
}
|
||||
if (!data) {
|
||||
return null;
|
||||
}
|
||||
@@ -338,10 +527,53 @@ export class NowebPersistentStore implements INowebStore {
|
||||
return this.messagesRepo.getByJidById(chatId, messageId);
|
||||
}
|
||||
|
||||
getChats(pagination: PaginationParams): Promise<Chat[]> {
|
||||
getChats(
|
||||
pagination: PaginationParams,
|
||||
broadcast: boolean,
|
||||
filter?: OverviewFilter,
|
||||
): Promise<Chat[]> {
|
||||
pagination.sortBy ||= 'conversationTimestamp';
|
||||
pagination.sortOrder ||= SortOrder.DESC;
|
||||
return this.chatRepo.getAllWithMessages(pagination);
|
||||
return this.chatRepo.getAllWithMessages(pagination, broadcast, filter);
|
||||
}
|
||||
|
||||
async getChat(jid: string): Promise<Chat | null> {
|
||||
return await this.chatRepo.getById(jid);
|
||||
}
|
||||
|
||||
private shouldFetchGroup(): boolean {
|
||||
const timePassed = new Date().getTime() - this.lastTimeGroupFetch.getTime();
|
||||
return timePassed > this.GROUP_METADATA_CACHE_TIME;
|
||||
}
|
||||
|
||||
private async fetchGroups() {
|
||||
await this.groupsFetchLock.acquire('groups-fetch', async () => {
|
||||
if (!this.shouldFetchGroup()) {
|
||||
// Update has been done by another request
|
||||
return;
|
||||
}
|
||||
const lastTimeGroupUpdate = this.lastTimeGroupUpdate;
|
||||
await this.groupRepo.deleteAll();
|
||||
await this.socket?.groupFetchAllParticipating();
|
||||
// Wait until the groups update is done
|
||||
await waitUntil(
|
||||
async () => this.lastTimeGroupUpdate > lastTimeGroupUpdate,
|
||||
100,
|
||||
5_000,
|
||||
);
|
||||
this.lastTimeGroupFetch = new Date();
|
||||
});
|
||||
}
|
||||
|
||||
resetGroupsCache() {
|
||||
this.lastTimeGroupFetch = new Date(0);
|
||||
}
|
||||
|
||||
async getGroups(pagination: PaginationParams): Promise<GroupMetadata[]> {
|
||||
if (this.shouldFetchGroup()) {
|
||||
await this.fetchGroups();
|
||||
}
|
||||
return this.groupRepo.getAll(pagination);
|
||||
}
|
||||
|
||||
getContactById(jid) {
|
||||
@@ -376,4 +608,64 @@ export class NowebPersistentStore implements INowebStore {
|
||||
const ids = associations.map((association) => association.labelId);
|
||||
return await this.labelsRepo.getAllByIds(ids);
|
||||
}
|
||||
|
||||
//
|
||||
// Lid methods
|
||||
//
|
||||
private async handleLidPNUpdates(contacts: Array<Partial<Contact>>) {
|
||||
let lids: LidToPN[] = [];
|
||||
for (const contact of contacts) {
|
||||
// contact.id = pn, contact.lid = lid
|
||||
if (isJidUser(contact.id) && isLidUser(contact.lid)) {
|
||||
lids.push({
|
||||
pn: contact.id,
|
||||
id: contact.lid,
|
||||
});
|
||||
}
|
||||
// contact.pn = pn, contact.lid = lid
|
||||
else if (isJidUser(contact.pn) && isLidUser(contact.lid)) {
|
||||
lids.push({
|
||||
pn: contact.pn,
|
||||
id: contact.lid,
|
||||
});
|
||||
}
|
||||
// contact.pn = pn, contact.id = lid
|
||||
else if (isJidUser(contact.pn) && isLidUser(contact.id)) {
|
||||
lids.push({
|
||||
pn: contact.pn,
|
||||
id: contact.id,
|
||||
});
|
||||
}
|
||||
}
|
||||
// make lids unique by id
|
||||
lids = lodash.uniqBy(lids, 'id');
|
||||
if (lids.length > 0) {
|
||||
await this.lidRepo.saveLids(lids);
|
||||
}
|
||||
return lids;
|
||||
}
|
||||
|
||||
async getAllLids(
|
||||
pagination?: LimitOffsetParams,
|
||||
): Promise<LidToPhoneNumber[]> {
|
||||
const lids = await this.lidRepo.getAllLids(pagination);
|
||||
return lids.map((value) => {
|
||||
return {
|
||||
lid: value.id,
|
||||
pn: value.pn,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
getLidsCount(): Promise<number> {
|
||||
return this.lidRepo.getLidsCount();
|
||||
}
|
||||
|
||||
findPNByLid(lid: string): Promise<string | null> {
|
||||
return this.lidRepo.findPNByLid(lid);
|
||||
}
|
||||
|
||||
findLidByPN(pn: string): Promise<string | null> {
|
||||
return this.lidRepo.findLidByPN(pn);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -1,58 +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(
|
||||
'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']),
|
||||
],
|
||||
),
|
||||
];
|
||||
@@ -0,0 +1,564 @@
|
||||
/* eslint-disable @typescript-eslint/no-non-null-assertion */
|
||||
|
||||
import type {
|
||||
BaileysEventEmitter,
|
||||
Chat,
|
||||
ConnectionState,
|
||||
Contact,
|
||||
GroupMetadata,
|
||||
PresenceData,
|
||||
WAMessage,
|
||||
WAMessageCursor,
|
||||
WAMessageKey,
|
||||
} from '@adiwajshing/baileys/lib/Types';
|
||||
import { Label } from '@adiwajshing/baileys/lib/Types/Label';
|
||||
import {
|
||||
LabelAssociation,
|
||||
LabelAssociationType,
|
||||
MessageLabelAssociation,
|
||||
} from '@adiwajshing/baileys/lib/Types/LabelAssociation';
|
||||
import {
|
||||
md5,
|
||||
toNumber,
|
||||
updateMessageWithReaction,
|
||||
updateMessageWithReceipt,
|
||||
} from '@adiwajshing/baileys/lib/Utils';
|
||||
import {
|
||||
jidDecode,
|
||||
jidNormalizedUser,
|
||||
} from '@adiwajshing/baileys/lib/WABinary';
|
||||
import { proto } from '@adiwajshing/baileys/WAProto';
|
||||
import type { Comparable } from '@adiwajshing/keyed-db/lib/Types';
|
||||
import type { Logger } from 'pino';
|
||||
|
||||
import makeOrderedDictionary from './make-ordered-dictionary';
|
||||
import { ObjectRepository } from './object-repository';
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
const KeyedDB = require('@adiwajshing/keyed-db').default;
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
const { writeFileSync } = require('fs');
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
const { readFileSync, existsSync } = require('fs');
|
||||
|
||||
type WASocket = ReturnType<any>;
|
||||
|
||||
export const waChatKey = (pin: boolean) => ({
|
||||
key: (c: Chat) =>
|
||||
(pin ? (c.pinned ? '1' : '0') : '') +
|
||||
(c.archived ? '0' : '1') +
|
||||
(c.conversationTimestamp
|
||||
? c.conversationTimestamp.toString(16).padStart(8, '0')
|
||||
: '') +
|
||||
c.id,
|
||||
compare: (k1: string, k2: string) => k2.localeCompare(k1),
|
||||
});
|
||||
|
||||
export const waMessageID = (m: WAMessage) => m.key.id || '';
|
||||
|
||||
export const waLabelAssociationKey: Comparable<LabelAssociation, string> = {
|
||||
key: (la: LabelAssociation) =>
|
||||
la.type === LabelAssociationType.Chat
|
||||
? la.chatId + la.labelId
|
||||
: la.chatId + la.messageId + la.labelId,
|
||||
compare: (k1: string, k2: string) => k2.localeCompare(k1),
|
||||
};
|
||||
|
||||
export type BaileysInMemoryStoreConfig = {
|
||||
chatKey?: Comparable<Chat, string>;
|
||||
labelAssociationKey?: Comparable<LabelAssociation, string>;
|
||||
logger: Logger;
|
||||
socket?: WASocket;
|
||||
};
|
||||
|
||||
const makeMessagesDictionary = () => makeOrderedDictionary(waMessageID);
|
||||
|
||||
export default (config: BaileysInMemoryStoreConfig) => {
|
||||
const socket = config.socket;
|
||||
const chatKey = config.chatKey || waChatKey(true);
|
||||
const labelAssociationKey =
|
||||
config.labelAssociationKey || waLabelAssociationKey;
|
||||
const logger: Logger = config.logger;
|
||||
|
||||
const chats = new KeyedDB(chatKey, (c) => c.id) as any;
|
||||
const messages: { [_: string]: ReturnType<typeof makeMessagesDictionary> } =
|
||||
{};
|
||||
const contacts: { [_: string]: Contact } = {};
|
||||
const groupMetadata: { [_: string]: GroupMetadata } = {};
|
||||
let presences: { [id: string]: { [participant: string]: PresenceData } } = {};
|
||||
const state: ConnectionState = { connection: 'close' };
|
||||
const labels = new ObjectRepository<Label>();
|
||||
const labelAssociations = new KeyedDB(
|
||||
labelAssociationKey,
|
||||
labelAssociationKey.key,
|
||||
) as any;
|
||||
|
||||
const assertMessageList = (jid: string) => {
|
||||
if (!messages[jid]) {
|
||||
messages[jid] = makeMessagesDictionary();
|
||||
}
|
||||
|
||||
return messages[jid];
|
||||
};
|
||||
|
||||
const contactsUpsert = (newContacts: Contact[]) => {
|
||||
const oldContacts = new Set(Object.keys(contacts));
|
||||
for (const contact of newContacts) {
|
||||
oldContacts.delete(contact.id);
|
||||
contacts[contact.id] = Object.assign(contacts[contact.id] || {}, contact);
|
||||
}
|
||||
|
||||
return oldContacts;
|
||||
};
|
||||
|
||||
const labelsUpsert = (newLabels: Label[]) => {
|
||||
for (const label of newLabels) {
|
||||
labels.upsertById(label.id, label);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* binds to a BaileysEventEmitter.
|
||||
* It listens to all events and constructs a state that you can query accurate data from.
|
||||
* Eg. can use the store to fetch chats, contacts, messages etc.
|
||||
* @param ev typically the event emitter from the socket connection
|
||||
*/
|
||||
const bind = (ev: BaileysEventEmitter) => {
|
||||
ev.on('connection.update', (update) => {
|
||||
Object.assign(state, update);
|
||||
});
|
||||
|
||||
ev.on(
|
||||
'messaging-history.set',
|
||||
({
|
||||
chats: newChats,
|
||||
contacts: newContacts,
|
||||
messages: newMessages,
|
||||
isLatest,
|
||||
syncType,
|
||||
}) => {
|
||||
if (syncType === proto.HistorySync.HistorySyncType.ON_DEMAND) {
|
||||
return; // FOR NOW,
|
||||
//TODO: HANDLE
|
||||
}
|
||||
|
||||
if (isLatest) {
|
||||
chats.clear();
|
||||
|
||||
for (const id in messages) {
|
||||
delete messages[id];
|
||||
}
|
||||
}
|
||||
|
||||
const chatsAdded = chats.insertIfAbsent(...newChats).length;
|
||||
logger.debug({ chatsAdded }, 'synced chats');
|
||||
|
||||
const oldContacts = contactsUpsert(newContacts);
|
||||
if (isLatest) {
|
||||
for (const jid of oldContacts) {
|
||||
delete contacts[jid];
|
||||
}
|
||||
}
|
||||
|
||||
logger.debug(
|
||||
{ deletedContacts: isLatest ? oldContacts.size : 0, newContacts },
|
||||
'synced contacts',
|
||||
);
|
||||
|
||||
for (const msg of newMessages) {
|
||||
const jid = msg.key.remoteJid!;
|
||||
const list = assertMessageList(jid);
|
||||
list.upsert(msg, 'prepend');
|
||||
}
|
||||
|
||||
logger.debug({ messages: newMessages.length }, 'synced messages');
|
||||
},
|
||||
);
|
||||
|
||||
ev.on('contacts.upsert', (contacts) => {
|
||||
contactsUpsert(contacts);
|
||||
});
|
||||
|
||||
ev.on('contacts.update', async (updates) => {
|
||||
for (const update of updates) {
|
||||
let contact: Contact;
|
||||
if (contacts[update.id!]) {
|
||||
contact = contacts[update.id!];
|
||||
} else {
|
||||
const contactHashes = await Promise.all(
|
||||
Object.keys(contacts).map(async (contactId) => {
|
||||
const { user } = jidDecode(contactId)!;
|
||||
return [
|
||||
contactId,
|
||||
(await md5(Buffer.from(user + 'WA_ADD_NOTIF', 'utf8')))
|
||||
.toString('base64')
|
||||
.slice(0, 3),
|
||||
];
|
||||
}),
|
||||
);
|
||||
contact =
|
||||
contacts[contactHashes.find(([, b]) => b === update.id)?.[0] || '']; // find contact by attrs.hash, when user is not saved as a contact
|
||||
}
|
||||
|
||||
if (contact) {
|
||||
if (update.imgUrl === 'changed') {
|
||||
contact.imgUrl = socket
|
||||
? await socket?.profilePictureUrl(contact.id)
|
||||
: undefined;
|
||||
} else if (update.imgUrl === 'removed') {
|
||||
delete contact.imgUrl;
|
||||
}
|
||||
} else {
|
||||
return logger.debug(
|
||||
{ update },
|
||||
'got update for non-existant contact',
|
||||
);
|
||||
}
|
||||
|
||||
Object.assign(contacts[contact.id], contact);
|
||||
}
|
||||
});
|
||||
ev.on('chats.upsert', (newChats) => {
|
||||
chats.upsert(...newChats);
|
||||
});
|
||||
ev.on('chats.update', (updates) => {
|
||||
for (let update of updates) {
|
||||
const result = chats.update(update.id!, (chat) => {
|
||||
if (update.unreadCount! > 0) {
|
||||
update = { ...update };
|
||||
update.unreadCount = (chat.unreadCount || 0) + update.unreadCount!;
|
||||
}
|
||||
|
||||
Object.assign(chat, update);
|
||||
});
|
||||
if (!result) {
|
||||
logger.debug({ update }, 'got update for non-existant chat');
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
ev.on('labels.edit', (label: Label) => {
|
||||
if (label.deleted) {
|
||||
return labels.deleteById(label.id);
|
||||
}
|
||||
|
||||
// WhatsApp can store only up to 20 labels
|
||||
if (labels.count() < 20) {
|
||||
return labels.upsertById(label.id, label);
|
||||
}
|
||||
|
||||
logger.error('Labels count exceed');
|
||||
});
|
||||
|
||||
ev.on('labels.association', ({ type, association }) => {
|
||||
switch (type) {
|
||||
case 'add':
|
||||
labelAssociations.upsert(association);
|
||||
break;
|
||||
case 'remove':
|
||||
labelAssociations.delete(association);
|
||||
break;
|
||||
default:
|
||||
console.error(`unknown operation type [${type}]`);
|
||||
}
|
||||
});
|
||||
|
||||
ev.on('presence.update', ({ id, presences: update }) => {
|
||||
presences[id] = presences[id] || {};
|
||||
Object.assign(presences[id], update);
|
||||
});
|
||||
ev.on('chats.delete', (deletions) => {
|
||||
for (const item of deletions) {
|
||||
if (chats.get(item)) {
|
||||
chats.deleteById(item);
|
||||
}
|
||||
}
|
||||
});
|
||||
ev.on('messages.upsert', ({ messages: newMessages, type }) => {
|
||||
switch (type) {
|
||||
case 'append':
|
||||
case 'notify':
|
||||
for (const msg of newMessages) {
|
||||
const jid = jidNormalizedUser(msg.key.remoteJid!);
|
||||
const list = assertMessageList(jid);
|
||||
list.upsert(msg, 'append');
|
||||
|
||||
if (type === 'notify' && !chats.get(jid)) {
|
||||
ev.emit('chats.upsert', [
|
||||
{
|
||||
id: jid,
|
||||
conversationTimestamp: toNumber(msg.messageTimestamp),
|
||||
unreadCount: 1,
|
||||
},
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
});
|
||||
ev.on('messages.update', (updates) => {
|
||||
for (const { update, key } of updates) {
|
||||
const list = assertMessageList(jidNormalizedUser(key.remoteJid!));
|
||||
if (update?.status) {
|
||||
const listStatus = list.get(key.id!)?.status;
|
||||
if (listStatus && update?.status <= listStatus) {
|
||||
logger.debug(
|
||||
{ update, storedStatus: listStatus },
|
||||
'status stored newer then update',
|
||||
);
|
||||
delete update.status;
|
||||
logger.debug({ update }, 'new update object');
|
||||
}
|
||||
}
|
||||
|
||||
const result = list.updateAssign(key.id!, update);
|
||||
if (!result) {
|
||||
logger.debug({ update }, 'got update for non-existent message');
|
||||
}
|
||||
}
|
||||
});
|
||||
ev.on('messages.delete', (item) => {
|
||||
if ('all' in item) {
|
||||
const list = messages[item.jid];
|
||||
list?.clear();
|
||||
} else {
|
||||
const jid = item.keys[0].remoteJid!;
|
||||
const list = messages[jid];
|
||||
if (list) {
|
||||
const idSet = new Set(item.keys.map((k) => k.id));
|
||||
list.filter((m) => !idSet.has(m.key.id));
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
ev.on('groups.update', (updates) => {
|
||||
for (const update of updates) {
|
||||
const id = update.id!;
|
||||
if (groupMetadata[id]) {
|
||||
Object.assign(groupMetadata[id], update);
|
||||
} else {
|
||||
logger.debug(
|
||||
{ update },
|
||||
'got update for non-existant group metadata',
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
ev.on('group-participants.update', ({ id, participants, action }) => {
|
||||
const metadata = groupMetadata[id];
|
||||
if (metadata) {
|
||||
switch (action) {
|
||||
case 'add':
|
||||
metadata.participants.push(
|
||||
...participants.map((id) => ({
|
||||
id,
|
||||
isAdmin: false,
|
||||
isSuperAdmin: false,
|
||||
})),
|
||||
);
|
||||
break;
|
||||
case 'demote':
|
||||
case 'promote':
|
||||
for (const participant of metadata.participants) {
|
||||
if (participants.includes(participant.id)) {
|
||||
participant.isAdmin = action === 'promote';
|
||||
}
|
||||
}
|
||||
|
||||
break;
|
||||
case 'remove':
|
||||
metadata.participants = metadata.participants.filter(
|
||||
(p) => !participants.includes(p.id),
|
||||
);
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
ev.on('message-receipt.update', (updates) => {
|
||||
for (const { key, receipt } of updates) {
|
||||
const obj = messages[key.remoteJid!];
|
||||
const msg = obj?.get(key.id!);
|
||||
if (msg) {
|
||||
updateMessageWithReceipt(msg, receipt);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
ev.on('messages.reaction', (reactions) => {
|
||||
for (const { key, reaction } of reactions) {
|
||||
const obj = messages[key.remoteJid!];
|
||||
const msg = obj?.get(key.id!);
|
||||
if (msg) {
|
||||
updateMessageWithReaction(msg, reaction);
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const toJSON = () => ({
|
||||
chats,
|
||||
contacts,
|
||||
messages,
|
||||
labels,
|
||||
labelAssociations,
|
||||
});
|
||||
|
||||
const fromJSON = (json: {
|
||||
chats: Chat[];
|
||||
contacts: { [id: string]: Contact };
|
||||
messages: { [id: string]: WAMessage[] };
|
||||
labels: { [labelId: string]: Label };
|
||||
labelAssociations: LabelAssociation[];
|
||||
}) => {
|
||||
chats.upsert(...json.chats);
|
||||
labelAssociations.upsert(...(json.labelAssociations || []));
|
||||
contactsUpsert(Object.values(json.contacts));
|
||||
labelsUpsert(Object.values(json.labels || {}));
|
||||
for (const jid in json.messages) {
|
||||
const list = assertMessageList(jid);
|
||||
for (const msg of json.messages[jid]) {
|
||||
list.upsert(proto.WebMessageInfo.fromObject(msg), 'append');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
chats,
|
||||
contacts,
|
||||
messages,
|
||||
groupMetadata,
|
||||
state,
|
||||
presences,
|
||||
setPresences: (obj: any) => {
|
||||
presences = obj;
|
||||
},
|
||||
labels,
|
||||
labelAssociations,
|
||||
bind,
|
||||
/** loads messages from the store, if not found -- uses the legacy connection */
|
||||
loadMessages: async (
|
||||
jid: string,
|
||||
count: number,
|
||||
cursor: WAMessageCursor,
|
||||
) => {
|
||||
const list = assertMessageList(jid);
|
||||
const mode = !cursor || 'before' in cursor ? 'before' : 'after';
|
||||
const cursorKey = !!cursor
|
||||
? 'before' in cursor
|
||||
? cursor.before
|
||||
: cursor.after
|
||||
: undefined;
|
||||
const cursorValue = cursorKey ? list.get(cursorKey.id!) : undefined;
|
||||
|
||||
let messages: WAMessage[];
|
||||
if (list && mode === 'before' && (!cursorKey || cursorValue)) {
|
||||
if (cursorValue) {
|
||||
const msgIdx = list.array.findIndex(
|
||||
(m) => m.key.id === cursorKey?.id,
|
||||
);
|
||||
messages = list.array.slice(0, msgIdx);
|
||||
} else {
|
||||
messages = list.array;
|
||||
}
|
||||
|
||||
const diff = count - messages.length;
|
||||
if (diff < 0) {
|
||||
messages = messages.slice(-count); // get the last X messages
|
||||
}
|
||||
} else {
|
||||
messages = [];
|
||||
}
|
||||
|
||||
return messages;
|
||||
},
|
||||
/**
|
||||
* Get all available labels for profile
|
||||
*
|
||||
* Keep in mind that the list is formed from predefined tags and tags
|
||||
* that were "caught" during their editing.
|
||||
*/
|
||||
getLabels: () => {
|
||||
return labels;
|
||||
},
|
||||
|
||||
/**
|
||||
* Get labels for chat
|
||||
*
|
||||
* @returns Label IDs
|
||||
**/
|
||||
getChatLabels: (chatId: string) => {
|
||||
return labelAssociations.filter((la) => la.chatId === chatId).all();
|
||||
},
|
||||
|
||||
/**
|
||||
* Get labels for message
|
||||
*
|
||||
* @returns Label IDs
|
||||
**/
|
||||
getMessageLabels: (messageId: string) => {
|
||||
const associations = labelAssociations
|
||||
.filter((la: MessageLabelAssociation) => la.messageId === messageId)
|
||||
.all();
|
||||
|
||||
return associations.map(({ labelId }) => labelId);
|
||||
},
|
||||
loadMessage: async (jid: string, id: string) => messages[jid]?.get(id),
|
||||
mostRecentMessage: async (jid: string) => {
|
||||
const message: WAMessage | undefined = messages[jid]?.array.slice(-1)[0];
|
||||
return message;
|
||||
},
|
||||
fetchImageUrl: async (jid: string, sock: WASocket | undefined) => {
|
||||
const contact = contacts[jid];
|
||||
if (!contact) {
|
||||
return sock?.profilePictureUrl(jid);
|
||||
}
|
||||
|
||||
if (typeof contact.imgUrl === 'undefined') {
|
||||
contact.imgUrl = await sock?.profilePictureUrl(jid);
|
||||
}
|
||||
|
||||
return contact.imgUrl;
|
||||
},
|
||||
fetchGroupMetadata: async (jid: string, sock: WASocket | undefined) => {
|
||||
if (!groupMetadata[jid]) {
|
||||
const metadata = await sock?.groupMetadata(jid);
|
||||
if (metadata) {
|
||||
groupMetadata[jid] = metadata;
|
||||
}
|
||||
}
|
||||
|
||||
return groupMetadata[jid];
|
||||
},
|
||||
// fetchBroadcastListInfo: async(jid: string, sock: WASocket | undefined) => {
|
||||
// if(!groupMetadata[jid]) {
|
||||
// const metadata = await sock?.getBroadcastListInfo(jid)
|
||||
// if(metadata) {
|
||||
// groupMetadata[jid] = metadata
|
||||
// }
|
||||
// }
|
||||
|
||||
// return groupMetadata[jid]
|
||||
// },
|
||||
fetchMessageReceipts: async ({ remoteJid, id }: WAMessageKey) => {
|
||||
const list = messages[remoteJid!];
|
||||
const msg = list?.get(id!);
|
||||
return msg?.userReceipt;
|
||||
},
|
||||
toJSON,
|
||||
fromJSON,
|
||||
writeToFile: (path: string) => {
|
||||
writeFileSync(path, JSON.stringify(toJSON()));
|
||||
},
|
||||
readFromFile: (path: string) => {
|
||||
if (existsSync(path)) {
|
||||
logger.debug({ path }, 'reading from file');
|
||||
const jsonStr = readFileSync(path, { encoding: 'utf-8' });
|
||||
const json = JSON.parse(jsonStr);
|
||||
fromJSON(json);
|
||||
}
|
||||
},
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,86 @@
|
||||
function makeOrderedDictionary<T>(idGetter: (item: T) => string) {
|
||||
const array: T[] = [];
|
||||
const dict: { [_: string]: T } = {};
|
||||
|
||||
const get = (id: string): T | undefined => dict[id];
|
||||
|
||||
const update = (item: T) => {
|
||||
const id = idGetter(item);
|
||||
const idx = array.findIndex((i) => idGetter(i) === id);
|
||||
if (idx >= 0) {
|
||||
array[idx] = item;
|
||||
dict[id] = item;
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
const upsert = (item: T, mode: 'append' | 'prepend') => {
|
||||
const id = idGetter(item);
|
||||
if (get(id)) {
|
||||
update(item);
|
||||
} else {
|
||||
if (mode === 'append') {
|
||||
array.push(item);
|
||||
} else {
|
||||
array.splice(0, 0, item);
|
||||
}
|
||||
|
||||
dict[id] = item;
|
||||
}
|
||||
};
|
||||
|
||||
const remove = (item: T) => {
|
||||
const id = idGetter(item);
|
||||
const idx = array.findIndex((i) => idGetter(i) === id);
|
||||
if (idx >= 0) {
|
||||
array.splice(idx, 1);
|
||||
delete dict[id];
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
return {
|
||||
array,
|
||||
get,
|
||||
upsert,
|
||||
update,
|
||||
remove,
|
||||
updateAssign: (id: string, update: Partial<T>) => {
|
||||
const item = get(id);
|
||||
if (item) {
|
||||
Object.assign(item, update);
|
||||
delete dict[id];
|
||||
dict[idGetter(item)] = item;
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
},
|
||||
clear: () => {
|
||||
array.splice(0, array.length);
|
||||
for (const key of Object.keys(dict)) {
|
||||
delete dict[key];
|
||||
}
|
||||
},
|
||||
filter: (contain: (item: T) => boolean) => {
|
||||
let i = 0;
|
||||
while (i < array.length) {
|
||||
if (!contain(array[i])) {
|
||||
delete dict[idGetter(array[i])];
|
||||
array.splice(i, 1);
|
||||
} else {
|
||||
i += 1;
|
||||
}
|
||||
}
|
||||
},
|
||||
toJSON: () => array,
|
||||
fromJSON: (newItems: T[]) => {
|
||||
array.splice(0, array.length, ...newItems);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export default makeOrderedDictionary;
|
||||
@@ -0,0 +1,32 @@
|
||||
export class ObjectRepository<T extends any> {
|
||||
readonly entityMap: Map<string, T>;
|
||||
|
||||
constructor(entities: Record<string, T> = {}) {
|
||||
this.entityMap = new Map(Object.entries(entities));
|
||||
}
|
||||
|
||||
findById(id: string) {
|
||||
return this.entityMap.get(id);
|
||||
}
|
||||
|
||||
findAll() {
|
||||
return Array.from(this.entityMap.values());
|
||||
}
|
||||
|
||||
upsertById(id: string, entity: T) {
|
||||
// @ts-ignore
|
||||
return this.entityMap.set(id, { ...entity });
|
||||
}
|
||||
|
||||
deleteById(id: string) {
|
||||
return this.entityMap.delete(id);
|
||||
}
|
||||
|
||||
count() {
|
||||
return this.entityMap.size;
|
||||
}
|
||||
|
||||
toJSON() {
|
||||
return this.findAll();
|
||||
}
|
||||
}
|
||||
@@ -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}`,
|
||||
);
|
||||
@@ -0,0 +1,117 @@
|
||||
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 NowebLidMapSchema = new Schema(
|
||||
'lid_map',
|
||||
[new Field('id', 'TEXT'), new Field('pn', 'TEXT'), new Field('data', 'TEXT')],
|
||||
[
|
||||
new Index('lid_map_id_index', ['id']),
|
||||
new Index('lid_map_pn_index', ['pn']),
|
||||
],
|
||||
);
|
||||
|
||||
export const NOWEB_STORE_SCHEMA = [
|
||||
NowebContactSchema,
|
||||
NowebChatSchema,
|
||||
NowebGroupsSchema,
|
||||
NowebMessagesSchema,
|
||||
NowebLabelsSchema,
|
||||
NowebLabelAssociationsSchema,
|
||||
NowebLidMapSchema,
|
||||
];
|
||||
|
||||
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")',
|
||||
// Lid Map
|
||||
'CREATE TABLE IF NOT EXISTS lid_map (id TEXT PRIMARY KEY, pn TEXT, data TEXT)',
|
||||
'CREATE UNIQUE INDEX IF NOT EXISTS lid_map_id_index ON lid_map (id)',
|
||||
'CREATE INDEX IF NOT EXISTS lid_map_pn_index ON lid_map (pn)',
|
||||
];
|
||||
@@ -0,0 +1,32 @@
|
||||
import { Chat } from '@adiwajshing/baileys';
|
||||
import { SqlKVRepository } from '@waha/core/storage/sql/SqlKVRepository';
|
||||
import { OverviewFilter } from '@waha/structures/chats.dto';
|
||||
import { PaginationParams } from '@waha/structures/pagination.dto';
|
||||
|
||||
export class SqlChatMethods {
|
||||
constructor(private repository: SqlKVRepository<any>) {}
|
||||
|
||||
async getAllWithMessages(
|
||||
pagination: PaginationParams,
|
||||
broadcast: boolean,
|
||||
filter?: OverviewFilter,
|
||||
): Promise<Chat[]> {
|
||||
// Get chats with conversationTimestamp is not Null
|
||||
let query = this.repository.select().whereNotNull('conversationTimestamp');
|
||||
|
||||
if (!broadcast) {
|
||||
// filter out chat by id if it ends at @newsletter or @broadcast
|
||||
query = query
|
||||
.andWhereNot('id', 'like', '%@broadcast')
|
||||
.andWhereNot('id', 'like', '%@newsletter');
|
||||
}
|
||||
|
||||
// Filter by IDs if provided
|
||||
if (filter?.ids && filter.ids.length > 0) {
|
||||
query = query.whereIn('id', filter.ids);
|
||||
}
|
||||
|
||||
query = this.repository.pagination(query, pagination);
|
||||
return await this.repository.all(query);
|
||||
}
|
||||
}
|
||||
@@ -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,86 @@
|
||||
import { ALL_JID } from '@waha/core/engines/noweb/session.noweb.core';
|
||||
import { SqlKVRepository } from '@waha/core/storage/sql/SqlKVRepository';
|
||||
import { AckToStatus } from '@waha/core/utils/acks';
|
||||
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, value] = this.repository.filterJson(
|
||||
'data',
|
||||
'key.fromMe',
|
||||
filter['filter.fromMe'],
|
||||
);
|
||||
query = query.whereRaw(sql, [value]);
|
||||
}
|
||||
if (filter['filter.ack'] != null) {
|
||||
const status = AckToStatus(filter['filter.ack']);
|
||||
const [sql, value] = this.repository.filterJson('data', 'status', status);
|
||||
query = query.whereRaw(sql, [value]);
|
||||
}
|
||||
query = this.repository.pagination(query, pagination);
|
||||
return this.repository.all(query);
|
||||
}
|
||||
|
||||
async getByJidById(jid: string, id: string): Promise<any> {
|
||||
if (jid === ALL_JID) {
|
||||
return this.repository.getBy({ id: id });
|
||||
}
|
||||
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,7 @@
|
||||
import { Chat } from '@adiwajshing/baileys';
|
||||
import { NowebChatSchema } from '@waha/core/engines/noweb/store/schemas';
|
||||
import { SqlChatMethods } from '@waha/core/engines/noweb/store/sql/SqlChatMethods';
|
||||
import { OverviewFilter } from '@waha/structures/chats.dto';
|
||||
import { PaginationParams } from '@waha/structures/pagination.dto';
|
||||
import { KnexPaginator } from '@waha/utils/Paginator';
|
||||
|
||||
@@ -15,10 +18,19 @@ 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,
|
||||
filter?: OverviewFilter,
|
||||
): Promise<Chat[]> {
|
||||
return this.methods.getAllWithMessages(pagination, broadcast, filter);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
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';
|
||||
|
||||
class Paginator extends KnexPaginator {
|
||||
indexes = ['id'];
|
||||
}
|
||||
|
||||
export class Sqlite3GroupRepository
|
||||
extends NOWEBSqlite3KVRepository<GroupMetadata>
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
import { NowebLidMapSchema } from '@waha/core/engines/noweb/store/schemas';
|
||||
import { LimitOffsetParams } from '@waha/structures/pagination.dto';
|
||||
import { KnexPaginator } from '@waha/utils/Paginator';
|
||||
|
||||
import { INowebLidPNRepository, LidToPN } from '../INowebLidPNRepository';
|
||||
import { NOWEBSqlite3KVRepository } from './NOWEBSqlite3KVRepository';
|
||||
|
||||
export class LidPaginator extends KnexPaginator {
|
||||
indexes = ['id', 'pn'];
|
||||
}
|
||||
|
||||
export class Sqlite3LidPNRepository
|
||||
extends NOWEBSqlite3KVRepository<LidToPN>
|
||||
implements INowebLidPNRepository
|
||||
{
|
||||
get schema() {
|
||||
return NowebLidMapSchema;
|
||||
}
|
||||
|
||||
saveLids(lids: LidToPN[]): Promise<void> {
|
||||
return this.upsertMany(lids);
|
||||
}
|
||||
|
||||
getAllLids(pagination?: LimitOffsetParams): Promise<LidToPN[]> {
|
||||
return this.getAll(pagination);
|
||||
}
|
||||
|
||||
getLidsCount(): Promise<number> {
|
||||
return this.getCount();
|
||||
}
|
||||
|
||||
async findLidByPN(pn: string): Promise<string | null> {
|
||||
const value = await this.getBy({ pn: pn });
|
||||
return value?.id || null;
|
||||
}
|
||||
|
||||
async findPNByLid(lid: string): Promise<string | null> {
|
||||
const value = await this.getBy({ id: lid });
|
||||
return value?.pn || null;
|
||||
}
|
||||
}
|
||||
@@ -1,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,13 +1,14 @@
|
||||
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 { INowebLidPNRepository } from '@waha/core/engines/noweb/store/INowebLidPNRepository';
|
||||
import { Sqlite3GroupRepository } from '@waha/core/engines/noweb/store/sqlite3/Sqlite3GroupRepository';
|
||||
import { Sqlite3LabelAssociationsRepository } from '@waha/core/engines/noweb/store/sqlite3/Sqlite3LabelAssociationsRepository';
|
||||
import { Sqlite3LabelsRepository } from '@waha/core/engines/noweb/store/sqlite3/Sqlite3LabelsRepository';
|
||||
import { Field, Index, Schema } from '@waha/core/storage/sqlite3/Schema';
|
||||
import { Sqlite3LidPNRepository } from '@waha/core/engines/noweb/store/sqlite3/Sqlite3LidPNRepository';
|
||||
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';
|
||||
@@ -43,109 +44,40 @@ 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)',
|
||||
);
|
||||
|
||||
// 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);
|
||||
}
|
||||
|
||||
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,
|
||||
);
|
||||
return new Sqlite3MessagesRepository(this.db);
|
||||
}
|
||||
|
||||
getSchema(name: string) {
|
||||
const schema = this.tables.find((table) => table.name === name);
|
||||
if (!schema) {
|
||||
throw new Error(`Schema not found: ${name}`);
|
||||
}
|
||||
return schema;
|
||||
getLidPNRepository(): INowebLidPNRepository {
|
||||
return new Sqlite3LidPNRepository(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');
|
||||
|
||||
@@ -15,7 +18,10 @@ const AsyncLock = require('async-lock');
|
||||
// https://github.com/nodejs/node/issues/26338
|
||||
// Default pending is 1000, set it to infinity
|
||||
// https://github.com/rogierschouten/async-lock/issues/63
|
||||
const fileLock = new AsyncLock({ maxPending: Infinity });
|
||||
const fileLock = new AsyncLock({
|
||||
maxPending: Infinity,
|
||||
maxExecutionTime: 30_000,
|
||||
});
|
||||
|
||||
/**
|
||||
* stores the full authentication state in a single folder.
|
||||
@@ -29,11 +35,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 +121,8 @@ export const useMultiFileAuthState = async (
|
||||
saveCreds: () => {
|
||||
return writeData(creds, 'creds.json');
|
||||
},
|
||||
close: async () => {
|
||||
return;
|
||||
},
|
||||
};
|
||||
};
|
||||
@@ -1,7 +1,7 @@
|
||||
import { extractMessageContent, proto } from '@adiwajshing/baileys';
|
||||
|
||||
export function extractMediaContent(
|
||||
content: proto.IMessage | null | undefined,
|
||||
content: any | proto.IMessage | null | undefined,
|
||||
) {
|
||||
content = extractMessageContent(content);
|
||||
const mediaContent =
|
||||
|
||||
@@ -0,0 +1,122 @@
|
||||
import { sleep } from '@waha/utils/promiseTimeout';
|
||||
import { EventEmitter } from 'events';
|
||||
import { Key } from 'node-cache';
|
||||
import { Page } from 'puppeteer';
|
||||
|
||||
export interface CallErrorEvent {
|
||||
method: string | symbol;
|
||||
error: unknown;
|
||||
}
|
||||
|
||||
export const PAGE_CALL_ERROR_EVENT = 'page.call.error';
|
||||
|
||||
/**
|
||||
* WPage is a puppeter.Page wrapper that use ALL original methods from Page,
|
||||
* but in case if there were any errors in the call (original) -
|
||||
* it calls the errorCallback with method name, error and arguments
|
||||
*
|
||||
* Find in webjs used functions like "pupPage.{function}"
|
||||
* >>> cd src
|
||||
* >>> grep -rho 'page\.[a-zA-Z0-9_]\+(' . --include="*.js" | sed 's/($//' | sort -u
|
||||
*
|
||||
* Find in webjs used functions like "page.{function}"
|
||||
* >>> cd src
|
||||
* >>> grep -rho 'page\.[a-zA-Z0-9_]\+(' . --include="*.js" | sed 's/($//' | sort -u
|
||||
*
|
||||
*/
|
||||
export class WPage extends EventEmitter {
|
||||
constructor(private page: Page) {
|
||||
super();
|
||||
}
|
||||
|
||||
async evaluate(fn, ...args: any[]) {
|
||||
try {
|
||||
return await this.page.evaluate(fn, ...args);
|
||||
} catch (err) {
|
||||
this.emit(PAGE_CALL_ERROR_EVENT, { method: 'evaluate', error: err });
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
async screenshot(options) {
|
||||
try {
|
||||
return await this.page.screenshot(options);
|
||||
} catch (err) {
|
||||
this.emit(PAGE_CALL_ERROR_EVENT, { method: 'screenshot', error: err });
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
// waitForNavigation
|
||||
async waitForNavigation(options: any) {
|
||||
try {
|
||||
return await this.page.waitForNavigation(options);
|
||||
} catch (err) {
|
||||
this.emit(PAGE_CALL_ERROR_EVENT, {
|
||||
method: 'waitForNavigation',
|
||||
error: err,
|
||||
});
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
// waitForFunction
|
||||
async waitForFunction(fn, options, ...args) {
|
||||
try {
|
||||
return await this.page.waitForFunction(fn, options, ...args);
|
||||
} catch (err) {
|
||||
this.emit(PAGE_CALL_ERROR_EVENT, {
|
||||
method: 'waitForFunction',
|
||||
error: err,
|
||||
});
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
// setRequestInterception
|
||||
async setRequestInterception(value: boolean) {
|
||||
try {
|
||||
return await this.page.setRequestInterception(value);
|
||||
} catch (err) {
|
||||
this.emit(PAGE_CALL_ERROR_EVENT, {
|
||||
method: 'setRequestInterception',
|
||||
error: err,
|
||||
});
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
// exposeFunction
|
||||
async exposeFunction(name: string, fn: (...args: any[]) => any) {
|
||||
try {
|
||||
return await this.page.exposeFunction(name, fn);
|
||||
} catch (err) {
|
||||
this.emit(PAGE_CALL_ERROR_EVENT, {
|
||||
method: 'exposeFunction',
|
||||
error: err,
|
||||
});
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
async reload(options?: any): Promise<any> {
|
||||
try {
|
||||
return await this.page.reload(options);
|
||||
} catch (err) {
|
||||
this.emit(PAGE_CALL_ERROR_EVENT, {
|
||||
method: 'reload',
|
||||
error: err,
|
||||
});
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
on(event: string | symbol, listener: (...args: any[]) => void): this {
|
||||
if (event === PAGE_CALL_ERROR_EVENT) {
|
||||
super.on(event, listener);
|
||||
return this;
|
||||
}
|
||||
this.page.on(event, listener);
|
||||
return this.page as any;
|
||||
}
|
||||
}
|
||||
@@ -1,200 +0,0 @@
|
||||
import { GetChatMessagesFilter } from '@waha/structures/chats.dto';
|
||||
import { Label } from '@waha/structures/labels.dto';
|
||||
import { PaginationParams } from '@waha/structures/pagination.dto';
|
||||
import { TextStatus } from '@waha/structures/status.dto';
|
||||
import * as lodash from 'lodash';
|
||||
import { Client, Events } from 'whatsapp-web.js';
|
||||
import { Message } from 'whatsapp-web.js/src/structures';
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
const { LoadWAHA } = require('./_WAHA.js');
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
const { LoadLodash } = require('./_lodash.js');
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
const { LoadPaginator } = require('./_Paginator.js');
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
const ChatFactory = require('whatsapp-web.js/src/factories/ChatFactory');
|
||||
|
||||
export class WebjsClient extends Client {
|
||||
constructor(options) {
|
||||
super(options);
|
||||
// Wait until it's READY and inject more utils
|
||||
this.on(Events.READY, async () => {
|
||||
await this.pupPage.evaluate(LoadLodash);
|
||||
await this.pupPage.evaluate(LoadPaginator);
|
||||
await this.pupPage.evaluate(LoadWAHA);
|
||||
});
|
||||
}
|
||||
|
||||
async unpair() {
|
||||
await this.pupPage.evaluate(async () => {
|
||||
if (
|
||||
// @ts-ignore
|
||||
window.Store &&
|
||||
// @ts-ignore
|
||||
window.Store.AppState &&
|
||||
// @ts-ignore
|
||||
typeof window.Store.AppState.logout === 'function'
|
||||
) {
|
||||
// @ts-ignore
|
||||
await window.Store.AppState.logout();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async createLabel(name: string, color: number): Promise<number> {
|
||||
const labelId: number = await this.pupPage.evaluate(
|
||||
async (name, color) => {
|
||||
// @ts-ignore
|
||||
return await window.WAHA.WAWebBizLabelEditingAction.labelAddAction(
|
||||
name,
|
||||
color,
|
||||
);
|
||||
},
|
||||
name,
|
||||
color,
|
||||
);
|
||||
return labelId;
|
||||
}
|
||||
|
||||
async deleteLabel(label: Label) {
|
||||
return await this.pupPage.evaluate(async (label) => {
|
||||
// @ts-ignore
|
||||
return await window.WAHA.WAWebBizLabelEditingAction.labelDeleteAction(
|
||||
label.id,
|
||||
label.name,
|
||||
label.color,
|
||||
);
|
||||
}, label);
|
||||
}
|
||||
|
||||
async updateLabel(label: Label) {
|
||||
return await this.pupPage.evaluate(async (label) => {
|
||||
// @ts-ignore
|
||||
return await window.WAHA.WAWebBizLabelEditingAction.labelEditAction(
|
||||
label.id,
|
||||
label.name,
|
||||
undefined, // predefinedId
|
||||
label.color,
|
||||
);
|
||||
}, label);
|
||||
}
|
||||
|
||||
async getChats(pagination?: PaginationParams) {
|
||||
if (lodash.isEmpty(pagination)) {
|
||||
return await super.getChats();
|
||||
}
|
||||
|
||||
// Get paginated chats
|
||||
pagination.limit ||= Infinity;
|
||||
pagination.offset ||= 0;
|
||||
|
||||
const chats = await this.pupPage.evaluate(async (pagination) => {
|
||||
// @ts-ignore
|
||||
return await window.WAHA.getChats(pagination);
|
||||
}, pagination);
|
||||
|
||||
return chats.map((chat) => ChatFactory.create(this, chat));
|
||||
}
|
||||
|
||||
async sendTextStatus(status: TextStatus) {
|
||||
// Convert from hex to number
|
||||
const waColor = 'FF' + status.backgroundColor.replace('#', '');
|
||||
const color = parseInt(waColor, 16);
|
||||
|
||||
const textStatus = {
|
||||
text: status.text,
|
||||
color: color,
|
||||
font: status.font,
|
||||
};
|
||||
const sentMsg = await this.pupPage.evaluate(async (status) => {
|
||||
// @ts-ignore
|
||||
await window.Store.SendStatus.sendStatusTextMsgAction(status);
|
||||
// @ts-ignore
|
||||
const meUser = window.Store.User.getMeUser();
|
||||
// @ts-ignore
|
||||
const myStatus = window.Store.Status.getModelsArray().findLast(
|
||||
(x) => x.id == meUser,
|
||||
);
|
||||
if (!myStatus) {
|
||||
return undefined;
|
||||
}
|
||||
// @ts-ignore
|
||||
const msg = myStatus.msgs.last();
|
||||
// @ts-ignore
|
||||
return msg ? window.WWebJS.getMessageModel(msg) : undefined;
|
||||
}, textStatus);
|
||||
|
||||
return sentMsg ? new Message(this, sentMsg) : undefined;
|
||||
}
|
||||
|
||||
async getMessages(
|
||||
chatId: string,
|
||||
filter: GetChatMessagesFilter,
|
||||
pagination: PaginationParams,
|
||||
) {
|
||||
const messages = await this.pupPage.evaluate(
|
||||
async (chatId, filter, pagination) => {
|
||||
pagination.limit ||= Infinity;
|
||||
pagination.offset ||= 0;
|
||||
|
||||
const msgFilter = (m) => {
|
||||
if (m.isNotification) {
|
||||
return false;
|
||||
}
|
||||
if (
|
||||
filter['filter.fromMe'] != null &&
|
||||
m.id.fromMe !== filter['filter.fromMe']
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
if (
|
||||
filter['filter.timestamp.gte'] != null &&
|
||||
m.t < filter['filter.timestamp.gte']
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
if (
|
||||
filter['filter.timestamp.lte'] != null &&
|
||||
m.t > filter['filter.timestamp.lte']
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
// @ts-ignore
|
||||
const chat = await window.WWebJS.getChat(chatId, { getAsModel: false });
|
||||
let msgs = chat.msgs.getModelsArray().filter(msgFilter);
|
||||
|
||||
while (msgs.length < pagination.limit + pagination.offset) {
|
||||
const loadedMessages =
|
||||
// @ts-ignore
|
||||
await window.Store.ConversationMsgs.loadEarlierMsgs(chat);
|
||||
if (!loadedMessages || !loadedMessages.length) break;
|
||||
msgs = [...loadedMessages.filter(msgFilter), ...msgs];
|
||||
}
|
||||
|
||||
if (msgs.length > pagination.limit + pagination.offset) {
|
||||
// sort by t - new first
|
||||
msgs = msgs.sort((a, b) => b.t - a.t);
|
||||
msgs = msgs.slice(
|
||||
pagination.offset,
|
||||
pagination.limit + pagination.offset,
|
||||
);
|
||||
}
|
||||
|
||||
// @ts-ignore
|
||||
return msgs.map((m) => window.WWebJS.getMessageModel(m));
|
||||
},
|
||||
chatId,
|
||||
filter,
|
||||
pagination,
|
||||
);
|
||||
|
||||
return messages.map((m) => new Message(this, m));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,416 @@
|
||||
import { WebJSPresence } from '@waha/core/engines/webjs/types';
|
||||
import { GetChatMessagesFilter } from '@waha/structures/chats.dto';
|
||||
import { Label } from '@waha/structures/labels.dto';
|
||||
import { LidToPhoneNumber } from '@waha/structures/lids.dto';
|
||||
import { PaginationParams } from '@waha/structures/pagination.dto';
|
||||
import { TextStatus } from '@waha/structures/status.dto';
|
||||
import { sleep } from '@waha/utils/promiseTimeout';
|
||||
import { EventEmitter } from 'events';
|
||||
import * as lodash from 'lodash';
|
||||
import { Page } from 'puppeteer';
|
||||
import { Client, Events } from 'whatsapp-web.js';
|
||||
import { Message } from 'whatsapp-web.js/src/structures';
|
||||
import { exposeFunctionIfAbsent } from 'whatsapp-web.js/src/util/Puppeter';
|
||||
|
||||
import { CallErrorEvent, PAGE_CALL_ERROR_EVENT, WPage } from './WPage';
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
const { LoadWAHA } = require('./_WAHA.js');
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
const { LoadLodash } = require('./_lodash.js');
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
const { LoadPaginator } = require('./_Paginator.js');
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
const ChatFactory = require('whatsapp-web.js/src/factories/ChatFactory');
|
||||
|
||||
export class WebjsClientCore extends Client {
|
||||
public events = new EventEmitter();
|
||||
private wpage: WPage = null;
|
||||
|
||||
constructor(options) {
|
||||
super(options);
|
||||
// Wait until it's READY and inject more utils
|
||||
this.on(Events.READY, async () => {
|
||||
await this.attachCustomEventListeners();
|
||||
await this.injectWaha();
|
||||
});
|
||||
}
|
||||
|
||||
async initialize() {
|
||||
const result = await super.initialize();
|
||||
if (this.pupPage && !(this.pupPage instanceof WPage)) {
|
||||
this.wpage = new WPage(this.pupPage);
|
||||
this.wpage.on(PAGE_CALL_ERROR_EVENT as any, (event: CallErrorEvent) => {
|
||||
this.events.emit(PAGE_CALL_ERROR_EVENT as any, event);
|
||||
});
|
||||
this.pupPage = this.wpage as any as Page;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
async injectWaha() {
|
||||
await this.pupPage.evaluate(LoadLodash);
|
||||
await this.pupPage.evaluate(LoadPaginator);
|
||||
await this.pupPage.evaluate(LoadWAHA);
|
||||
}
|
||||
|
||||
/**
|
||||
* @result indicating whether the UX fresh look was successfully hidden.
|
||||
*/
|
||||
hideUXFreshLook(): Promise<boolean> {
|
||||
return this.pupPage.evaluate(() => {
|
||||
const WAWebUserPrefsUiRefresh = window.require('WAWebUserPrefsUiRefresh');
|
||||
if (!WAWebUserPrefsUiRefresh) {
|
||||
return false;
|
||||
}
|
||||
if (WAWebUserPrefsUiRefresh.getUiRefreshNuxAcked()) {
|
||||
return false;
|
||||
}
|
||||
WAWebUserPrefsUiRefresh.incrementNuxViewCount();
|
||||
WAWebUserPrefsUiRefresh.setUiRefreshNuxAcked(true);
|
||||
const WAWebModalManager = window.require('WAWebModalManager');
|
||||
WAWebModalManager.ModalManager.close();
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
async attachCustomEventListeners() {
|
||||
await exposeFunctionIfAbsent(
|
||||
this.pupPage,
|
||||
'onNewMessageId',
|
||||
(messageId: string) => {
|
||||
this.events.emit('message.id', { id: messageId });
|
||||
return;
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
async destroy() {
|
||||
this.events.removeAllListeners();
|
||||
this.wpage?.removeAllListeners();
|
||||
await super.destroy();
|
||||
}
|
||||
|
||||
async setPushName(name: string) {
|
||||
await this.pupPage.evaluate(async (pushName) => {
|
||||
return await window['WAHA'].WAWebSetPushnameConnAction.setPushname(
|
||||
pushName,
|
||||
);
|
||||
}, name);
|
||||
if (this.info) {
|
||||
this.info.pushname = name;
|
||||
}
|
||||
}
|
||||
|
||||
async unpair() {
|
||||
await this.pupPage.evaluate(async () => {
|
||||
if (
|
||||
// @ts-ignore
|
||||
window.Store &&
|
||||
// @ts-ignore
|
||||
window.Store.AppState &&
|
||||
// @ts-ignore
|
||||
typeof window.Store.AppState.logout === 'function'
|
||||
) {
|
||||
// @ts-ignore
|
||||
await window.Store.AppState.logout();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async createLabel(name: string, color: number): Promise<number> {
|
||||
const labelId: number = (await this.pupPage.evaluate(
|
||||
async (name, color) => {
|
||||
// @ts-ignore
|
||||
return await window.WAHA.WAWebBizLabelEditingAction.labelAddAction(
|
||||
name,
|
||||
color,
|
||||
);
|
||||
},
|
||||
name,
|
||||
color,
|
||||
)) as any;
|
||||
return labelId;
|
||||
}
|
||||
|
||||
async deleteLabel(label: Label) {
|
||||
return await this.pupPage.evaluate(async (label) => {
|
||||
// @ts-ignore
|
||||
return await window.WAHA.WAWebBizLabelEditingAction.labelDeleteAction(
|
||||
label.id,
|
||||
label.name,
|
||||
label.color,
|
||||
);
|
||||
}, label);
|
||||
}
|
||||
|
||||
async updateLabel(label: Label) {
|
||||
return await this.pupPage.evaluate(async (label) => {
|
||||
// @ts-ignore
|
||||
return await window.WAHA.WAWebBizLabelEditingAction.labelEditAction(
|
||||
label.id,
|
||||
label.name,
|
||||
undefined, // predefinedId
|
||||
label.color,
|
||||
);
|
||||
}, label);
|
||||
}
|
||||
|
||||
async getChats(pagination?: PaginationParams, filter?: { ids?: string[] }) {
|
||||
if (lodash.isEmpty(pagination)) {
|
||||
return await super.getChats();
|
||||
}
|
||||
|
||||
// Get paginated chats
|
||||
pagination.limit ||= Infinity;
|
||||
pagination.offset ||= 0;
|
||||
|
||||
const chats = await this.pupPage.evaluate(
|
||||
async (pagination, filter) => {
|
||||
// @ts-ignore
|
||||
return await window.WAHA.getChats(pagination, filter);
|
||||
},
|
||||
pagination,
|
||||
filter,
|
||||
);
|
||||
|
||||
return chats.map((chat) => ChatFactory.create(this, chat));
|
||||
}
|
||||
|
||||
async sendTextStatus(status: TextStatus) {
|
||||
// Convert from hex to number
|
||||
const waColor = 'FF' + status.backgroundColor.replace('#', '');
|
||||
const color = parseInt(waColor, 16);
|
||||
|
||||
const textStatus = {
|
||||
text: status.text,
|
||||
color: color,
|
||||
font: status.font,
|
||||
};
|
||||
const sentMsg = await this.pupPage.evaluate(async (status) => {
|
||||
// @ts-ignore
|
||||
await window.Store.SendStatus.sendStatusTextMsgAction(status);
|
||||
// @ts-ignore
|
||||
const meUser = window.Store.User.getMeUser();
|
||||
// @ts-ignore
|
||||
const myStatus = window.Store.Status.getModelsArray().findLast(
|
||||
(x) => x.id == meUser,
|
||||
);
|
||||
if (!myStatus) {
|
||||
return undefined;
|
||||
}
|
||||
// @ts-ignore
|
||||
const msg = myStatus.msgs.last();
|
||||
// @ts-ignore
|
||||
return msg ? window.WWebJS.getMessageModel(msg) : undefined;
|
||||
}, textStatus);
|
||||
|
||||
return sentMsg ? new Message(this, sentMsg) : undefined;
|
||||
}
|
||||
|
||||
async getMessages(
|
||||
chatId: string,
|
||||
filter: GetChatMessagesFilter,
|
||||
pagination: PaginationParams,
|
||||
) {
|
||||
const messages = await this.pupPage.evaluate(
|
||||
async (chatId, filter, pagination) => {
|
||||
pagination.limit ||= Infinity;
|
||||
pagination.offset ||= 0;
|
||||
|
||||
const msgFilter = (m) => {
|
||||
if (m.isNotification) {
|
||||
return false;
|
||||
}
|
||||
if (
|
||||
filter['filter.fromMe'] != null &&
|
||||
m.id.fromMe !== filter['filter.fromMe']
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
if (
|
||||
filter['filter.timestamp.gte'] != null &&
|
||||
m.t < filter['filter.timestamp.gte']
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
if (
|
||||
filter['filter.timestamp.lte'] != null &&
|
||||
m.t > filter['filter.timestamp.lte']
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
if (filter['filter.ack'] != null && m.ack !== filter['filter.ack']) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
// @ts-ignore
|
||||
const chat = await window.WWebJS.getChat(chatId, { getAsModel: false });
|
||||
let msgs = chat.msgs.getModelsArray().filter(msgFilter);
|
||||
|
||||
while (msgs.length < pagination.limit + pagination.offset) {
|
||||
const loadedMessages =
|
||||
// @ts-ignore
|
||||
await window.Store.ConversationMsgs.loadEarlierMsgs(chat);
|
||||
if (!loadedMessages || loadedMessages.length == 0) break;
|
||||
|
||||
msgs = [...loadedMessages.filter(msgFilter), ...msgs];
|
||||
msgs = msgs.sort((a, b) => b.t - a.t);
|
||||
|
||||
// Check if the earliest message is already outside the timerange filter
|
||||
const earliest = msgs[msgs.length - 1];
|
||||
if (earliest.t < (filter['filter.timestamp.gte'] || Infinity)) {
|
||||
// Add only messages that pass the filter and stop loading more
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (msgs.length > pagination.limit + pagination.offset) {
|
||||
// sort by t - new first
|
||||
msgs = msgs.sort((a, b) => b.t - a.t);
|
||||
msgs = msgs.slice(
|
||||
pagination.offset,
|
||||
pagination.limit + pagination.offset,
|
||||
);
|
||||
}
|
||||
|
||||
// @ts-ignore
|
||||
return msgs.map((m) => window.WWebJS.getMessageModel(m));
|
||||
},
|
||||
chatId,
|
||||
filter,
|
||||
pagination,
|
||||
);
|
||||
|
||||
return messages.map((m) => new Message(this, m));
|
||||
}
|
||||
|
||||
public async getAllLids(
|
||||
pagination: PaginationParams,
|
||||
): Promise<Array<LidToPhoneNumber>> {
|
||||
const lids: Array<LidToPhoneNumber> = (await this.pupPage.evaluate(
|
||||
async (pagination) => {
|
||||
pagination.limit ||= Infinity;
|
||||
pagination.offset ||= 0;
|
||||
pagination.sortBy ||= 'lid';
|
||||
|
||||
// @ts-ignore
|
||||
const WAWebApiContact = window.Store.LidUtils;
|
||||
|
||||
await WAWebApiContact.warmUpAllLidPnMappings();
|
||||
const lidMap = WAWebApiContact.lidPnCache['$1'];
|
||||
const values = Array.from(lidMap.values());
|
||||
const result = values.map((map) => {
|
||||
return {
|
||||
// @ts-ignore
|
||||
lid: map.lid._serialized,
|
||||
// @ts-ignore
|
||||
pn: map.phoneNumber._serialized,
|
||||
};
|
||||
});
|
||||
// @ts-ignore
|
||||
const paginator = new window.Paginator(pagination);
|
||||
const page = paginator.apply(result);
|
||||
return page;
|
||||
},
|
||||
pagination,
|
||||
)) as any;
|
||||
return lids;
|
||||
}
|
||||
|
||||
public async getLidsCount(): Promise<number> {
|
||||
const count: number = (await this.pupPage.evaluate(async () => {
|
||||
// @ts-ignore
|
||||
const WAWebApiContact = window.Store.LidUtils;
|
||||
|
||||
await WAWebApiContact.warmUpAllLidPnMappings();
|
||||
const lidMap = WAWebApiContact.lidPnCache['$1'];
|
||||
return lidMap.size;
|
||||
})) as any;
|
||||
return count;
|
||||
}
|
||||
|
||||
public async findPNByLid(lid: string): Promise<string> {
|
||||
const pn = await this.pupPage.evaluate(async (lid) => {
|
||||
// @ts-ignore
|
||||
const WAWebApiContact = window.Store.LidUtils;
|
||||
// @ts-ignore
|
||||
const WAWebWidFactory = window.Store.WidFactory;
|
||||
|
||||
const wid = WAWebWidFactory.createWid(lid);
|
||||
const result = WAWebApiContact.getPhoneNumber(wid);
|
||||
return result ? result._serialized : null;
|
||||
}, lid);
|
||||
return pn;
|
||||
}
|
||||
|
||||
public async findLIDByPhoneNumber(phoneNumber: string): Promise<string> {
|
||||
const lid: string = (await this.pupPage.evaluate(async (pn) => {
|
||||
// @ts-ignore
|
||||
const WAWebApiContact = window.Store.LidUtils;
|
||||
// @ts-ignore
|
||||
const WAWebWidFactory = window.Store.WidFactory;
|
||||
|
||||
const wid = WAWebWidFactory.createWid(pn);
|
||||
const result = WAWebApiContact.getCurrentLid(wid);
|
||||
return result ? result._serialized : null;
|
||||
}, phoneNumber)) as any;
|
||||
return lid;
|
||||
}
|
||||
|
||||
/**
|
||||
* Presences methods
|
||||
*/
|
||||
public async subscribePresence(chatId: string): Promise<void> {
|
||||
await this.pupPage.evaluate(async (chatId) => {
|
||||
const d = require;
|
||||
const WidFactory = d('WAWebWidFactory');
|
||||
|
||||
const wid = WidFactory.createWidFromWidLike(chatId);
|
||||
const chat = d('WAWebChatCollection').ChatCollection.get(wid);
|
||||
const tc = chat == null ? void 0 : chat.getTcToken();
|
||||
await d('WAWebContactPresenceBridge').subscribePresence(wid, tc);
|
||||
}, chatId);
|
||||
}
|
||||
|
||||
private async getCurrentPresence(chatId: string): Promise<WebJSPresence[]> {
|
||||
const result = await this.pupPage.evaluate(async (chatId) => {
|
||||
const d = require;
|
||||
const WidFactory = d('WAWebWidFactory');
|
||||
const PresenceCollection = d(
|
||||
'WAWebPresenceCollection',
|
||||
).PresenceCollection;
|
||||
const wid = WidFactory.createWidFromWidLike(chatId);
|
||||
const presence = PresenceCollection.get(wid);
|
||||
if (!presence) {
|
||||
return [];
|
||||
}
|
||||
let chatstates = [];
|
||||
if (chatId.endsWith('@c.us')) {
|
||||
chatstates = [presence.chatstate];
|
||||
} else {
|
||||
chatstates = presence.chatstates.getModelsArray();
|
||||
}
|
||||
return chatstates.map((chatstate) => {
|
||||
return {
|
||||
participant: chatstate.id._serialized,
|
||||
lastSeen: chatstate.t,
|
||||
state: chatstate.type,
|
||||
};
|
||||
});
|
||||
}, chatId);
|
||||
return result;
|
||||
}
|
||||
|
||||
public async getPresence(chatId: string): Promise<WebJSPresence[]> {
|
||||
await this.sendPresenceAvailable();
|
||||
await this.subscribePresence(chatId);
|
||||
await sleep(3_000);
|
||||
return await this.getCurrentPresence(chatId);
|
||||
}
|
||||
}
|
||||
@@ -17,7 +17,7 @@ exports.LoadPaginator = () => {
|
||||
}
|
||||
return window.lodash.orderBy(
|
||||
data,
|
||||
[this.pagination.sortBy],
|
||||
[this.NullLast(this.pagination.sortBy)],
|
||||
[this.pagination.sortOrder || 'asc'],
|
||||
);
|
||||
}
|
||||
@@ -30,6 +30,16 @@ exports.LoadPaginator = () => {
|
||||
const limit = this.pagination.limit || Infinity;
|
||||
return data.slice(offset, offset + limit);
|
||||
}
|
||||
|
||||
NullLast(field) {
|
||||
return (item) => {
|
||||
const value = item?.[field];
|
||||
if (value == null) {
|
||||
return -Infinity;
|
||||
}
|
||||
return value;
|
||||
};
|
||||
}
|
||||
}
|
||||
window.Paginator = Paginator;
|
||||
};
|
||||
@@ -4,11 +4,27 @@ exports.LoadWAHA = () => {
|
||||
'WAWebBizLabelEditingAction',
|
||||
);
|
||||
|
||||
window.WAHA.getChats = async (pagination) => {
|
||||
window.WAHA.getChats = async (pagination, filter) => {
|
||||
let chats = window.Store.Chat.getModelsArray().slice();
|
||||
|
||||
// Filter chats by IDs if filter is provided
|
||||
if (filter && filter.ids && filter.ids.length > 0) {
|
||||
chats = chats.filter((chat) => filter.ids.includes(chat.id._serialized));
|
||||
}
|
||||
|
||||
const paginator = new window.Paginator(pagination);
|
||||
chats = paginator.apply(chats);
|
||||
const chatPromises = chats.map((chat) => window.WWebJS.getChatModel(chat));
|
||||
return await Promise.all(chatPromises);
|
||||
};
|
||||
|
||||
// Set push name
|
||||
window.WAHA.WAWebSetPushnameConnAction = window.require(
|
||||
'WAWebSetPushnameConnAction',
|
||||
);
|
||||
|
||||
// Get my status
|
||||
window.WAHA.WAWebTextStatusCollection = window.require(
|
||||
'WAWebTextStatusCollection',
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,163 @@
|
||||
import {
|
||||
areJidsSameUser,
|
||||
BinaryNode,
|
||||
getBinaryNodeChildren,
|
||||
getStatusFromReceiptType,
|
||||
isJidGroup,
|
||||
isJidStatusBroadcast,
|
||||
jidEncode,
|
||||
jidNormalizedUser,
|
||||
proto,
|
||||
} from '@adiwajshing/baileys';
|
||||
|
||||
export interface ReceiptEvent {
|
||||
key: proto.IMessageKey;
|
||||
participant?: string;
|
||||
messageIds: string[];
|
||||
status: number;
|
||||
_node: any;
|
||||
}
|
||||
|
||||
interface Me {
|
||||
id: string;
|
||||
lid?: string;
|
||||
}
|
||||
|
||||
export function jid(field: any) {
|
||||
if (!field) {
|
||||
return field;
|
||||
}
|
||||
const data = field['$1'];
|
||||
if (!data) {
|
||||
return data;
|
||||
}
|
||||
|
||||
let server = data.server;
|
||||
if (!server) {
|
||||
server =
|
||||
data.domainType === 0 || data.domainType === 128
|
||||
? 's.whatsapp.net'
|
||||
: 'lid';
|
||||
}
|
||||
return jidEncode(data.user, server, data.device);
|
||||
}
|
||||
|
||||
export function TagReceiptNodeToReceiptEvent(
|
||||
node: BinaryNode,
|
||||
me: Me,
|
||||
): ReceiptEvent[] {
|
||||
const { attrs, content } = node;
|
||||
const status = getStatusFromReceiptType(attrs.type);
|
||||
if (status == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const from = jidNormalizedUser(jid(attrs.from));
|
||||
const participant = jidNormalizedUser(jid(attrs.participant));
|
||||
const recipient = jidNormalizedUser(jid(attrs.recipient));
|
||||
|
||||
const isLid = from.includes('lid');
|
||||
const isNodeFromMe = areJidsSameUser(
|
||||
participant || from,
|
||||
isLid ? me?.lid : me?.id,
|
||||
);
|
||||
const remoteJid = !isNodeFromMe || isJidGroup(from) ? from : recipient;
|
||||
const fromMe = !recipient || (attrs.type === 'retry' && isNodeFromMe);
|
||||
|
||||
// basically, we only want to know when a message from us has been delivered to/read by the other person
|
||||
// or another device of ours has read some messages
|
||||
if (status < proto.WebMessageInfo.Status.SERVER_ACK && isNodeFromMe) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const key: proto.IMessageKey = {
|
||||
remoteJid: remoteJid,
|
||||
id: '',
|
||||
fromMe: fromMe,
|
||||
};
|
||||
|
||||
const ids = [attrs.id];
|
||||
if (Array.isArray(content)) {
|
||||
const items = getBinaryNodeChildren(content[0], 'item');
|
||||
ids.push(...items.map((i) => i.attrs.id));
|
||||
}
|
||||
|
||||
if (isJidGroup(remoteJid) || isJidStatusBroadcast(remoteJid)) {
|
||||
if (participant) {
|
||||
key.participant = fromMe ? (isLid ? me.lid : me.id) : recipient;
|
||||
const eventParticipant = fromMe ? participant : isLid ? me.lid : me.id;
|
||||
return [
|
||||
{
|
||||
key: key,
|
||||
messageIds: ids,
|
||||
status: status as any,
|
||||
participant: eventParticipant,
|
||||
_node: node,
|
||||
},
|
||||
];
|
||||
} else {
|
||||
// Handle grouped receipts
|
||||
return handleGroupedReceipts(node, key, status, fromMe, isLid, me);
|
||||
}
|
||||
}
|
||||
|
||||
return [
|
||||
{
|
||||
key: key,
|
||||
messageIds: ids,
|
||||
status: status as any,
|
||||
_node: node,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
function handleGroupedReceipts(
|
||||
node: BinaryNode,
|
||||
key: proto.IMessageKey,
|
||||
status: number,
|
||||
fromMe: boolean,
|
||||
isLid: boolean,
|
||||
me: Me,
|
||||
): ReceiptEvent[] | null {
|
||||
const { content } = node;
|
||||
if (!Array.isArray(content)) {
|
||||
return [];
|
||||
}
|
||||
const participantsTags = content.filter((c) => c.tag === 'participants');
|
||||
if (participantsTags.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const receiptEvents: ReceiptEvent[] = [];
|
||||
|
||||
for (const participants of participantsTags) {
|
||||
const participantKey = participants.attrs?.key;
|
||||
if (!participantKey) continue;
|
||||
|
||||
const users = getBinaryNodeChildren(participants, 'user');
|
||||
for (const user of users) {
|
||||
const userAttrs = user.attrs;
|
||||
if (!userAttrs) continue;
|
||||
|
||||
const userJid = jidNormalizedUser(jid(userAttrs.jid));
|
||||
if (!userJid) continue;
|
||||
|
||||
key.participant = fromMe ? (isLid ? me.lid : me.id) : userJid;
|
||||
const eventParticipant = fromMe ? userJid : isLid ? me.lid : me.id;
|
||||
const receiptEvent: ReceiptEvent = {
|
||||
key: {
|
||||
...key,
|
||||
id: participantKey,
|
||||
},
|
||||
messageIds: [participantKey],
|
||||
status: status as any,
|
||||
participant: eventParticipant,
|
||||
_node: node,
|
||||
};
|
||||
|
||||
receiptEvents.push(receiptEvent);
|
||||
}
|
||||
}
|
||||
|
||||
return receiptEvents;
|
||||
}
|
||||
@@ -0,0 +1,154 @@
|
||||
import { WebjsClientCore } from '@waha/core/engines/webjs/WebjsClientCore';
|
||||
import {
|
||||
GroupId,
|
||||
GroupInfo,
|
||||
GroupParticipant,
|
||||
GroupParticipantRole,
|
||||
} from '@waha/structures/groups.dto';
|
||||
import {
|
||||
GroupParticipantType,
|
||||
GroupV2JoinEvent,
|
||||
GroupV2LeaveEvent,
|
||||
GroupV2ParticipantsEvent,
|
||||
GroupV2UpdateEvent,
|
||||
} from '@waha/structures/groups.events.dto';
|
||||
import {
|
||||
GroupChat,
|
||||
GroupNotification,
|
||||
GroupNotificationTypes,
|
||||
GroupParticipant as WEBJSGroupParticipant,
|
||||
} from 'whatsapp-web.js';
|
||||
|
||||
function ToGroupInfo(
|
||||
group: GroupChat,
|
||||
invite: string,
|
||||
participants = [],
|
||||
): GroupInfo {
|
||||
// @ts-ignore
|
||||
const groupMetadata = group.groupMetadata;
|
||||
const info: GroupInfo = {
|
||||
// @ts-ignore
|
||||
id: group.id._serialized,
|
||||
subject: group.name,
|
||||
description: group.description,
|
||||
invite: invite,
|
||||
membersCanAddNewMember: groupMetadata.restrict,
|
||||
membersCanSendMessages: groupMetadata.announce,
|
||||
newMembersApprovalRequired: groupMetadata.membershipApprovalMode,
|
||||
participants: participants,
|
||||
};
|
||||
return info;
|
||||
}
|
||||
|
||||
export async function ToGroupV2JoinEvent(
|
||||
client: WebjsClientCore,
|
||||
me: string,
|
||||
notification: GroupNotification,
|
||||
): Promise<GroupV2JoinEvent | null> {
|
||||
if (!notification.recipientIds.includes(me)) {
|
||||
return null;
|
||||
}
|
||||
// @ts-ignore
|
||||
const group: GroupChat = await client.getChatById(notification.id.remote);
|
||||
const invite = await group.getInviteCode();
|
||||
const participants = getParticipants(group.participants);
|
||||
const info: GroupInfo = ToGroupInfo(group, invite, participants);
|
||||
return {
|
||||
timestamp: notification.timestamp,
|
||||
group: info,
|
||||
_data: notification,
|
||||
};
|
||||
}
|
||||
|
||||
function getParticipants(
|
||||
participants: WEBJSGroupParticipant[],
|
||||
): GroupParticipant[] {
|
||||
return participants.map((participant) => {
|
||||
let role: GroupParticipantRole = GroupParticipantRole.PARTICIPANT;
|
||||
if (participant.isSuperAdmin) {
|
||||
role = GroupParticipantRole.SUPERADMIN;
|
||||
} else if (participant.isAdmin) {
|
||||
role = GroupParticipantRole.ADMIN;
|
||||
}
|
||||
|
||||
return {
|
||||
id: participant.id._serialized,
|
||||
role: role,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
export function ToGroupV2LeaveEvent(
|
||||
me: string,
|
||||
notification: GroupNotification,
|
||||
): GroupV2LeaveEvent | null {
|
||||
if (!notification.recipientIds.includes(me)) {
|
||||
return null;
|
||||
}
|
||||
const group: GroupId = {
|
||||
// @ts-ignore
|
||||
id: notification.id.remote,
|
||||
};
|
||||
return {
|
||||
timestamp: notification.timestamp,
|
||||
group: group,
|
||||
_data: notification,
|
||||
};
|
||||
}
|
||||
|
||||
export async function ToGroupV2UpdateEvent(
|
||||
client: WebjsClientCore,
|
||||
notification: GroupNotification,
|
||||
): Promise<GroupV2UpdateEvent> {
|
||||
// @ts-ignore
|
||||
const group: GroupChat = await client.getChatById(notification.id.remote);
|
||||
const invite = await group.getInviteCode();
|
||||
const info: GroupInfo = ToGroupInfo(group, invite, undefined);
|
||||
return {
|
||||
group: info,
|
||||
timestamp: notification.timestamp,
|
||||
_data: notification,
|
||||
};
|
||||
}
|
||||
|
||||
export function ToGroupV2ParticipantsEvent(
|
||||
notification: GroupNotification,
|
||||
): GroupV2ParticipantsEvent | null {
|
||||
let type: GroupParticipantType;
|
||||
let role: GroupParticipantRole;
|
||||
if (['add', 'invite', 'linked_group_join'].includes(notification.type)) {
|
||||
type = GroupParticipantType.JOIN;
|
||||
role = GroupParticipantRole.PARTICIPANT;
|
||||
} else if (['remove', 'leave'].includes(notification.type)) {
|
||||
type = GroupParticipantType.LEAVE;
|
||||
role = GroupParticipantRole.LEFT;
|
||||
} else if (['promote'].includes(notification.type)) {
|
||||
type = GroupParticipantType.PROMOTE;
|
||||
role = GroupParticipantRole.ADMIN;
|
||||
} else if (['demote'].includes(notification.type)) {
|
||||
type = GroupParticipantType.DEMOTE;
|
||||
role = GroupParticipantRole.PARTICIPANT;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
|
||||
const participants: GroupParticipant[] = notification.recipientIds.map(
|
||||
(id) => {
|
||||
return {
|
||||
id: id,
|
||||
role: role,
|
||||
};
|
||||
},
|
||||
);
|
||||
const group: GroupId = {
|
||||
// @ts-ignore
|
||||
id: notification.id.remote,
|
||||
};
|
||||
return {
|
||||
group: group,
|
||||
type: type,
|
||||
timestamp: notification.timestamp,
|
||||
participants: participants,
|
||||
_data: notification,
|
||||
};
|
||||
}
|
||||
Loaded 100 of 172 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user