Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
100d0be513 | ||
|
|
fc8db2d977 | ||
|
|
e5d98d7889 | ||
|
|
c7b1a05ed3 | ||
|
|
649a90bd5e | ||
|
|
0fc880cf21 | ||
|
|
0d82a4c2c6 | ||
|
|
fb3aa59683 | ||
|
|
b938792ca8 | ||
|
|
bb74946d54 | ||
|
|
3e78105509 | ||
|
|
c6adf0ac89 | ||
|
|
219b4dca94 | ||
|
|
acec292b0f | ||
|
|
7ba86cbd23 | ||
|
|
3002eaf479 | ||
|
|
15422ca504 | ||
|
|
7067f00bee | ||
|
|
a0616eb251 | ||
|
|
5d4da0e4c7 | ||
|
|
546e3475fb | ||
|
|
56c44ebb58 | ||
|
|
bd03bc730c | ||
|
|
e0e1e58104 | ||
|
|
940de662b6 | ||
|
|
d151c1192b | ||
|
|
15ae10579c | ||
|
|
359696408d | ||
|
|
d3766368f8 | ||
|
|
73af7451fc | ||
|
|
e27fcefe45 | ||
|
|
aa5ade5517 | ||
|
|
94b6a66ce4 | ||
|
|
a51e486fa6 | ||
|
|
2b608daea4 | ||
|
|
aa60c2afd1 | ||
|
|
cff22c74a9 | ||
|
|
b77b8ee0da | ||
|
|
610aef2f14 | ||
|
|
e15ca41d6e | ||
|
|
60e9e53af4 | ||
|
|
9812000724 | ||
|
|
d72401fc39 | ||
|
|
822eba7982 | ||
|
|
9327564e6e | ||
|
|
6e9fe42a83 | ||
|
|
a32cf28ccb | ||
|
|
55b101802a | ||
|
|
ecda8b4394 | ||
|
|
6d4f8d4c26 | ||
|
|
26aa53f3fa | ||
|
|
2f7a94ea3f | ||
|
|
2b622b4ee9 | ||
|
|
3e280f1e08 | ||
|
|
c54d52ad45 | ||
|
|
e54d3ea96b | ||
|
|
e07cb8257d | ||
|
|
74b6a8fdba | ||
|
|
e48c76397c | ||
|
|
64aff5d041 | ||
|
|
87b24e35d0 | ||
|
|
fb48c81207 | ||
|
|
cd9f302f8b | ||
|
|
df3bc97988 | ||
|
|
94191a2902 | ||
|
|
a0dd540075 | ||
|
|
5684665123 | ||
|
|
5db407d750 | ||
|
|
27a86f0dc1 | ||
|
|
767ea23173 | ||
|
|
85707508ed | ||
|
|
e8bc59ff9e | ||
|
|
7f9e622a64 | ||
|
|
b22e37596b | ||
|
|
642e90b1c8 | ||
|
|
8402ab896a | ||
|
|
d6dc7edf66 | ||
|
|
8f6dbf70f7 | ||
|
|
bd14d9a71e | ||
|
|
61a0a38dd2 | ||
|
|
102f438a99 | ||
|
|
586a2d1c79 | ||
|
|
5b04c39107 | ||
|
|
d96e91ea70 |
No files matched your search
@@ -23,8 +23,8 @@ assignees: ''
|
||||
```
|
||||
|
||||
> You can get the WAHA version by calling `GET /api/version` Try to update to
|
||||
> [the latest version](https://github.com/devlikeapro/whatsapp-http-api/releases)
|
||||
> before creating an issue!
|
||||
> [the latest version](https://github.com/devlikeapro/waha/releases) before
|
||||
> creating an issue!
|
||||
|
||||
**To Reproduce** Steps to reproduce the behavior:
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
#
|
||||
|
||||
OWNER=devlikeapro
|
||||
REPO=whatsapp-http-api-plus
|
||||
REPO=waha-plus
|
||||
|
||||
# list workflows
|
||||
gh api -X GET /repos/$OWNER/$REPO/actions/workflows | jq '.workflows[] | .name,.id'
|
||||
|
||||
@@ -55,6 +55,7 @@ jobs:
|
||||
flavor: latest=false
|
||||
tags: |
|
||||
type=raw,value=${{ matrix.tag }}
|
||||
type=raw,value=${{ matrix.tag }}-{{tag}}
|
||||
|
||||
- name: Login to image repository
|
||||
if: github.ref_type == 'tag'
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
.yarn
|
||||
example.ts
|
||||
.*sessions
|
||||
.secrets
|
||||
tokens
|
||||
files
|
||||
venv
|
||||
.wwebjs_auth
|
||||
test.js
|
||||
.wwebjs_cache
|
||||
src/dashboard
|
||||
|
||||
# compiled output
|
||||
/dist
|
||||
|
||||
@@ -46,3 +46,15 @@ repos:
|
||||
src/structures/.*|
|
||||
src/config.service.ts|
|
||||
)$
|
||||
|
||||
- repo: local
|
||||
hooks:
|
||||
- id: no-console-log
|
||||
name: No console.log() calls
|
||||
language: pygrep
|
||||
entry: 'console\.log'
|
||||
exclude: |
|
||||
(?x)^(
|
||||
.pre-commit-config.yaml|
|
||||
^src/core/engines/webjs/session.webjs.core.ts|
|
||||
)$
|
||||
+26
-8
@@ -6,35 +6,48 @@ FROM node:${NODE_VERSION} as build
|
||||
ENV PUPPETEER_SKIP_DOWNLOAD=True
|
||||
|
||||
# npm packages
|
||||
RUN apt-get update \
|
||||
&& apt-get install -y openssh-client
|
||||
RUN apt install openssh-client
|
||||
RUN mkdir -p -m 0700 ~/.ssh && ssh-keyscan github.com >> ~/.ssh/known_hosts
|
||||
|
||||
WORKDIR /src
|
||||
COPY package.json .
|
||||
COPY yarn.lock .
|
||||
RUN yarn set version 3.6.3
|
||||
RUN --mount=type=ssh yarn install
|
||||
RUN yarn install
|
||||
|
||||
# App
|
||||
WORKDIR /src
|
||||
ADD . /src
|
||||
RUN --mount=type=ssh yarn install
|
||||
RUN yarn install
|
||||
RUN yarn build && find ./dist -name "*.d.ts" -delete
|
||||
|
||||
#
|
||||
# Dashboard
|
||||
#
|
||||
FROM node:${NODE_VERSION} as dashboard
|
||||
|
||||
# Download WAHA Dashboard
|
||||
ENV WAHA_DASHBOARD_SHA 7bfa9269fd385af687a464882637881dfd34cfb6
|
||||
RUN \
|
||||
wget https://github.com/devlikeapro/dashboard/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}
|
||||
|
||||
#
|
||||
# Final
|
||||
#
|
||||
FROM node:${NODE_VERSION} as release
|
||||
ENV PUPPETEER_SKIP_DOWNLOAD=True
|
||||
# Quick fix for memory potential memory leaks
|
||||
# https://github.com/devlikeapro/whatsapp-http-api/issues/347
|
||||
# https://github.com/devlikeapro/waha/issues/347
|
||||
ENV NODE_OPTIONS="--max-old-space-size=16384"
|
||||
ARG USE_BROWSER=chromium
|
||||
|
||||
RUN echo "USE_BROWSER=$USE_BROWSER"
|
||||
|
||||
# Install ffmpeg to generate previews for videos
|
||||
RUN apt-get update && apt-get install -y ffmpeg --no-install-recommends && rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Install fonts if using either chromium or chrome
|
||||
RUN if [ "$USE_BROWSER" = "chromium" ] || [ "$USE_BROWSER" = "chrome" ]; then \
|
||||
apt-get update \
|
||||
@@ -69,6 +82,11 @@ WORKDIR /app
|
||||
COPY package.json ./
|
||||
COPY --from=build /src/node_modules ./node_modules
|
||||
COPY --from=build /src/dist ./dist
|
||||
COPY --from=dashboard /dashboard ./dist/dashboard
|
||||
|
||||
# Chokidar options to monitor file changes
|
||||
ENV CHOKIDAR_USEPOLLING=1
|
||||
ENV CHOKIDAR_INTERVAL=5000
|
||||
|
||||
# Run command, etc
|
||||
EXPOSE 3000
|
||||
|
||||
@@ -1,39 +1,39 @@
|
||||
build:
|
||||
docker build . -t devlikeapro/whatsapp-http-api
|
||||
docker build . -t devlikeapro/waha
|
||||
|
||||
build-chrome:
|
||||
docker build . -t devlikeapro/whatsapp-http-api-plus:chrome --build-arg USE_BROWSER=chrome
|
||||
docker build . -t devlikeapro/waha-plus:chrome --build-arg USE_BROWSER=chrome
|
||||
|
||||
build-noweb:
|
||||
docker build . -t devlikeapro/whatsapp-http-api:noweb --build-arg USE_BROWSER=none
|
||||
docker build . -t devlikeapro/waha:noweb --build-arg USE_BROWSER=none
|
||||
|
||||
build-all: build build-chrome build-noweb
|
||||
|
||||
build-plus:
|
||||
docker build . -t devlikeapro/whatsapp-http-api-plus
|
||||
docker build . -t devlikeapro/waha-plus
|
||||
|
||||
build-ssh:
|
||||
# check IMAGE provided
|
||||
@[ "${IMAGE}" ] || ( echo "Add APP: make build-ssh image=devlikeapro/whatsapp-http-api"; exit 1 );
|
||||
@[ "${IMAGE}" ] || ( echo "Add APP: make build-ssh image=devlikeapro/waha"; exit 1 );
|
||||
eval $(ssh-agent) && \
|
||||
ssh-add ~/.ssh/id_rsa && \
|
||||
docker buildx build --ssh default=${SSH_AUTH_SOCK} . -t ${IMAGE} --build-arg USE_BROWSER=none
|
||||
|
||||
stop:
|
||||
docker stop whatsapp-http-api
|
||||
docker stop waha
|
||||
|
||||
clean: stop
|
||||
sudo rm -rf .sessions
|
||||
|
||||
push:
|
||||
docker push devlikeapro/whatsapp-http-api
|
||||
docker push devlikeapro/waha
|
||||
|
||||
for-swagger:
|
||||
WHATSAPP_SWAGGER_CONFIG_ADVANCED=true npm run start
|
||||
WHATSAPP_SWAGGER_CONFIG_ADVANCED=true . ${NVM_DIR}/nvm.sh && nvm exec yarn start
|
||||
|
||||
update-dependencies:
|
||||
. ${NVM_DIR}/nvm.sh && nvm exec yarn up whatsapp-web.js@https://github.com/devlikeapro/whatsapp-web.js#main-fork
|
||||
. ${NVM_DIR}/nvm.sh && nvm exec yarn up @adiwajshing/baileys@github:WhiskeySockets/Baileys
|
||||
. ${NVM_DIR}/nvm.sh && nvm exec yarn up whatsapp-web.js@github:devlikeapro/whatsapp-web.js#fork-webpack-exodus-channels
|
||||
. ${NVM_DIR}/nvm.sh && nvm exec yarn up @adiwajshing/baileys@github:devlikeapro/Baileys#fork-master-channels
|
||||
. ${NVM_DIR}/nvm.sh && nvm exec yarn up venom-bot
|
||||
|
||||
start-proxy:
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# WhatsApp HTTP API (WAHA)
|
||||
# WAHA
|
||||
|
||||
<p align="center">
|
||||
<img src="./logo.png" style='border-radius: 50%' width='150'/>
|
||||
@@ -6,7 +6,7 @@
|
||||
|
||||
**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/whatsapp-http-api)
|
||||
[](https://hub.docker.com/r/devlikeapro/waha)
|
||||
|
||||
- Documentation: [https://waha.devlike.pro/](https://waha.devlike.pro/)
|
||||
- Swagger Example: [https://waha.devlike.pro/swagger](https://waha.devlike.pro/swagger)
|
||||
@@ -104,13 +104,13 @@ Assuming you have installed [Docker](https://docs.docker.com/get-docker/), let's
|
||||
|
||||
|
||||
```bash
|
||||
docker pull devlikeapro/whatsapp-http-api
|
||||
docker pull devlikeapro/waha
|
||||
```
|
||||
|
||||
|
||||
```bash
|
||||
docker login -u devlikeapro -p {KEY}
|
||||
docker pull devlikeapro/whatsapp-http-api-plus
|
||||
docker pull devlikeapro/waha-plus
|
||||
docker logout
|
||||
```
|
||||
|
||||
@@ -121,7 +121,7 @@ Read more about how to get `PASSWORD` for [Plus Version →]({{< relref "plus-ve
|
||||
Run WhatsApp HTTP API:
|
||||
|
||||
```bash
|
||||
docker run -it --rm -p 3000:3000/tcp --name whatsapp-http-api devlikeapro/whatsapp-http-api
|
||||
docker run -it --rm -p 3000:3000/tcp --name waha devlikeapro/waha
|
||||
|
||||
# It prints logs and the last line must be
|
||||
# WhatsApp HTTP API is running on: http://[::1]:3000
|
||||
|
||||
+47
-12
@@ -1,24 +1,25 @@
|
||||
version: '3'
|
||||
services:
|
||||
whatsapp-http-api:
|
||||
waha:
|
||||
# Check available Docker images:
|
||||
# https://waha.devlike.pro/docs/how-to/engines/#docker-images
|
||||
image: devlikeapro/whatsapp-http-api
|
||||
container_name: whatsapp-http-api
|
||||
image: devlikeapro/waha
|
||||
container_name: waha
|
||||
ports:
|
||||
- '3000:3000/tcp'
|
||||
environment:
|
||||
# Environment variables from https://waha.devlike.pro/docs/how-to/config/
|
||||
- WHATSAPP_HOOK_URL=https://httpbin.org/post
|
||||
# https://docs.webhook.site/
|
||||
- WHATSAPP_HOOK_URL=https://webhook.site/11111111-1111-1111-1111-11111111
|
||||
- WHATSAPP_HOOK_EVENTS=message
|
||||
- WHATSAPP_DEFAULT_ENGINE=WEBJS
|
||||
restart: always
|
||||
|
||||
whatsapp-http-api-plus:
|
||||
waha-plus:
|
||||
# Check available Docker images:
|
||||
# https://waha.devlike.pro/docs/how-to/engines/#docker-images
|
||||
image: devlikeapro/whatsapp-http-api-plus:chrome
|
||||
container_name: whatsapp-http-api-plus
|
||||
image: devlikeapro/waha-plus:chrome
|
||||
container_name: waha-plus
|
||||
ports:
|
||||
- '3000:3000/tcp'
|
||||
volumes:
|
||||
@@ -26,7 +27,7 @@ services:
|
||||
environment:
|
||||
# Environment variables from https://waha.devlike.pro/docs/how-to/config/
|
||||
# - WHATSAPP_RESTART_ALL_SESSIONS=True
|
||||
# - WHATSAPP_HOOK_URL=https://httpbin.org/post
|
||||
# - WHATSAPP_HOOK_URL=https://webhook.site/11111111-1111-1111-1111-11111111
|
||||
# - WHATSAPP_HOOK_EVENTS=message
|
||||
# - WHATSAPP_API_HOSTNAME=localhost
|
||||
- WHATSAPP_DEFAULT_ENGINE=WEBJS
|
||||
@@ -43,11 +44,11 @@ services:
|
||||
# - WAHA_WEBJS_WEB_VERSION=2.2412.54-videofix
|
||||
restart: always
|
||||
|
||||
whatsapp-http-api-plus--mongodb:
|
||||
waha-plus--mongodb:
|
||||
# Check available Docker images:
|
||||
# https://waha.devlike.pro/docs/how-to/engines/#docker-images
|
||||
image: devlikeapro/whatsapp-http-api-plus:chrome
|
||||
container_name: whatsapp-http-api-plus--mongodb
|
||||
image: devlikeapro/waha-plus:chrome
|
||||
container_name: waha-plus--mongodb
|
||||
ports:
|
||||
- '3000:3000/tcp'
|
||||
volumes:
|
||||
@@ -55,7 +56,7 @@ services:
|
||||
environment:
|
||||
# Environment variables from https://waha.devlike.pro/docs/how-to/config/
|
||||
# - WHATSAPP_RESTART_ALL_SESSIONS=True
|
||||
# - WHATSAPP_HOOK_URL=https://httpbin.org/post
|
||||
# - WHATSAPP_HOOK_URL=https://webhook.site/11111111-1111-1111-1111-11111111
|
||||
# - WHATSAPP_HOOK_EVENTS=message
|
||||
# - WHATSAPP_API_KEY=321
|
||||
# - WHATSAPP_SWAGGER_USERNAME=admin
|
||||
@@ -67,6 +68,40 @@ services:
|
||||
- WHATSAPP_SESSIONS_MONGO_URL=mongodb://mongodb:27017
|
||||
restart: always
|
||||
|
||||
# Let's Encrypt + Certbot
|
||||
# https://waha.devlike.pro/docs/how-to/security/#https
|
||||
waha-https--certbot:
|
||||
image: devlikeapro/waha-plus
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- '3000:3000'
|
||||
volumes:
|
||||
- './.sessions:/app/.sessions'
|
||||
- '/etc/letsencrypt:/etc/letsencrypt'
|
||||
environment:
|
||||
- DOMAIN=waha.example.pro
|
||||
- WHATSAPP_API_HOSTNAME=${DOMAIN}
|
||||
- WAHA_HTTPS_ENABLED=true
|
||||
- WAHA_HTTPS_PATH_KEY=/etc/letsencrypt/live/${DOMAIN}/privkey.pem
|
||||
- WAHA_HTTPS_PATH_CERT=/etc/letsencrypt/live/${DOMAIN}/cert.pem
|
||||
- WAHA_HTTPS_PATH_CA=/etc/letsencrypt/live/${DOMAIN}/chain.pem
|
||||
|
||||
# Self-signed certificates example
|
||||
# https://waha.devlike.pro/docs/how-to/security/#https
|
||||
waha-https--self-signed:
|
||||
image: devlikeapro/waha-plus
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- '3000:3000'
|
||||
volumes:
|
||||
- './.sessions:/app/.sessions'
|
||||
- './.secrets:/app/.secrets'
|
||||
environment:
|
||||
- WAHA_HTTPS_ENABLED=true
|
||||
- WAHA_HTTPS_PATH_KEY=.secrets/privkey.pem
|
||||
- WAHA_HTTPS_PATH_CERT=.secrets/cert.pem
|
||||
- WAHA_HTTPS_PATH_CA=.secrets/chain.pem
|
||||
|
||||
mongodb:
|
||||
image: mongo
|
||||
container_name: mongodb
|
||||
|
||||
+12
-12
@@ -19,9 +19,9 @@ Download and start WhatsApp HTTP API docker container
|
||||
|
||||
```bash
|
||||
# Download the image
|
||||
docker pull devlikeapro/whatsapp-http-api
|
||||
docker pull devlikeapro/waha
|
||||
# Run the docker container
|
||||
docker run -it --rm --network=host -e WHATSAPP_HOOK_URL=http://localhost:5000/bot -e "WHATSAPP_HOOK_EVENTS=*" --name whatsapp-http-api devlikeapro/whatsapp-http-api
|
||||
docker run -it --rm --network=host -e WHATSAPP_HOOK_URL=http://localhost:5000/bot -e "WHATSAPP_HOOK_EVENTS=*" --name waha devlikeapro/waha
|
||||
|
||||
# It prints logs and the last line must be
|
||||
# WhatsApp HTTP API is running on: http://[::1]:3000
|
||||
@@ -36,11 +36,11 @@ commands to download the image
|
||||
|
||||
```bash
|
||||
# Download the image
|
||||
docker pull devlikeapro/whatsapp-http-api:arm
|
||||
# Rename it, so you can use devlikeapro/whatsapp-http-api image in other place
|
||||
docker tag devlikeapro/whatsapp-http-api:arm devlikeapro/whatsapp-http-api
|
||||
docker pull devlikeapro/waha:arm
|
||||
# Rename it, so you can use devlikeapro/waha image in other place
|
||||
docker tag devlikeapro/waha:arm devlikeapro/waha
|
||||
# Run the docker container
|
||||
docker run -it --rm --network=host -e WHATSAPP_HOOK_URL=http://localhost:5000/bot -e "WHATSAPP_HOOK_EVENTS=*" --name whatsapp-http-api devlikeapro/whatsapp-http-api
|
||||
docker run -it --rm --network=host -e WHATSAPP_HOOK_URL=http://localhost:5000/bot -e "WHATSAPP_HOOK_EVENTS=*" --name waha devlikeapro/waha
|
||||
|
||||
# It prints logs and the last line must be
|
||||
# WhatsApp HTTP API is running on: http://[::1]:3000
|
||||
@@ -79,9 +79,9 @@ set up webhooks and reply to incoming messages.
|
||||
|
||||
```bash
|
||||
# Clone the git repository with example
|
||||
git clone https://github.com/devlikeapro/whatsapp-http-api.git
|
||||
git clone https://github.com/devlikeapro/waha.git
|
||||
# Open python example folder
|
||||
cd whatsapp-http-api/examples/python
|
||||
cd waha/examples/python
|
||||
# Install requirements
|
||||
python -mpip install -r requirements.txt
|
||||
# Run the bot
|
||||
@@ -106,18 +106,18 @@ Download **WAHA Plus** version:
|
||||
|
||||
```bash
|
||||
# Download the image
|
||||
docker pull devlikeapro/whatsapp-http-api-plus
|
||||
docker pull devlikeapro/waha-plus
|
||||
# Run the docker container
|
||||
docker run -it --rm --network=host -e WHATSAPP_HOOK_URL=http://localhost:5000/bot -e "WHATSAPP_HOOK_EVENTS=*" --name whatsapp-http-api devlikeapro/whatsapp-http-api-plus
|
||||
docker run -it --rm --network=host -e WHATSAPP_HOOK_URL=http://localhost:5000/bot -e "WHATSAPP_HOOK_EVENTS=*" --name waha devlikeapro/waha-plus
|
||||
```
|
||||
|
||||
Run the WhatsApp Download Files bot:
|
||||
|
||||
```bash
|
||||
# Clone the git repository with example
|
||||
git clone https://github.com/devlikeapro/whatsapp-http-api.git
|
||||
git clone https://github.com/devlikeapro/waha.git
|
||||
# Open python example folder
|
||||
cd whatsapp-http-api/examples/python
|
||||
cd waha/examples/python
|
||||
# Install requirements
|
||||
python -mpip install -r requirements.txt
|
||||
# Run the bot
|
||||
|
||||
+16
-4
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"name": "whatsapp-http-api",
|
||||
"name": "waha",
|
||||
"version": "0.0.1",
|
||||
"description": "",
|
||||
"author": "",
|
||||
@@ -22,7 +22,7 @@
|
||||
"test:e2e": "jest --config ./test/jest-e2e.json"
|
||||
},
|
||||
"dependencies": {
|
||||
"@adiwajshing/baileys": "github:WhiskeySockets/Baileys",
|
||||
"@adiwajshing/baileys": "github:devlikeapro/Baileys#fork-master-channels",
|
||||
"@adiwajshing/keyed-db": "^0.2.4",
|
||||
"@nestjs/axios": "^3.0.2",
|
||||
"@nestjs/common": "^9.0.9",
|
||||
@@ -30,24 +30,35 @@
|
||||
"@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/swagger": "^7.1.11",
|
||||
"@nestjs/terminus": "^10.2.3",
|
||||
"@nestjs/websockets": "^9.0.9",
|
||||
"@types/better-sqlite3": "^7.6.10",
|
||||
"@types/lodash": "^4.14.194",
|
||||
"@types/ws": "^8.5.4",
|
||||
"async-lock": "^1.4.1",
|
||||
"better-sqlite3": "9.6.0",
|
||||
"check-disk-space": "^3.4.0",
|
||||
"class-validator": "^0.12.2",
|
||||
"chokidar": "^3.6.0",
|
||||
"class-transformer": "^0.5.1",
|
||||
"class-validator": "0.14.0",
|
||||
"del": "^6.0.0",
|
||||
"express-basic-auth": "^1.2.1",
|
||||
"file-type": "16.5.4",
|
||||
"https-proxy-agent": "^7.0.0",
|
||||
"knex": "^3.1.0",
|
||||
"libphonenumber-js": "^1.10.36",
|
||||
"link-preview-js": "^3.0.4",
|
||||
"lodash": "^4.17.21",
|
||||
"mime-types": "^2.1.27",
|
||||
"mongodb": "^6.3.0",
|
||||
"nestjs-pino": "^4.1.0",
|
||||
"passport": "^0.6.0",
|
||||
"passport-headerapikey": "^1.2.2",
|
||||
"pino-http": "^10.2.0",
|
||||
"pino-pretty": "^11.2.1",
|
||||
"puppeteer": "^21.0.0",
|
||||
"qrcode": "^1.5.1",
|
||||
"qrcode-terminal": "^0.12.0",
|
||||
@@ -56,9 +67,10 @@
|
||||
"requestretry": "^4.1.1",
|
||||
"rimraf": "^3.0.2",
|
||||
"rxjs": "^7.1.0",
|
||||
"sharp": "^0.33.4",
|
||||
"swagger-ui-express": "^4.1.4",
|
||||
"venom-bot": "5.0.1",
|
||||
"whatsapp-web.js": "https://github.com/devlikeapro/whatsapp-web.js#main-fork-update"
|
||||
"whatsapp-web.js": "github:devlikeapro/whatsapp-web.js#fork-webpack-exodus-channels"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@nestjs/cli": "^9.0.0",
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
import { BufferJSON } from '@adiwajshing/baileys/lib/Utils';
|
||||
import {
|
||||
CallHandler,
|
||||
ExecutionContext,
|
||||
Injectable,
|
||||
NestInterceptor,
|
||||
StreamableFile,
|
||||
} from '@nestjs/common';
|
||||
import { Observable } from 'rxjs';
|
||||
import { map } from 'rxjs/operators';
|
||||
|
||||
@Injectable()
|
||||
export class BufferJsonReplacerInterceptor implements NestInterceptor {
|
||||
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
|
||||
return next.handle().pipe(
|
||||
map((data) => {
|
||||
if (typeof data !== 'object' || data === null) {
|
||||
return data;
|
||||
}
|
||||
// Buffer
|
||||
if (Buffer.isBuffer(data) || data?.data || data?.url) {
|
||||
return data;
|
||||
}
|
||||
|
||||
// StreamableFile
|
||||
if (data instanceof StreamableFile) {
|
||||
return data;
|
||||
}
|
||||
|
||||
return JSON.parse(JSON.stringify(data, BufferJSON.replacer));
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
import {
|
||||
Body,
|
||||
Controller,
|
||||
Delete,
|
||||
Get,
|
||||
Param,
|
||||
Post,
|
||||
Query,
|
||||
} from '@nestjs/common';
|
||||
import { ApiOperation, ApiSecurity, ApiTags } from '@nestjs/swagger';
|
||||
import {
|
||||
Channel,
|
||||
CreateChannelRequest,
|
||||
ListChannelsQuery,
|
||||
NewsletterIdApiParam,
|
||||
NewsletterIdOrInviteCodeApiParam,
|
||||
} from '@waha/structures/channels.dto';
|
||||
|
||||
import { SessionManager } from '../core/abc/manager.abc';
|
||||
import { isNewsletter, WhatsappSession } from '../core/abc/session.abc';
|
||||
import { SessionApiParam, SessionParam } from './helpers';
|
||||
|
||||
@ApiSecurity('api_key')
|
||||
@Controller('api/:session/channels')
|
||||
@ApiTags('channels')
|
||||
export class ChannelsController {
|
||||
constructor(private manager: SessionManager) {}
|
||||
|
||||
@Get('')
|
||||
@SessionApiParam
|
||||
@ApiOperation({ summary: 'Get list of know channels' })
|
||||
async list(
|
||||
@SessionParam session: WhatsappSession,
|
||||
@Query() query: ListChannelsQuery,
|
||||
): Promise<Channel[]> {
|
||||
return session.channelsList(query);
|
||||
}
|
||||
|
||||
@Post('')
|
||||
@SessionApiParam
|
||||
@ApiOperation({ summary: 'Create a new channel.' })
|
||||
create(
|
||||
@SessionParam session: WhatsappSession,
|
||||
@Body() request: CreateChannelRequest,
|
||||
): Promise<Channel> {
|
||||
return session.channelsCreateChannel(request);
|
||||
}
|
||||
|
||||
@Delete(':id')
|
||||
@SessionApiParam
|
||||
@NewsletterIdApiParam
|
||||
@ApiOperation({ summary: 'Delete the channel.' })
|
||||
delete(@SessionParam session: WhatsappSession, @Param('id') id: string) {
|
||||
return session.channelsDeleteChannel(id);
|
||||
}
|
||||
|
||||
@Get(':id')
|
||||
@SessionApiParam
|
||||
@NewsletterIdOrInviteCodeApiParam
|
||||
@ApiOperation({
|
||||
summary: 'Get the channel info - either by id @newsletter OR invite code.',
|
||||
})
|
||||
get(
|
||||
@SessionParam session: WhatsappSession,
|
||||
@Param('id') id: string,
|
||||
): Promise<Channel> {
|
||||
if (isNewsletter(id)) {
|
||||
return session.channelsGetChannel(id);
|
||||
} else {
|
||||
// https://www.whatsapp.com/channel/123 => 123
|
||||
const inviteCode = id.split('/').pop();
|
||||
return session.channelsGetChannelByInviteCode(inviteCode);
|
||||
}
|
||||
}
|
||||
|
||||
@Post('follow')
|
||||
@SessionApiParam
|
||||
@NewsletterIdApiParam
|
||||
@ApiOperation({ summary: 'Follow the channel.' })
|
||||
follow(
|
||||
@SessionParam session: WhatsappSession,
|
||||
@Param('id') id: string,
|
||||
): Promise<void> {
|
||||
return session.channelsFollowChannel(id);
|
||||
}
|
||||
|
||||
@Post('unfollow')
|
||||
@SessionApiParam
|
||||
@NewsletterIdApiParam
|
||||
@ApiOperation({ summary: 'Unfollow the channel.' })
|
||||
unfollow(
|
||||
@SessionParam session: WhatsappSession,
|
||||
@Param('id') id: string,
|
||||
): Promise<void> {
|
||||
return session.channelsUnfollowChannel(id);
|
||||
}
|
||||
|
||||
@Post('mute')
|
||||
@SessionApiParam
|
||||
@NewsletterIdApiParam
|
||||
@ApiOperation({ summary: 'Mute the channel.' })
|
||||
mute(
|
||||
@SessionParam session: WhatsappSession,
|
||||
@Param('id') id: string,
|
||||
): Promise<void> {
|
||||
return session.channelsMuteChannel(id);
|
||||
}
|
||||
|
||||
@Post('unmute')
|
||||
@SessionApiParam
|
||||
@NewsletterIdApiParam
|
||||
@ApiOperation({ summary: 'Unmute the channel.' })
|
||||
unmute(
|
||||
@SessionParam session: WhatsappSession,
|
||||
@Param('id') id: string,
|
||||
): Promise<void> {
|
||||
return session.channelsUnmuteChannel(id);
|
||||
}
|
||||
}
|
||||
@@ -4,16 +4,17 @@ import {
|
||||
Delete,
|
||||
Get,
|
||||
Param,
|
||||
Post,
|
||||
Put,
|
||||
Query,
|
||||
UsePipes,
|
||||
ValidationPipe,
|
||||
} from '@nestjs/common';
|
||||
import { ApiOperation, ApiSecurity, ApiTags } from '@nestjs/swagger';
|
||||
|
||||
import { SessionManager } from '../core/abc/manager.abc';
|
||||
import { WhatsappSession } from '../core/abc/session.abc';
|
||||
import { parseBool } from '../helpers';
|
||||
import { GetChatMessagesQuery } from '../structures/chats.dto';
|
||||
import { GetChatMessagesQuery, GetChatsQuery } from '../structures/chats.dto';
|
||||
import { EditMessageRequest } from '../structures/chatting.dto';
|
||||
import {
|
||||
ChatIdApiParam,
|
||||
@@ -25,14 +26,18 @@ import {
|
||||
@ApiSecurity('api_key')
|
||||
@Controller('api/:session/chats')
|
||||
@ApiTags('chats')
|
||||
@UsePipes(new ValidationPipe({ transform: true }))
|
||||
class ChatsController {
|
||||
constructor(private manager: SessionManager) {}
|
||||
|
||||
@Get('')
|
||||
@SessionApiParam
|
||||
@ApiOperation({ summary: 'Get all chats' })
|
||||
getAllChats(@SessionParam session: WhatsappSession) {
|
||||
return session.getChats();
|
||||
@ApiOperation({ summary: 'Get chats' })
|
||||
getChats(
|
||||
@SessionParam session: WhatsappSession,
|
||||
@Query() query: GetChatsQuery,
|
||||
) {
|
||||
return session.getChats(query);
|
||||
}
|
||||
|
||||
@Delete(':chatId')
|
||||
|
||||
@@ -46,7 +46,7 @@ export class ChattingController {
|
||||
}
|
||||
|
||||
@Get('/sendText')
|
||||
@ApiOperation({ summary: 'Send a text message' })
|
||||
@ApiOperation({ summary: 'Send a text message', deprecated: true })
|
||||
sendTextGet(@Query() query: MessageTextQuery) {
|
||||
const whatsapp = this.manager.getSession(query.session);
|
||||
const msg = new MessageTextRequest();
|
||||
|
||||
@@ -31,7 +31,7 @@ export class AllExceptionsFilter implements ExceptionFilter {
|
||||
/**
|
||||
* If file not found - we get weird 500 error
|
||||
* So we convert that exception manually to 404 and send appropriate JSON response
|
||||
* @issue https://github.com/devlikeapro/whatsapp-http-api/issues/134
|
||||
* @issue https://github.com/devlikeapro/waha/issues/134
|
||||
* @solution https://github.com/nestjs/serve-static/issues/139#issuecomment-612429557
|
||||
*/
|
||||
if (exception.code === 'ENOENT') {
|
||||
|
||||
@@ -63,7 +63,7 @@ class SessionsController {
|
||||
}
|
||||
|
||||
@Get('/')
|
||||
async list(@Query() query: ListSessionsQuery): Promise<SessionInfo[]> {
|
||||
list(@Query() query: ListSessionsQuery): Promise<SessionInfo[]> {
|
||||
const all = parseBool(query.all);
|
||||
return this.manager.getSessions(all);
|
||||
}
|
||||
@@ -87,7 +87,7 @@ class SessionController {
|
||||
@Get('me')
|
||||
@SessionApiParam
|
||||
@ApiOperation({ summary: 'Get information about the authenticated account' })
|
||||
getMe(@SessionParam session: WhatsappSession): Promise<MeInfo | null> {
|
||||
getMe(@SessionParam session: WhatsappSession): MeInfo | null {
|
||||
return session.getSessionMeInfo();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ import { ApiOperation, ApiSecurity, ApiTags } from '@nestjs/swagger';
|
||||
import { SessionManager } from '../core/abc/manager.abc';
|
||||
import { WhatsappSession } from '../core/abc/session.abc';
|
||||
import {
|
||||
DeleteStatusRequest,
|
||||
ImageStatus,
|
||||
TextStatus,
|
||||
VideoStatus,
|
||||
@@ -64,6 +65,16 @@ class StatusController {
|
||||
) {
|
||||
return session.sendVideoStatus(status);
|
||||
}
|
||||
|
||||
@Post('delete')
|
||||
@SessionApiParam
|
||||
@ApiOperation({ summary: 'DELETE sent status.' })
|
||||
deleteStatus(
|
||||
@SessionParam session: WhatsappSession,
|
||||
@Body() status: DeleteStatusRequest,
|
||||
) {
|
||||
return session.deleteStatus(status);
|
||||
}
|
||||
}
|
||||
|
||||
export { StatusController };
|
||||
@@ -112,17 +112,6 @@ export class WhatsappConfigService {
|
||||
return this.configService.get('WHATSAPP_SESSIONS_MONGO_URL', undefined);
|
||||
}
|
||||
|
||||
getDefaultEngineName(): WAHAEngine {
|
||||
const value = getEngineName();
|
||||
if (value in WAHAEngine) {
|
||||
return WAHAEngine[value];
|
||||
}
|
||||
console.log(
|
||||
`Unknown WhatsApp default engine WHATSAPP_DEFAULT_ENGINE=${value}. Using WEBJS`,
|
||||
);
|
||||
return WAHAEngine.WEBJS;
|
||||
}
|
||||
|
||||
get(name: string, defaultValue: any = undefined): any {
|
||||
return this.configService.get(name, defaultValue);
|
||||
}
|
||||
|
||||
@@ -1,13 +1,18 @@
|
||||
import { INestApplication } from '@nestjs/common';
|
||||
import { DocumentBuilder, OpenAPIObject, SwaggerModule } from '@nestjs/swagger';
|
||||
import { DECORATORS } from '@nestjs/swagger/dist/constants';
|
||||
import { Logger } from 'nestjs-pino';
|
||||
|
||||
import { WhatsappConfigService } from '../config.service';
|
||||
import { VERSION } from '../version';
|
||||
import { SwaggerConfigServiceCore } from './config/SwaggerConfigServiceCore';
|
||||
|
||||
export class SwaggerConfiguratorCore {
|
||||
constructor(protected app: INestApplication) {}
|
||||
protected logger: any;
|
||||
|
||||
constructor(protected app: INestApplication) {
|
||||
this.logger = app.get(Logger);
|
||||
}
|
||||
|
||||
get title() {
|
||||
return 'WAHA - WhatsApp HTTP API';
|
||||
@@ -21,8 +26,8 @@ export class SwaggerConfiguratorCore {
|
||||
'<ul>' +
|
||||
'<li><a href="https://waha.devlike.pro/" target="_blank">Documentation</a></li>' +
|
||||
'<li><a href="https://waha.devlike.pro/docs/how-to/engines/#features" target="_blank">Supported features in engines</a></li>' +
|
||||
'<li><a href="https://github.com/devlikeapro/whatsapp-http-api" target="_blank">GitHub - WAHA Core</a></li>' +
|
||||
'<li><a href="https://github.com/devlikeapro/whatsapp-http-api-plu' + // Separate line to pass pre-commit check
|
||||
'<li><a href="https://github.com/devlikeapro/waha" target="_blank">GitHub - WAHA Core</a></li>' +
|
||||
'<li><a href="https://github.com/devlikeapro/waha-plu' + // Separate line to pass pre-commit check
|
||||
's" target="_blank">GitHub - WAHA Plus</a></li>' +
|
||||
'<li><b>NEW!</b> - <a href="/dashboard">WAHA Dashboard</a></li>' +
|
||||
'<li><b>NEW!</b> - <a href="https://waha.devlike.pro/docs/how-to/waha-dashboard">WAHA Dashboard Documentation</a></li>' +
|
||||
@@ -55,6 +60,7 @@ export class SwaggerConfiguratorCore {
|
||||
.addTag('auth', 'Authentication')
|
||||
.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!',
|
||||
|
||||
@@ -1,7 +1,14 @@
|
||||
import { OnApplicationShutdown } from '@nestjs/common';
|
||||
|
||||
import { WAHAEngine } from '../../structures/enums.dto';
|
||||
import {
|
||||
BeforeApplicationShutdown,
|
||||
OnApplicationShutdown,
|
||||
} from '@nestjs/common';
|
||||
import { WAHAWebhook } from '@waha/structures/webhooks.dto';
|
||||
import { VERSION } from '@waha/version';
|
||||
import { EventEmitter } from 'events';
|
||||
|
||||
import { WAHAEngine, WAHAEvents } from '../../structures/enums.dto';
|
||||
import {
|
||||
MeInfo,
|
||||
SessionDTO,
|
||||
SessionInfo,
|
||||
SessionLogoutRequest,
|
||||
@@ -13,10 +20,11 @@ import { ISessionConfigRepository } from '../storage/ISessionConfigRepository';
|
||||
import { WhatsappSession } from './session.abc';
|
||||
import { WebhookConductor } from './webhooks.abc';
|
||||
|
||||
export abstract class SessionManager implements OnApplicationShutdown {
|
||||
export abstract class SessionManager implements BeforeApplicationShutdown {
|
||||
public store: any;
|
||||
public sessionAuthRepository: ISessionAuthRepository;
|
||||
public sessionConfigRepository: ISessionConfigRepository;
|
||||
public events: EventEmitter;
|
||||
|
||||
protected abstract getEngine(engine: WAHAEngine): typeof WhatsappSession;
|
||||
|
||||
@@ -24,7 +32,7 @@ export abstract class SessionManager implements OnApplicationShutdown {
|
||||
|
||||
protected abstract get WebhookConductorClass(): typeof WebhookConductor;
|
||||
|
||||
abstract onApplicationShutdown(signal?: string);
|
||||
abstract beforeApplicationShutdown(signal?: string);
|
||||
|
||||
//
|
||||
// API Methods
|
||||
@@ -40,4 +48,19 @@ export abstract class SessionManager implements OnApplicationShutdown {
|
||||
abstract getSessionInfo(name: string): Promise<SessionInfo | null>;
|
||||
|
||||
abstract getSessions(all: boolean): Promise<SessionInfo[]>;
|
||||
|
||||
handleSessionEvent(event: WAHAEvents, session: WhatsappSession) {
|
||||
return (payload: any) => {
|
||||
const me = session.getSessionMeInfo();
|
||||
const data: WAHAWebhook = {
|
||||
event: event,
|
||||
session: session.name,
|
||||
me: me,
|
||||
payload: payload,
|
||||
engine: session.engine,
|
||||
environment: VERSION,
|
||||
};
|
||||
this.events.emit(event, data);
|
||||
};
|
||||
}
|
||||
}
|
||||
+77
-17
@@ -1,6 +1,13 @@
|
||||
import { ConsoleLogger } from '@nestjs/common';
|
||||
import {
|
||||
Channel,
|
||||
CreateChannelRequest,
|
||||
ListChannelsQuery,
|
||||
} from '@waha/structures/channels.dto';
|
||||
import { GetChatsQuery } from '@waha/structures/chats.dto';
|
||||
import { LoggerBuilder } from '@waha/utils/logging';
|
||||
import { EventEmitter } from 'events';
|
||||
import * as fs from 'fs';
|
||||
import { Logger } from 'pino';
|
||||
import { MessageId } from 'whatsapp-web.js';
|
||||
|
||||
import {
|
||||
@@ -40,6 +47,7 @@ import {
|
||||
SessionConfig,
|
||||
} from '../../structures/sessions.dto';
|
||||
import {
|
||||
DeleteStatusRequest,
|
||||
ImageStatus,
|
||||
TextStatus,
|
||||
VideoStatus,
|
||||
@@ -74,14 +82,13 @@ export function ensureSuffix(phone) {
|
||||
|
||||
export enum WAHAInternalEvent {
|
||||
ENGINE_START = 'engine.start',
|
||||
SESSION_STATUS_CHANGED = 'session.status.changed',
|
||||
}
|
||||
|
||||
export interface SessionParams {
|
||||
name: string;
|
||||
printQR: boolean;
|
||||
mediaManager: MediaManager;
|
||||
log: ConsoleLogger;
|
||||
loggerBuilder: LoggerBuilder;
|
||||
sessionStore: DataStore;
|
||||
proxyConfig?: ProxyConfig;
|
||||
sessionConfig?: SessionConfig;
|
||||
@@ -94,7 +101,8 @@ export abstract class WhatsappSession {
|
||||
|
||||
public name: string;
|
||||
protected mediaManager: MediaManager;
|
||||
protected log: ConsoleLogger;
|
||||
public loggerBuilder: LoggerBuilder;
|
||||
protected logger: Logger;
|
||||
protected sessionStore: DataStore;
|
||||
protected proxyConfig?: ProxyConfig;
|
||||
public sessionConfig?: SessionConfig;
|
||||
@@ -106,7 +114,7 @@ export abstract class WhatsappSession {
|
||||
public constructor({
|
||||
name,
|
||||
printQR,
|
||||
log,
|
||||
loggerBuilder,
|
||||
sessionStore,
|
||||
proxyConfig,
|
||||
mediaManager,
|
||||
@@ -116,7 +124,8 @@ export abstract class WhatsappSession {
|
||||
this.events = new EventEmitter();
|
||||
this.name = name;
|
||||
this.proxyConfig = proxyConfig;
|
||||
this.log = log;
|
||||
this.loggerBuilder = loggerBuilder;
|
||||
this.logger = loggerBuilder.child({ name: 'WhatsappSession' });
|
||||
this.sessionStore = sessionStore;
|
||||
this.mediaManager = mediaManager;
|
||||
this.sessionConfig = sessionConfig;
|
||||
@@ -126,7 +135,8 @@ export abstract class WhatsappSession {
|
||||
|
||||
protected set status(value: WAHASessionStatus) {
|
||||
this._status = value;
|
||||
this.events.emit(WAHAInternalEvent.SESSION_STATUS_CHANGED, value);
|
||||
const body: WASessionStatusBody = { name: this.name, status: value };
|
||||
this.events.emit(WAHAEvents.SESSION_STATUS, body);
|
||||
}
|
||||
|
||||
public get status() {
|
||||
@@ -168,7 +178,7 @@ export abstract class WhatsappSession {
|
||||
}
|
||||
|
||||
protected isDebugEnabled() {
|
||||
return this.log.isLevelEnabled('debug');
|
||||
return this.logger.isLevelEnabled('debug');
|
||||
}
|
||||
|
||||
/** Start the session */
|
||||
@@ -184,10 +194,7 @@ export abstract class WhatsappSession {
|
||||
): boolean {
|
||||
switch (hook) {
|
||||
case WAHAEvents.SESSION_STATUS:
|
||||
this.events.on(WAHAInternalEvent.SESSION_STATUS_CHANGED, (value) => {
|
||||
const body: WASessionStatusBody = { name: this.name, status: value };
|
||||
handler(body);
|
||||
});
|
||||
this.events.on(WAHAEvents.SESSION_STATUS, handler);
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
@@ -221,8 +228,8 @@ export abstract class WhatsappSession {
|
||||
|
||||
abstract getScreenshot(): Promise<Buffer>;
|
||||
|
||||
public getSessionMeInfo(): Promise<MeInfo | null> {
|
||||
throw new NotImplementedByEngineError();
|
||||
public getSessionMeInfo(): MeInfo | null {
|
||||
return null;
|
||||
}
|
||||
|
||||
public getCaptcha(): Promise<QR> {
|
||||
@@ -285,7 +292,7 @@ export abstract class WhatsappSession {
|
||||
/**
|
||||
* Chats methods
|
||||
*/
|
||||
public getChats() {
|
||||
public getChats(query: GetChatsQuery) {
|
||||
throw new NotImplementedByEngineError();
|
||||
}
|
||||
|
||||
@@ -435,6 +442,47 @@ export abstract class WhatsappSession {
|
||||
throw new NotImplementedByEngineError();
|
||||
}
|
||||
|
||||
/**
|
||||
* Channels methods
|
||||
*/
|
||||
public channelsList(query: ListChannelsQuery): Promise<Channel[]> {
|
||||
throw new NotImplementedByEngineError();
|
||||
}
|
||||
|
||||
public channelsCreateChannel(
|
||||
request: CreateChannelRequest,
|
||||
): Promise<Channel> {
|
||||
throw new NotImplementedByEngineError();
|
||||
}
|
||||
|
||||
public channelsGetChannel(id: string): Promise<Channel> {
|
||||
throw new NotImplementedByEngineError();
|
||||
}
|
||||
|
||||
public channelsGetChannelByInviteCode(inviteCode: string): Promise<Channel> {
|
||||
throw new NotImplementedByEngineError();
|
||||
}
|
||||
|
||||
public channelsDeleteChannel(id: string): Promise<void> {
|
||||
throw new NotImplementedByEngineError();
|
||||
}
|
||||
|
||||
public channelsFollowChannel(id: string): Promise<void> {
|
||||
throw new NotImplementedByEngineError();
|
||||
}
|
||||
|
||||
public channelsUnfollowChannel(id: string): Promise<void> {
|
||||
throw new NotImplementedByEngineError();
|
||||
}
|
||||
|
||||
public channelsMuteChannel(id: string): Promise<void> {
|
||||
throw new NotImplementedByEngineError();
|
||||
}
|
||||
|
||||
public channelsUnmuteChannel(id: string): Promise<void> {
|
||||
throw new NotImplementedByEngineError();
|
||||
}
|
||||
|
||||
/**
|
||||
* Status methods
|
||||
*/
|
||||
@@ -454,6 +502,10 @@ export abstract class WhatsappSession {
|
||||
throw new NotImplementedByEngineError();
|
||||
}
|
||||
|
||||
public deleteStatus(request: DeleteStatusRequest) {
|
||||
throw new NotImplementedByEngineError();
|
||||
}
|
||||
|
||||
public async getEngineInfo(): Promise<any> {
|
||||
return {};
|
||||
}
|
||||
@@ -485,14 +537,22 @@ export abstract class WhatsappSession {
|
||||
return;
|
||||
}
|
||||
if (!qr.raw) {
|
||||
this.log.error(
|
||||
this.logger.error(
|
||||
'QR.raw is not available, can not print it in the console',
|
||||
);
|
||||
return;
|
||||
}
|
||||
this.log.log(
|
||||
this.logger.info(
|
||||
"You can disable QR in console by setting 'WAHA_PRINT_QR=false' in your environment variables.",
|
||||
);
|
||||
qrcode.generate(qr.raw, { small: true });
|
||||
}
|
||||
}
|
||||
|
||||
export function isNewsletter(jid: string) {
|
||||
return jid.endsWith('@newsletter');
|
||||
}
|
||||
|
||||
export function getChannelInviteLink(code: string) {
|
||||
return `https://whatsapp.com/channel/${code}`;
|
||||
}
|
||||
@@ -1,15 +1,19 @@
|
||||
import { ConsoleLogger } from '@nestjs/common';
|
||||
import { LoggerBuilder } from '@waha/utils/logging';
|
||||
import { Logger } from 'pino';
|
||||
|
||||
import { WebhookConfig } from '../../structures/webhooks.config.dto';
|
||||
import { WhatsappSession } from './session.abc';
|
||||
|
||||
export abstract class WebhookSender {
|
||||
protected url: string;
|
||||
protected logger: Logger;
|
||||
|
||||
constructor(
|
||||
protected log: ConsoleLogger,
|
||||
loggerBuilder: LoggerBuilder,
|
||||
protected webhookConfig: WebhookConfig,
|
||||
) {
|
||||
this.url = webhookConfig.url;
|
||||
this.logger = loggerBuilder.child({ name: WebhookSender.name });
|
||||
}
|
||||
|
||||
abstract send(json);
|
||||
|
||||
@@ -0,0 +1,135 @@
|
||||
import {
|
||||
BeforeApplicationShutdown,
|
||||
ConsoleLogger,
|
||||
Logger,
|
||||
LoggerService,
|
||||
} from '@nestjs/common';
|
||||
import { sleep } from '@nestjs/terminus/dist/utils';
|
||||
import {
|
||||
OnGatewayConnection,
|
||||
OnGatewayDisconnect,
|
||||
OnGatewayInit,
|
||||
WebSocketGateway,
|
||||
WebSocketServer,
|
||||
} from '@nestjs/websockets';
|
||||
import { SessionManager } from '@waha/core/abc/manager.abc';
|
||||
import { WAHAEvents } from '@waha/structures/enums.dto';
|
||||
import { HeartbeatJob } from '@waha/utils/HeartbeatJob';
|
||||
import { WebSocket } from '@waha/utils/ws';
|
||||
import { IncomingMessage } from 'http';
|
||||
import * as lodash from 'lodash';
|
||||
import * as url from 'url';
|
||||
import { Server } from 'ws';
|
||||
|
||||
@WebSocketGateway({
|
||||
path: '/ws',
|
||||
cors: true,
|
||||
})
|
||||
export class WebsocketGatewayCore
|
||||
implements
|
||||
OnGatewayInit,
|
||||
OnGatewayConnection,
|
||||
OnGatewayDisconnect,
|
||||
BeforeApplicationShutdown
|
||||
{
|
||||
HEARTBEAT_INTERVAL = 10_000;
|
||||
|
||||
@WebSocketServer()
|
||||
server: Server;
|
||||
|
||||
private listeners: Map<WebSocket, { session: string; events: string[] }> =
|
||||
new Map();
|
||||
|
||||
private readonly logger: LoggerService;
|
||||
private heartbeat: HeartbeatJob;
|
||||
|
||||
constructor(private manager: SessionManager) {
|
||||
this.logger = new Logger('WebsocketGateway');
|
||||
this.heartbeat = new HeartbeatJob(this.logger, this.HEARTBEAT_INTERVAL);
|
||||
}
|
||||
|
||||
handleConnection(socket: WebSocket, request: IncomingMessage, ...args): any {
|
||||
const random = Math.random().toString(36).substring(7);
|
||||
const id = `wsc_${random}`;
|
||||
socket.id = id;
|
||||
this.logger.debug(`New client connected: ${request.url}`);
|
||||
const params = this.getParams(id, request, socket);
|
||||
if (!params) {
|
||||
return;
|
||||
}
|
||||
const { session, events } = params;
|
||||
this.logger.debug(
|
||||
`Client connected to session: '${session}', events: ${events}, ${id}`,
|
||||
);
|
||||
this.listeners.set(socket, { session, events });
|
||||
}
|
||||
|
||||
private getParams(id: string, request: IncomingMessage, socket: WebSocket) {
|
||||
const query = url.parse(request.url, true).query;
|
||||
const session = (query.session as string) || '*';
|
||||
if (session !== '*') {
|
||||
this.logger.warn(
|
||||
`Only connecting to all sessions is allowed for now, use session=*, ${id}`,
|
||||
);
|
||||
const error =
|
||||
'Only connecting to all sessions is allowed for now, use session=*';
|
||||
socket.close(4001, JSON.stringify({ error }));
|
||||
return null;
|
||||
}
|
||||
|
||||
const events = ((query.events as string) || '*').split(',');
|
||||
if (
|
||||
!lodash.isEqual(events, ['*']) &&
|
||||
!lodash.isEqual(events, [WAHAEvents.SESSION_STATUS])
|
||||
) {
|
||||
this.logger.warn(
|
||||
`Only \'session.status\' event is allowed for now, use events=session.status or events=*, ${id}`,
|
||||
);
|
||||
const error =
|
||||
"Only 'session.status' event is allowed for now, use events=session.status or events=*";
|
||||
socket.close(4001, JSON.stringify({ error }));
|
||||
return null;
|
||||
}
|
||||
return { session, events };
|
||||
}
|
||||
|
||||
handleDisconnect(socket: WebSocket): any {
|
||||
this.logger.debug(`Client disconnected - ${socket.id}`);
|
||||
this.listeners.delete(socket);
|
||||
}
|
||||
|
||||
async beforeApplicationShutdown(signal?: string) {
|
||||
this.logger.log('Shutting down websocket server');
|
||||
// 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) => {
|
||||
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
|
||||
this.logger.log('Websocket server is down');
|
||||
}
|
||||
|
||||
afterInit(server: Server) {
|
||||
this.logger.debug('Websocket server initialized');
|
||||
this.manager.events.on(
|
||||
WAHAEvents.SESSION_STATUS,
|
||||
this.sendToAll.bind(this),
|
||||
);
|
||||
this.logger.debug('Subscribed to manager events');
|
||||
|
||||
this.logger.debug('Starting heartbeat service...');
|
||||
this.heartbeat.start(server);
|
||||
this.logger.debug('Heartbeat service started');
|
||||
}
|
||||
|
||||
sendToAll(data: any) {
|
||||
this.listeners.forEach((options, client) => {
|
||||
if (options.events.length === 0) {
|
||||
return;
|
||||
}
|
||||
this.logger.debug('Sending data to client', data);
|
||||
client.send(JSON.stringify(data));
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,16 @@
|
||||
import { ConsoleLogger, Module } from '@nestjs/common';
|
||||
import { ConsoleLogger, INestApplication, Module } from '@nestjs/common';
|
||||
import { ConfigModule } from '@nestjs/config';
|
||||
import { APP_INTERCEPTOR } from '@nestjs/core';
|
||||
import { PassportModule } from '@nestjs/passport';
|
||||
import { ServeStaticModule } from '@nestjs/serve-static';
|
||||
import { TerminusModule } from '@nestjs/terminus';
|
||||
import { BufferJsonReplacerInterceptor } from '@waha/api/BufferJsonReplacerInterceptor';
|
||||
import { ChannelsController } from '@waha/api/channels.controller';
|
||||
import { WebsocketGatewayCore } from '@waha/core/api/websocket.gateway.core';
|
||||
import { getPinoLogLevel, getPinoTransport } from '@waha/utils/logging';
|
||||
import { LoggerModule } from 'nestjs-pino';
|
||||
import { join } from 'path';
|
||||
import { Logger } from 'pino';
|
||||
|
||||
import { AuthController } from '../api/auth.controller';
|
||||
import { ChatsController } from '../api/chats.controller';
|
||||
@@ -29,6 +36,31 @@ import { WAHAHealthCheckServiceCore } from './health/WAHAHealthCheckServiceCore'
|
||||
import { SessionManagerCore } from './manager.core';
|
||||
|
||||
export const IMPORTS = [
|
||||
LoggerModule.forRoot({
|
||||
renameContext: 'name',
|
||||
pinoHttp: {
|
||||
quietReqLogger: true,
|
||||
level: getPinoLogLevel(),
|
||||
transport: getPinoTransport(),
|
||||
autoLogging: {
|
||||
ignore: (req) => {
|
||||
return req.url.startsWith('/dashboard/');
|
||||
},
|
||||
},
|
||||
serializers: {
|
||||
req: (req) => ({
|
||||
id: req.id,
|
||||
method: req.method,
|
||||
url: req.url,
|
||||
query: req.query,
|
||||
params: req.params,
|
||||
}),
|
||||
res: (res) => ({
|
||||
statusCode: res.statusCode,
|
||||
}),
|
||||
},
|
||||
},
|
||||
}),
|
||||
ConfigModule.forRoot({
|
||||
isGlobal: true,
|
||||
}),
|
||||
@@ -65,6 +97,7 @@ export const CONTROLLERS = [
|
||||
SessionController,
|
||||
ChattingController,
|
||||
ChatsController,
|
||||
ChannelsController,
|
||||
StatusController,
|
||||
ContactsController,
|
||||
GroupsController,
|
||||
@@ -82,11 +115,16 @@ const PROVIDERS = [
|
||||
provide: WAHAHealthCheckService,
|
||||
useClass: WAHAHealthCheckServiceCore,
|
||||
},
|
||||
{
|
||||
provide: APP_INTERCEPTOR,
|
||||
useClass: BufferJsonReplacerInterceptor,
|
||||
},
|
||||
DashboardConfigServiceCore,
|
||||
SwaggerConfigServiceCore,
|
||||
WhatsappConfigService,
|
||||
EngineConfigService,
|
||||
ConsoleLogger,
|
||||
WebsocketGatewayCore,
|
||||
];
|
||||
|
||||
@Module({
|
||||
@@ -96,4 +134,12 @@ const PROVIDERS = [
|
||||
})
|
||||
export class AppModuleCore {
|
||||
constructor(protected config: WhatsappConfigService) {}
|
||||
|
||||
static getHttpsOptions(logger: Logger) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
static appReady(app: INestApplication, logger: Logger) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { InjectPinoLogger, PinoLogger } from 'nestjs-pino';
|
||||
|
||||
import { parseBool } from '../../helpers';
|
||||
|
||||
@@ -7,7 +8,11 @@ import { parseBool } from '../../helpers';
|
||||
export class DashboardConfigServiceCore {
|
||||
public dashboardUri = '/dashboard';
|
||||
|
||||
constructor(protected configService: ConfigService) {}
|
||||
constructor(
|
||||
protected configService: ConfigService,
|
||||
@InjectPinoLogger('DashboardConfigService')
|
||||
protected logger: PinoLogger,
|
||||
) {}
|
||||
|
||||
get enabled(): boolean {
|
||||
const value = this.configService.get('WAHA_DASHBOARD_ENABLED', 'true');
|
||||
|
||||
@@ -1,11 +1,28 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
|
||||
import { parseBool } from '../../helpers';
|
||||
import { WAHAEngine } from '../../structures/enums.dto';
|
||||
import { getEngineName } from '../../version';
|
||||
|
||||
@Injectable()
|
||||
export class EngineConfigService {
|
||||
constructor(protected configService: ConfigService) {}
|
||||
private logger: Logger;
|
||||
|
||||
constructor(protected configService: ConfigService) {
|
||||
this.logger = new Logger('EngineConfigService');
|
||||
}
|
||||
|
||||
getDefaultEngineName(): WAHAEngine {
|
||||
const value = getEngineName();
|
||||
if (value in WAHAEngine) {
|
||||
return WAHAEngine[value];
|
||||
}
|
||||
this.logger.warn(
|
||||
`Unknown WhatsApp default engine WHATSAPP_DEFAULT_ENGINE=${value}. Using WEBJS`,
|
||||
);
|
||||
return WAHAEngine.WEBJS;
|
||||
}
|
||||
|
||||
get shouldPrintQR(): boolean {
|
||||
const value = this.configService.get('WAHA_PRINT_QR', true);
|
||||
|
||||
@@ -1,11 +1,16 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { InjectPinoLogger, PinoLogger } from 'nestjs-pino';
|
||||
|
||||
import { parseBool } from '../../helpers';
|
||||
|
||||
@Injectable()
|
||||
export class SwaggerConfigServiceCore {
|
||||
constructor(protected configService: ConfigService) {}
|
||||
constructor(
|
||||
protected configService: ConfigService,
|
||||
@InjectPinoLogger('SwaggerConfigService')
|
||||
protected logger: PinoLogger,
|
||||
) {}
|
||||
|
||||
get advancedConfigEnabled(): boolean {
|
||||
const value = this.configService.get(
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
import { useMultiFileAuthState } from '@adiwajshing/baileys';
|
||||
import { Store } from 'whatsapp-web.js';
|
||||
|
||||
import { DataStore } from '../../abc/DataStore';
|
||||
import { LocalStore } from '../../storage/LocalStore';
|
||||
import { useMultiFileAuthState } from './useMultiFileAuthState';
|
||||
|
||||
export class NowebAuthFactoryCore {
|
||||
buildAuth(store: DataStore, name: string) {
|
||||
|
||||
@@ -1,29 +1,53 @@
|
||||
import makeWASocket, {
|
||||
Browsers,
|
||||
Contact,
|
||||
DisconnectReason,
|
||||
extractMessageContent,
|
||||
getAggregateVotesInPollMessage,
|
||||
getKeyAuthor,
|
||||
getUrlFromDirectPath,
|
||||
isJidGroup,
|
||||
isJidStatusBroadcast,
|
||||
isRealMessage,
|
||||
jidNormalizedUser,
|
||||
makeCacheableSignalKeyStore,
|
||||
makeInMemoryStore,
|
||||
NewsletterMetadata,
|
||||
normalizeMessageContent,
|
||||
PresenceData,
|
||||
proto,
|
||||
WAMessageContent,
|
||||
WAMessageKey,
|
||||
} from '@adiwajshing/baileys';
|
||||
import { isLidUser } from '@adiwajshing/baileys/lib/WABinary/jid-utils';
|
||||
import { Logger as BaileysLogger } from '@adiwajshing/baileys/node_modules/pino';
|
||||
import { UnprocessableEntityException } from '@nestjs/common';
|
||||
import { NowebInMemoryStore } from '@waha/core/engines/noweb/store/NowebInMemoryStore';
|
||||
import { flipObject, parseBool, splitAt } from '@waha/helpers';
|
||||
import { PairingCodeResponse } from '@waha/structures/auth.dto';
|
||||
import {
|
||||
Channel,
|
||||
ChannelRole,
|
||||
CreateChannelRequest,
|
||||
ListChannelsQuery,
|
||||
} from '@waha/structures/channels.dto';
|
||||
import { GetChatsQuery } from '@waha/structures/chats.dto';
|
||||
import { ContactQuery, ContactRequest } from '@waha/structures/contacts.dto';
|
||||
import {
|
||||
PollVote,
|
||||
PollVotePayload,
|
||||
WAMessageAckBody,
|
||||
} from '@waha/structures/webhooks.dto';
|
||||
import * as Buffer from 'buffer';
|
||||
import { Agent } from 'https';
|
||||
import * as lodash from 'lodash';
|
||||
import { PairingCodeResponse } from 'src/structures/auth.dto';
|
||||
import { toNumber } from 'lodash';
|
||||
import * as NodeCache from 'node-cache';
|
||||
|
||||
import { flipObject, splitAt } from '../../../helpers';
|
||||
import {
|
||||
ChatRequest,
|
||||
CheckNumberStatusQuery,
|
||||
EditMessageRequest,
|
||||
GetMessageQuery,
|
||||
MessageContactVcardRequest,
|
||||
MessageDestination,
|
||||
MessageFileRequest,
|
||||
@@ -39,7 +63,6 @@ import {
|
||||
SendSeenRequest,
|
||||
WANumberExistResult,
|
||||
} from '../../../structures/chatting.dto';
|
||||
import { ContactQuery, ContactRequest } from '../../../structures/contacts.dto';
|
||||
import {
|
||||
ACK_UNKNOWN,
|
||||
SECOND,
|
||||
@@ -62,15 +85,16 @@ import {
|
||||
WAMessageReaction,
|
||||
} from '../../../structures/responses.dto';
|
||||
import { MeInfo } from '../../../structures/sessions.dto';
|
||||
import { BROADCAST_ID, TextStatus } from '../../../structures/status.dto';
|
||||
import {
|
||||
PollVote,
|
||||
PollVotePayload,
|
||||
WAMessageAckBody,
|
||||
} from '../../../structures/webhooks.dto';
|
||||
BROADCAST_ID,
|
||||
DeleteStatusRequest,
|
||||
TextStatus,
|
||||
} from '../../../structures/status.dto';
|
||||
import { IEngineMediaProcessor } from '../../abc/media.abc';
|
||||
import {
|
||||
ensureSuffix,
|
||||
getChannelInviteLink,
|
||||
isNewsletter,
|
||||
WAHAInternalEvent,
|
||||
WhatsappSession,
|
||||
} from '../../abc/session.abc';
|
||||
@@ -82,12 +106,13 @@ import { toVcard } from '../../helpers';
|
||||
import { createAgentProxy } from '../../helpers.proxy';
|
||||
import { QR } from '../../QR';
|
||||
import { NowebAuthFactoryCore } from './NowebAuthFactoryCore';
|
||||
import { INowebStore } from './store/INowebStore';
|
||||
import { NowebPersistentStore } from './store/NowebPersistentStore';
|
||||
import { NowebStorageFactoryCore } from './store/NowebStorageFactoryCore';
|
||||
import { extractMediaContent } from './utils';
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
const QRCode = require('qrcode');
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
const logger = require('pino')();
|
||||
|
||||
export const BaileysEvents = {
|
||||
CONNECTION_UPDATE: 'connection.update',
|
||||
@@ -109,21 +134,38 @@ const PresenceStatuses = {
|
||||
const ToEnginePresenceStatus = flipObject(PresenceStatuses);
|
||||
|
||||
export class WhatsappSessionNoWebCore extends WhatsappSession {
|
||||
private START_ATTEMPT_DELAY_SECONDS = 2;
|
||||
private AUTO_RESTART_AFTER_SECONDS = 30 * 60;
|
||||
|
||||
engine = WAHAEngine.NOWEB;
|
||||
authFactory = new NowebAuthFactoryCore();
|
||||
private restartTimeoutId: null | ReturnType<typeof setTimeout> = null;
|
||||
storageFactory = new NowebStorageFactoryCore();
|
||||
private startTimeoutId: null | ReturnType<typeof setTimeout> = null;
|
||||
private autoRestartTimeoutId: null | ReturnType<typeof setTimeout> = null;
|
||||
private msgRetryCounterCache: NodeCache;
|
||||
protected engineLogger: BaileysLogger;
|
||||
|
||||
get listenConnectionEventsFromTheStart() {
|
||||
return true;
|
||||
}
|
||||
|
||||
sock: any;
|
||||
store: any;
|
||||
sock: ReturnType<typeof makeWASocket>;
|
||||
store: INowebStore;
|
||||
private qr: QR;
|
||||
|
||||
public constructor(config) {
|
||||
super(config);
|
||||
this.qr = new QR();
|
||||
// external map to store retry counts of messages when decryption/encryption fails
|
||||
// keep this out of the socket itself, to prevent a message decryption/encryption loop across socket restarts
|
||||
this.msgRetryCounterCache = new NodeCache({
|
||||
stdTTL: 60 * 60, // 1 hour
|
||||
useClones: false,
|
||||
});
|
||||
|
||||
this.engineLogger = this.loggerBuilder.child({
|
||||
name: 'NOWEBEngine',
|
||||
}) as unknown as BaileysLogger;
|
||||
}
|
||||
|
||||
start() {
|
||||
@@ -131,33 +173,40 @@ export class WhatsappSessionNoWebCore extends WhatsappSession {
|
||||
this.buildClient();
|
||||
}
|
||||
|
||||
getSocketConfig(agent, state) {
|
||||
getSocketConfig(agent, state): any {
|
||||
const fullSyncEnabled = this.sessionConfig?.noweb?.store?.fullSync || false;
|
||||
const browser = fullSyncEnabled
|
||||
? Browsers.ubuntu('Desktop')
|
||||
: Browsers.ubuntu('Chrome');
|
||||
return {
|
||||
agent: agent,
|
||||
fetchAgent: agent,
|
||||
auth: {
|
||||
creds: state.creds,
|
||||
/** caching makes the store faster to send/recv messages */
|
||||
keys: makeCacheableSignalKeyStore(state.keys, logger),
|
||||
keys: makeCacheableSignalKeyStore(state.keys, this.engineLogger),
|
||||
},
|
||||
printQRInTerminal: false,
|
||||
browser: Browsers.ubuntu('Chrome'),
|
||||
logger: logger,
|
||||
browser: browser,
|
||||
logger: this.engineLogger,
|
||||
mobile: false,
|
||||
defaultQueryTimeoutMs: undefined,
|
||||
defaultQueryTimeoutMs: 120_000,
|
||||
keepAliveIntervalMs: 30_000,
|
||||
getMessage: (key) => this.getMessage(key),
|
||||
syncFullHistory: fullSyncEnabled,
|
||||
msgRetryCounterCache: this.msgRetryCounterCache,
|
||||
};
|
||||
}
|
||||
|
||||
async makeSocket() {
|
||||
async makeSocket(): Promise<any> {
|
||||
const { state, saveCreds } = await this.authFactory.buildAuth(
|
||||
this.sessionStore,
|
||||
this.name,
|
||||
);
|
||||
const agent = this.makeAgent();
|
||||
const socketConfig = this.getSocketConfig(agent, state);
|
||||
const sock: any = makeWASocket(socketConfig);
|
||||
sock.ev.on(BaileysEvents.CREDS_UPDATE, saveCreds);
|
||||
const sock = makeWASocket(socketConfig);
|
||||
sock.ev.on('creds.update', saveCreds);
|
||||
return sock;
|
||||
}
|
||||
|
||||
@@ -168,14 +217,34 @@ export class WhatsappSessionNoWebCore extends WhatsappSession {
|
||||
return createAgentProxy(this.proxyConfig);
|
||||
}
|
||||
|
||||
connectStore() {
|
||||
this.log.debug(`Connecting store...`);
|
||||
async connectStore() {
|
||||
this.logger.debug(`Connecting store...`);
|
||||
if (!this.store) {
|
||||
this.log.debug(`Making a new auth store...`);
|
||||
this.store = makeInMemoryStore({ logger: logger });
|
||||
this.logger.debug(`Making a new store...`);
|
||||
const storeEnabled = this.sessionConfig?.noweb?.store?.enabled || false;
|
||||
if (storeEnabled) {
|
||||
this.logger.debug('Using NowebPersistentStore');
|
||||
const storage = this.storageFactory.createStorage(
|
||||
this.sessionStore,
|
||||
this.name,
|
||||
);
|
||||
this.store = new NowebPersistentStore(
|
||||
this.loggerBuilder.child({ name: NowebPersistentStore.name }),
|
||||
storage,
|
||||
);
|
||||
await this.store.init().catch((err) => {
|
||||
this.logger.error(`Failed to initialize storage or store: ${err}`);
|
||||
this.status = WAHASessionStatus.FAILED;
|
||||
this.end();
|
||||
throw err;
|
||||
});
|
||||
} else {
|
||||
this.logger.debug('Using NowebInMemoryStore');
|
||||
this.store = new NowebInMemoryStore();
|
||||
}
|
||||
}
|
||||
this.log.debug(`Binding store to socket...`);
|
||||
this.store.bind(this.sock.ev);
|
||||
this.logger.debug(`Binding store to socket...`);
|
||||
this.store.bind(this.sock.ev, this.sock);
|
||||
}
|
||||
|
||||
resubscribeToKnownPresences() {
|
||||
@@ -186,14 +255,33 @@ export class WhatsappSessionNoWebCore extends WhatsappSession {
|
||||
|
||||
async buildClient() {
|
||||
this.sock = await this.makeSocket();
|
||||
this.connectStore();
|
||||
this.issueMessageUpdateOnEdits();
|
||||
this.issuePresenceUpdateOnMessageUpsert();
|
||||
if (this.isDebugEnabled()) {
|
||||
this.listenEngineEventsInDebugMode();
|
||||
}
|
||||
await this.connectStore();
|
||||
if (this.listenConnectionEventsFromTheStart) {
|
||||
this.listenConnectionEvents();
|
||||
this.events.emit(WAHAInternalEvent.ENGINE_START);
|
||||
}
|
||||
this.enableAutoRestart();
|
||||
}
|
||||
|
||||
protected enableAutoRestart() {
|
||||
if (this.autoRestartTimeoutId) {
|
||||
return;
|
||||
}
|
||||
|
||||
// add random delay to avoid multiple restarts at the same time
|
||||
const shiftSeconds = Math.floor(Math.random() * 30);
|
||||
const delay = this.AUTO_RESTART_AFTER_SECONDS + shiftSeconds;
|
||||
this.logger.debug(`Auto-restart is enabled, after ${delay} seconds`);
|
||||
this.autoRestartTimeoutId = setTimeout(() => {
|
||||
this.autoRestartTimeoutId = null;
|
||||
this.logger.info('Auto-restarting the client connection...');
|
||||
this.sock?.end(new Error('auto-restart'));
|
||||
}, delay * SECOND);
|
||||
}
|
||||
|
||||
protected async getMessage(
|
||||
@@ -208,27 +296,29 @@ export class WhatsappSessionNoWebCore extends WhatsappSession {
|
||||
|
||||
protected listenEngineEventsInDebugMode() {
|
||||
this.sock.ev.process((events) => {
|
||||
this.log.debug(`NOWEB events: ${JSON.stringify(events)}`);
|
||||
this.logger.debug({ events: events }, `NOWEB events`);
|
||||
});
|
||||
}
|
||||
|
||||
private restartClient() {
|
||||
if (this.restartTimeoutId) {
|
||||
this.log.log(
|
||||
if (this.startTimeoutId) {
|
||||
this.logger.info(
|
||||
'Request to restart is already in progress, ignoring this request',
|
||||
);
|
||||
return;
|
||||
}
|
||||
this.log.log('Setting up client restart in 2 seconds...');
|
||||
this.restartTimeoutId = setTimeout(() => {
|
||||
this.restartTimeoutId = undefined;
|
||||
this.logger.info(
|
||||
`Setting up client start in ${this.START_ATTEMPT_DELAY_SECONDS} seconds...`,
|
||||
);
|
||||
this.startTimeoutId = setTimeout(() => {
|
||||
this.startTimeoutId = undefined;
|
||||
this.buildClient();
|
||||
}, 2 * SECOND);
|
||||
}, this.START_ATTEMPT_DELAY_SECONDS * SECOND);
|
||||
}
|
||||
|
||||
protected listenConnectionEvents() {
|
||||
this.log.debug(`Start listening ${BaileysEvents.CONNECTION_UPDATE}...`);
|
||||
this.sock.ev.on(BaileysEvents.CONNECTION_UPDATE, async (update) => {
|
||||
this.logger.debug(`Start listening ${BaileysEvents.CONNECTION_UPDATE}...`);
|
||||
this.sock.ev.on('connection.update', async (update) => {
|
||||
const { connection, lastDisconnect, qr, isNewLogin } = update;
|
||||
if (isNewLogin) {
|
||||
this.restartClient();
|
||||
@@ -239,19 +329,21 @@ export class WhatsappSessionNoWebCore extends WhatsappSession {
|
||||
return;
|
||||
} else if (connection === 'close') {
|
||||
const shouldReconnect =
|
||||
// @ts-ignore: Property output does not exist on type 'Error'
|
||||
lastDisconnect.error?.output?.statusCode !==
|
||||
DisconnectReason.loggedOut;
|
||||
this.log.error(
|
||||
'connection closed due to ',
|
||||
lastDisconnect.error,
|
||||
', reconnecting ',
|
||||
shouldReconnect,
|
||||
);
|
||||
this.qr.save('');
|
||||
// reconnect if not logged out
|
||||
if (shouldReconnect) {
|
||||
this.logger.info(
|
||||
`Connection closed due to '${lastDisconnect.error}', reconnecting...`,
|
||||
);
|
||||
this.restartClient();
|
||||
} else {
|
||||
this.logger.error(
|
||||
`Connection closed due to '${lastDisconnect.error}', do not reconnect the session.`,
|
||||
);
|
||||
await this.end();
|
||||
this.status = WAHASessionStatus.FAILED;
|
||||
}
|
||||
}
|
||||
@@ -267,24 +359,90 @@ export class WhatsappSessionNoWebCore extends WhatsappSession {
|
||||
}
|
||||
|
||||
async stop() {
|
||||
this.sock?.ev?.removeAllListeners();
|
||||
this.sock?.ws?.removeAllListeners();
|
||||
this.sock?.ws?.close();
|
||||
this.status = WAHASessionStatus.STOPPED;
|
||||
this.events.removeAllListeners();
|
||||
await this.end();
|
||||
return;
|
||||
}
|
||||
|
||||
async getSessionMeInfo(): Promise<MeInfo | null> {
|
||||
const me = this.sock.authState?.creds?.me;
|
||||
private issueMessageUpdateOnEdits() {
|
||||
// Remove it after it's been merged
|
||||
// https://github.com/WhiskeySockets/Baileys/pull/855/
|
||||
this.sock.ev.on('messages.upsert', ({ messages }) => {
|
||||
for (const message of messages) {
|
||||
const content = normalizeMessageContent(message.message);
|
||||
const protocolMsg = content?.protocolMessage;
|
||||
if (
|
||||
protocolMsg !== null &&
|
||||
protocolMsg !== undefined &&
|
||||
protocolMsg.editedMessage
|
||||
) {
|
||||
this.sock?.ev.emit('messages.update', [
|
||||
{
|
||||
key: {
|
||||
...message.key,
|
||||
id: protocolMsg.key.id,
|
||||
},
|
||||
update: { message: protocolMsg.editedMessage },
|
||||
},
|
||||
]);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private issuePresenceUpdateOnMessageUpsert() {
|
||||
//
|
||||
// Fix for "typing" after sending a message
|
||||
// https://github.com/devlikeapro/waha/issues/379
|
||||
//
|
||||
this.sock.ev.on('messages.upsert', ({ messages }) => {
|
||||
const meId = this.sock?.authState?.creds?.me?.id;
|
||||
for (const message of messages) {
|
||||
if (!isRealMessage(message, meId)) {
|
||||
continue;
|
||||
}
|
||||
if (message.key.fromMe) {
|
||||
continue;
|
||||
}
|
||||
const jid = message.key.remoteJid;
|
||||
const participant = message.key.participant || jid;
|
||||
const jidPresences = this.store?.presences?.[jid];
|
||||
const participantPresence = jidPresences?.[participant];
|
||||
if (participantPresence?.lastKnownPresence === 'composing') {
|
||||
this.logger.debug(
|
||||
`Fixing presence for '${participant}' in '${jid} since it's typing`,
|
||||
);
|
||||
const presence: PresenceData = { lastKnownPresence: 'available' };
|
||||
this.sock?.ev?.emit('presence.update', {
|
||||
id: jid,
|
||||
presences: { [participant]: presence },
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private async end() {
|
||||
clearTimeout(this.autoRestartTimeoutId);
|
||||
clearTimeout(this.startTimeoutId);
|
||||
// @ts-ignore
|
||||
this.sock?.ev?.removeAllListeners();
|
||||
this.sock?.ws?.removeAllListeners();
|
||||
this.sock?.end(undefined);
|
||||
this.store?.close();
|
||||
}
|
||||
|
||||
getSessionMeInfo(): MeInfo | null {
|
||||
const me = this.sock?.authState?.creds?.me;
|
||||
if (!me) {
|
||||
return null;
|
||||
}
|
||||
const meId = jidNormalizedUser(me.id);
|
||||
const meInfo: MeInfo = {
|
||||
return {
|
||||
id: toCusFormat(meId),
|
||||
pushName: me.name,
|
||||
};
|
||||
return meInfo;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -309,7 +467,7 @@ export class WhatsappSessionNoWebCore extends WhatsappSession {
|
||||
}
|
||||
|
||||
if (this.status == WAHASessionStatus.STARTING) {
|
||||
this.log.debug('Waiting for connection update...');
|
||||
this.logger.debug('Waiting for connection update...');
|
||||
await this.sock.waitForConnectionUpdate((update) => !!update.qr);
|
||||
}
|
||||
|
||||
@@ -318,12 +476,12 @@ export class WhatsappSessionNoWebCore extends WhatsappSession {
|
||||
throw new UnprocessableEntityException(err);
|
||||
}
|
||||
|
||||
this.log.log(`Requesting pairing code for '${phoneNumber}'...`);
|
||||
this.logger.info(`Requesting pairing code for '${phoneNumber}'...`);
|
||||
const code: string = await this.sock.requestPairingCode(phoneNumber);
|
||||
// show it as ABCD-ABCD
|
||||
const parts = splitAt(code, 4);
|
||||
const codeRepr = parts.join('-');
|
||||
this.log.log(`Your code: ${codeRepr}`);
|
||||
this.logger.info(`Your code: ${codeRepr}`);
|
||||
return { code: codeRepr };
|
||||
}
|
||||
|
||||
@@ -361,7 +519,7 @@ export class WhatsappSessionNoWebCore extends WhatsappSession {
|
||||
}
|
||||
|
||||
sendText(request: MessageTextRequest) {
|
||||
const chatId = this.ensureSuffix(request.chatId);
|
||||
const chatId = toJID(this.ensureSuffix(request.chatId));
|
||||
const message = {
|
||||
text: request.text,
|
||||
mentions: request.mentions?.map(toJID),
|
||||
@@ -372,7 +530,14 @@ export class WhatsappSessionNoWebCore extends WhatsappSession {
|
||||
public deleteMessage(chatId: string, messageId: string) {
|
||||
const jid = toJID(this.ensureSuffix(chatId));
|
||||
const key = parseMessageId(messageId);
|
||||
return this.sock.sendMessage(jid, { delete: key });
|
||||
const options = {
|
||||
statusJidList: [
|
||||
'79521562380@s.whatsapp.net',
|
||||
'79069571990@s.whatsapp.net',
|
||||
'79069471990@s.whatsapp.net',
|
||||
],
|
||||
};
|
||||
return this.sock.sendMessage(jid, { delete: key }, options);
|
||||
}
|
||||
|
||||
public editMessage(
|
||||
@@ -391,7 +556,7 @@ export class WhatsappSessionNoWebCore extends WhatsappSession {
|
||||
}
|
||||
|
||||
async sendContactVCard(request: MessageContactVcardRequest) {
|
||||
const chatId = this.ensureSuffix(request.chatId);
|
||||
const chatId = toJID(this.ensureSuffix(request.chatId));
|
||||
const contacts = request.contacts.map((el) => ({ vcard: toVcard(el) }));
|
||||
await this.sock.sendMessage(chatId, {
|
||||
contacts: {
|
||||
@@ -453,7 +618,7 @@ export class WhatsappSessionNoWebCore extends WhatsappSession {
|
||||
|
||||
sendLinkPreview(request: MessageLinkPreviewRequest) {
|
||||
const text = `${request.title}\n${request.url}`;
|
||||
const chatId = this.ensureSuffix(request.chatId);
|
||||
const chatId = toJID(this.ensureSuffix(request.chatId));
|
||||
return this.sock.sendMessage(chatId, { text: text });
|
||||
}
|
||||
|
||||
@@ -475,6 +640,31 @@ export class WhatsappSessionNoWebCore extends WhatsappSession {
|
||||
return this.sock.sendPresenceUpdate('paused', request.chatId);
|
||||
}
|
||||
|
||||
async getMessages(query: GetMessageQuery) {
|
||||
return this.getChatMessages(query.chatId, query.limit, query.downloadMedia);
|
||||
}
|
||||
|
||||
public async getChatMessages(
|
||||
chatId: string,
|
||||
limit: number,
|
||||
downloadMedia: boolean,
|
||||
) {
|
||||
downloadMedia = parseBool(downloadMedia);
|
||||
const messages = await this.store.getMessagesByJid(
|
||||
toJID(chatId),
|
||||
toNumber(limit),
|
||||
);
|
||||
const result = [];
|
||||
for (const msg of messages) {
|
||||
const wamsg = await this.processIncomingMessage(msg, downloadMedia);
|
||||
if (!wamsg) {
|
||||
continue;
|
||||
}
|
||||
result.push(wamsg);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
async setReaction(request: MessageReactionRequest) {
|
||||
const key = parseMessageId(request.messageId);
|
||||
const reactionMessage = {
|
||||
@@ -500,14 +690,31 @@ export class WhatsappSessionNoWebCore extends WhatsappSession {
|
||||
}
|
||||
|
||||
/**
|
||||
* Contacts methods
|
||||
* Chats methods
|
||||
*/
|
||||
getContact(query: ContactQuery) {
|
||||
throw new NotImplementedByEngineError();
|
||||
|
||||
async getChats(query: GetChatsQuery) {
|
||||
const chats = await this.store.getChats(query.limit, query.offset);
|
||||
// Remove unreadCount, it's not ready yet
|
||||
chats.forEach((chat) => delete chat.unreadCount);
|
||||
return chats;
|
||||
}
|
||||
|
||||
getContacts() {
|
||||
throw new NotImplementedByEngineError();
|
||||
/**
|
||||
* Contacts methods
|
||||
*/
|
||||
async getContact(query: ContactQuery) {
|
||||
const jid = toJID(query.contactId);
|
||||
const contact = await this.store.getContactById(jid);
|
||||
if (!contact) {
|
||||
return null;
|
||||
}
|
||||
return this.toWAContact(contact);
|
||||
}
|
||||
|
||||
async getContacts() {
|
||||
const contacts = await this.store.getContacts();
|
||||
return contacts.map(this.toWAContact);
|
||||
}
|
||||
|
||||
public async getContactAbout(query: ContactQuery) {
|
||||
@@ -615,20 +822,13 @@ export class WhatsappSessionNoWebCore extends WhatsappSession {
|
||||
}
|
||||
|
||||
public async getPresence(chatId: string): Promise<WAHAChatPresences> {
|
||||
await this.subscribePresence(chatId);
|
||||
const remoteJid = toJID(chatId);
|
||||
const storedPresences = this.store.presences[remoteJid];
|
||||
return this.toWahaPresences(remoteJid, storedPresences || []);
|
||||
}
|
||||
|
||||
public async subscribePresence(chatId: string): Promise<void> {
|
||||
const remoteJid = toJID(chatId);
|
||||
if (this.store.presences[remoteJid]) {
|
||||
return;
|
||||
if (!(remoteJid in this.store.presences)) {
|
||||
this.store.presences[remoteJid] = [];
|
||||
await this.sock.presenceSubscribe(remoteJid);
|
||||
}
|
||||
// Have no info - subscribe to listen for events
|
||||
await this.sock.presenceSubscribe(remoteJid);
|
||||
return;
|
||||
const result = this.store.presences[remoteJid];
|
||||
return this.toWahaPresences(remoteJid, result);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -636,14 +836,120 @@ export class WhatsappSessionNoWebCore extends WhatsappSession {
|
||||
*/
|
||||
public sendTextStatus(status: TextStatus) {
|
||||
const message = { text: status.text };
|
||||
const JIDs = status.contacts.map(toJID);
|
||||
this.upsertMeInJIDs(JIDs);
|
||||
const options = {
|
||||
backgroundColor: status.backgroundColor,
|
||||
font: status.font,
|
||||
statusJidList: status.contacts.map(toJID),
|
||||
statusJidList: JIDs,
|
||||
};
|
||||
|
||||
return this.sock.sendMessage(BROADCAST_ID, message, options);
|
||||
}
|
||||
|
||||
public deleteStatus(request: DeleteStatusRequest) {
|
||||
const messageId = request.id;
|
||||
let key: any;
|
||||
if (messageId.includes('_')) {
|
||||
key = parseMessageId(messageId);
|
||||
} else {
|
||||
key = { id: messageId };
|
||||
}
|
||||
|
||||
key.fromMe = true;
|
||||
key.remoteJid = BROADCAST_ID;
|
||||
const JIDs = request.contacts.map(toJID);
|
||||
const options = {
|
||||
statusJidList: JIDs,
|
||||
};
|
||||
return this.sock.sendMessage(BROADCAST_ID, { delete: key }, options);
|
||||
}
|
||||
|
||||
protected upsertMeInJIDs(JIDs: string[]) {
|
||||
if (!this.sock?.authState?.creds?.me) {
|
||||
return;
|
||||
}
|
||||
const myJID = jidNormalizedUser(this.sock.authState.creds.me.id);
|
||||
if (!JIDs.includes(myJID)) {
|
||||
JIDs.push(myJID);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Channels methods
|
||||
*/
|
||||
protected toChannel(newsletter: NewsletterMetadata): Channel {
|
||||
const role =
|
||||
// @ts-ignore
|
||||
newsletter.viewer_metadata?.role ||
|
||||
(newsletter.viewer_metadata?.view_role as ChannelRole) ||
|
||||
ChannelRole.GUEST;
|
||||
const preview = newsletter.preview
|
||||
? getUrlFromDirectPath(newsletter.preview)
|
||||
: null;
|
||||
const picture = newsletter.picture
|
||||
? getUrlFromDirectPath(newsletter.picture)
|
||||
: null;
|
||||
return {
|
||||
id: newsletter.id,
|
||||
name: newsletter.name,
|
||||
description: newsletter.description,
|
||||
invite: getChannelInviteLink(newsletter.invite),
|
||||
preview: preview,
|
||||
picture: picture,
|
||||
verified: newsletter.verification === 'VERIFIED',
|
||||
role: role,
|
||||
};
|
||||
}
|
||||
|
||||
public async channelsList(query: ListChannelsQuery): Promise<Channel[]> {
|
||||
const newsletters = await this.sock.newsletterSubscribed();
|
||||
let channels = newsletters.map(this.toChannel);
|
||||
if (query.role) {
|
||||
// @ts-ignore
|
||||
channels = channels.filter((channel) => channel.role === query.role);
|
||||
}
|
||||
return channels;
|
||||
}
|
||||
|
||||
public async channelsCreateChannel(request: CreateChannelRequest) {
|
||||
const newsletter = await this.sock.newsletterCreate(
|
||||
request.name,
|
||||
request.description,
|
||||
);
|
||||
return this.toChannel(newsletter);
|
||||
}
|
||||
|
||||
public async channelsGetChannel(id: string) {
|
||||
const newsletter = await this.sock.newsletterMetadata('jid', id);
|
||||
return this.toChannel(newsletter);
|
||||
}
|
||||
|
||||
public async channelsGetChannelByInviteCode(inviteCode: string) {
|
||||
const newsletter = await this.sock.newsletterMetadata('invite', inviteCode);
|
||||
return this.toChannel(newsletter);
|
||||
}
|
||||
|
||||
public async channelsDeleteChannel(id: string) {
|
||||
return await this.sock.newsletterDelete(id);
|
||||
}
|
||||
|
||||
public async channelsFollowChannel(id: string): Promise<void> {
|
||||
return await this.sock.newsletterFollow(id);
|
||||
}
|
||||
|
||||
public async channelsUnfollowChannel(id: string): Promise<void> {
|
||||
return await this.sock.newsletterUnfollow(id);
|
||||
}
|
||||
|
||||
public async channelsMuteChannel(id: string): Promise<void> {
|
||||
return await this.sock.newsletterMute(id);
|
||||
}
|
||||
|
||||
public async channelsUnmuteChannel(id: string): Promise<void> {
|
||||
return await this.sock.newsletterUnmute(id);
|
||||
}
|
||||
|
||||
/**
|
||||
* END - Methods for API
|
||||
*/
|
||||
@@ -651,23 +957,23 @@ export class WhatsappSessionNoWebCore extends WhatsappSession {
|
||||
subscribeEngineEvent(event, handler): boolean {
|
||||
switch (event) {
|
||||
case WAHAEvents.MESSAGE:
|
||||
this.sock.ev.on(BaileysEvents.MESSAGES_UPSERT, ({ messages }) => {
|
||||
this.sock.ev.on('messages.upsert', ({ messages }) => {
|
||||
this.handleIncomingMessages(messages, handler, false);
|
||||
});
|
||||
return true;
|
||||
case WAHAEvents.MESSAGE_REACTION:
|
||||
this.sock.ev.on(BaileysEvents.MESSAGES_UPSERT, ({ messages }) => {
|
||||
this.sock.ev.on('messages.upsert', ({ messages }) => {
|
||||
const reactions = this.processMessageReaction(messages);
|
||||
reactions.map(handler);
|
||||
});
|
||||
return true;
|
||||
case WAHAEvents.MESSAGE_ANY:
|
||||
this.sock.ev.on(BaileysEvents.MESSAGES_UPSERT, ({ messages }) =>
|
||||
this.sock.ev.on('messages.upsert', ({ messages }) =>
|
||||
this.handleIncomingMessages(messages, handler, true),
|
||||
);
|
||||
return true;
|
||||
case WAHAEvents.MESSAGE_ACK: // Direct message ack
|
||||
this.sock.ev.on(BaileysEvents.MESSAGES_UPDATE, (events) => {
|
||||
this.sock.ev.on('messages.update', (events) => {
|
||||
events
|
||||
.filter(isMine)
|
||||
.filter(isAckUpdateMessageEvent)
|
||||
@@ -675,7 +981,7 @@ export class WhatsappSessionNoWebCore extends WhatsappSession {
|
||||
.forEach(handler);
|
||||
});
|
||||
// Group message ack
|
||||
this.sock.ev.on(BaileysEvents.MESSAGE_RECEIPT_UPDATE, (events) => {
|
||||
this.sock.ev.on('message-receipt.update', (events) => {
|
||||
events
|
||||
.filter(isMine)
|
||||
.map(this.convertMessageReceiptUpdateToMessageAck)
|
||||
@@ -683,25 +989,25 @@ export class WhatsappSessionNoWebCore extends WhatsappSession {
|
||||
});
|
||||
return true;
|
||||
case WAHAEvents.STATE_CHANGE:
|
||||
this.sock.ev.on(BaileysEvents.CONNECTION_UPDATE, handler);
|
||||
this.sock.ev.on('connection.update', handler);
|
||||
return true;
|
||||
case WAHAEvents.GROUP_JOIN:
|
||||
this.sock.ev.on(BaileysEvents.GROUPS_UPSERT, handler);
|
||||
this.sock.ev.on('groups.upsert', handler);
|
||||
return true;
|
||||
case WAHAEvents.PRESENCE_UPDATE:
|
||||
this.sock.ev.on(BaileysEvents.PRESENCE_UPDATE, (data) =>
|
||||
this.sock.ev.on('presence.update', (data) =>
|
||||
handler(this.toWahaPresences(data.id, data.presences)),
|
||||
);
|
||||
return true;
|
||||
case WAHAEvents.POLL_VOTE:
|
||||
this.sock.ev.on(BaileysEvents.MESSAGES_UPDATE, (events) => {
|
||||
this.sock.ev.on('messages.update', (events) => {
|
||||
events.forEach((event) =>
|
||||
this.handleMessagesUpdatePollVote(event, handler),
|
||||
);
|
||||
});
|
||||
return true;
|
||||
case WAHAEvents.POLL_VOTE_FAILED:
|
||||
this.sock.ev.on(BaileysEvents.MESSAGES_UPSERT, ({ messages }) => {
|
||||
this.sock.ev.on('messages.upsert', ({ messages }) => {
|
||||
messages.forEach((message) =>
|
||||
this.handleMessageUpsertPollVoteFailed(message, handler),
|
||||
);
|
||||
@@ -714,18 +1020,16 @@ export class WhatsappSessionNoWebCore extends WhatsappSession {
|
||||
|
||||
private handleIncomingMessages(messages, handler, includeFromMe) {
|
||||
for (const message of messages) {
|
||||
// if there is no text or media message
|
||||
if (!message) return;
|
||||
if (!message.message) return;
|
||||
// Ignore reactions, we have dedicated handler for that
|
||||
if (message.message.reactionMessage) return;
|
||||
// Ignore poll votes, we have dedicated handler for that
|
||||
if (message.message.pollUpdateMessage) return;
|
||||
// Do not include my messages
|
||||
if (!includeFromMe && message.key.fromMe) {
|
||||
continue;
|
||||
}
|
||||
this.processIncomingMessage(message).then(handler);
|
||||
this.processIncomingMessage(message).then((msg) => {
|
||||
if (!msg) {
|
||||
return;
|
||||
}
|
||||
handler(msg);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -757,14 +1061,32 @@ export class WhatsappSessionNoWebCore extends WhatsappSession {
|
||||
return reactions;
|
||||
}
|
||||
|
||||
private processIncomingMessage(message) {
|
||||
return this.downloadMedia(message)
|
||||
.then(this.toWAMessage)
|
||||
.catch((error) => {
|
||||
this.log.error('Failed to process incoming message');
|
||||
this.log.error(error);
|
||||
console.trace(error);
|
||||
});
|
||||
private async processIncomingMessage(message, downloadMedia = true) {
|
||||
// if there is no text or media message
|
||||
if (!message) return;
|
||||
if (!message.message) return;
|
||||
// Ignore reactions, we have dedicated handler for that
|
||||
if (message.message.reactionMessage) return;
|
||||
// Ignore poll votes, we have dedicated handler for that
|
||||
if (message.message.pollUpdateMessage) return;
|
||||
|
||||
if (downloadMedia) {
|
||||
try {
|
||||
message = await this.downloadMedia(message);
|
||||
} catch (e) {
|
||||
this.logger.error('Failed when tried to download media for a message');
|
||||
this.logger.error(e, e.stack);
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
return await this.toWAMessage(message);
|
||||
} catch (error) {
|
||||
this.logger.error('Failed to process incoming message');
|
||||
this.logger.error(error);
|
||||
console.trace(error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
protected toWAMessage(message): Promise<WAMessage> {
|
||||
@@ -773,15 +1095,16 @@ export class WhatsappSessionNoWebCore extends WhatsappSession {
|
||||
let body = message.message.conversation;
|
||||
if (!body) {
|
||||
// Some of the messages have no conversation, but instead have text in extendedTextMessage
|
||||
// https://github.com/devlikeapro/whatsapp-http-api/issues/90
|
||||
// https://github.com/devlikeapro/waha/issues/90
|
||||
body = message.message.extendedTextMessage?.text;
|
||||
}
|
||||
if (!body) {
|
||||
// Populate from caption
|
||||
const mediaContent = extractMediaContent(message.message);
|
||||
// @ts-ignore - AudioMessage doesn't have caption field
|
||||
body = mediaContent.caption;
|
||||
body = mediaContent?.caption;
|
||||
}
|
||||
const ack = message.ack || message.status - 1;
|
||||
return Promise.resolve({
|
||||
id: id,
|
||||
timestamp: message.messageTimestamp,
|
||||
@@ -795,15 +1118,24 @@ export class WhatsappSessionNoWebCore extends WhatsappSession {
|
||||
media: message.media,
|
||||
mediaUrl: message.media?.url,
|
||||
// @ts-ignore
|
||||
ack: message.ack,
|
||||
ack: ack,
|
||||
// @ts-ignore
|
||||
ackName: WAMessageAck[message.ack] || ACK_UNKNOWN,
|
||||
ackName: WAMessageAck[ack] || ACK_UNKNOWN,
|
||||
location: message.location,
|
||||
vCards: message.vCards,
|
||||
_data: message,
|
||||
});
|
||||
}
|
||||
|
||||
protected toWAContact(contact: Contact) {
|
||||
contact.id = toCusFormat(contact.id);
|
||||
// @ts-ignore
|
||||
contact.pushname = contact.notify;
|
||||
// @ts-ignore
|
||||
delete contact.notify;
|
||||
return contact;
|
||||
}
|
||||
|
||||
protected convertMessageUpdateToMessageAck(event): WAMessageAckBody {
|
||||
const message = event;
|
||||
const fromToParticipant = getFromToParticipant(message);
|
||||
@@ -919,8 +1251,8 @@ export class WhatsappSessionNoWebCore extends WhatsappSession {
|
||||
}
|
||||
|
||||
private toWahaPresences(
|
||||
remoteJid,
|
||||
storedPresences: PresenceData[],
|
||||
remoteJid: string,
|
||||
storedPresences: { [participant: string]: PresenceData },
|
||||
): WAHAChatPresences {
|
||||
const presences: WAHAPresenceData[] = [];
|
||||
for (const participant in storedPresences) {
|
||||
@@ -977,9 +1309,21 @@ export class EngineMediaProcessor implements IEngineMediaProcessor<any> {
|
||||
* Convert from 11111111111@s.whatsapp.net to 11111111111@c.us
|
||||
*/
|
||||
function toCusFormat(remoteJid) {
|
||||
if (!remoteJid) {
|
||||
return remoteJid;
|
||||
}
|
||||
if (isJidGroup(remoteJid)) {
|
||||
return remoteJid;
|
||||
}
|
||||
if (isJidStatusBroadcast(remoteJid)) {
|
||||
return remoteJid;
|
||||
}
|
||||
if (isLidUser(remoteJid)) {
|
||||
return remoteJid;
|
||||
}
|
||||
if (isNewsletter(remoteJid)) {
|
||||
return remoteJid;
|
||||
}
|
||||
if (!remoteJid) {
|
||||
return;
|
||||
}
|
||||
@@ -998,6 +1342,12 @@ export function toJID(chatId) {
|
||||
if (isJidGroup(chatId)) {
|
||||
return chatId;
|
||||
}
|
||||
if (isJidStatusBroadcast(chatId)) {
|
||||
return chatId;
|
||||
}
|
||||
if (isNewsletter(chatId)) {
|
||||
return chatId;
|
||||
}
|
||||
const number = chatId.split('@')[0];
|
||||
return number + '@s.whatsapp.net';
|
||||
}
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
import { Chat } from '@adiwajshing/baileys';
|
||||
|
||||
export interface IChatRepository {
|
||||
getAll(): Promise<Chat[]>;
|
||||
|
||||
getAllWithMessages(limit?: number, offset?: number): Promise<Chat[]>;
|
||||
|
||||
getById(id: string): Promise<Chat | null>;
|
||||
|
||||
deleteAll(): Promise<void>;
|
||||
|
||||
deleteById(id: string): Promise<void>;
|
||||
|
||||
save(chat: Chat): Promise<void>;
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import { Contact } from '@adiwajshing/baileys';
|
||||
|
||||
export interface IContactRepository {
|
||||
getAll(): Promise<Contact[]>;
|
||||
|
||||
getById(id: string): Promise<Contact | null>;
|
||||
|
||||
deleteAll(): Promise<void>;
|
||||
|
||||
deleteById(id: string): Promise<void>;
|
||||
|
||||
save(contact: Contact): Promise<void>;
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
export interface IMessagesRepository {
|
||||
deleteAll(): Promise<void>;
|
||||
|
||||
upsert(messages: any[]): Promise<void>;
|
||||
|
||||
upsertOne(message: any): Promise<void>;
|
||||
|
||||
getAllByJid(jid: string, limit: number): Promise<any[]>;
|
||||
|
||||
getByJidById(jid: string, id: string): Promise<any | null>;
|
||||
|
||||
updateByJidAndId(jid: string, id: string, update: any): Promise<boolean>;
|
||||
|
||||
deleteByJidByIds(jid: string, ids: string[]): Promise<void>;
|
||||
|
||||
deleteAllByJid(jid: string): Promise<void>;
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
import { IChatRepository } from './IChatRepository';
|
||||
import { IContactRepository } from './IContactRepository';
|
||||
import { IMessagesRepository } from './IMessagesRepository';
|
||||
|
||||
export interface INowebStorage {
|
||||
init(): Promise<void>;
|
||||
|
||||
close(): Promise<void>;
|
||||
|
||||
getContactsRepository(): IContactRepository;
|
||||
|
||||
getChatRepository(): IChatRepository;
|
||||
|
||||
getMessagesRepository(): IMessagesRepository;
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
import {
|
||||
BaileysEventEmitter,
|
||||
Chat,
|
||||
Contact,
|
||||
proto,
|
||||
} from '@adiwajshing/baileys';
|
||||
|
||||
export interface INowebStore {
|
||||
presences: any;
|
||||
|
||||
init(): Promise<void>;
|
||||
|
||||
close(): Promise<void>;
|
||||
|
||||
bind(ev: BaileysEventEmitter, socket: any): void;
|
||||
|
||||
loadMessage(jid: string, id: string): Promise<proto.IWebMessageInfo>;
|
||||
|
||||
getMessagesByJid(chatId: string, limit: number): Promise<any>;
|
||||
|
||||
getChats(limit?: number, offset?: number): Promise<Chat[]>;
|
||||
|
||||
getContacts(): Promise<Contact[]>;
|
||||
|
||||
getContactById(jid: string): Promise<Contact>;
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
import { Chat, Contact, makeInMemoryStore, proto } from '@adiwajshing/baileys';
|
||||
import { BadRequestException, ConsoleLogger } from '@nestjs/common';
|
||||
|
||||
import { INowebStore } from './INowebStore';
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
const logger = require('pino')();
|
||||
|
||||
export class NowebInMemoryStore implements INowebStore {
|
||||
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. ' +
|
||||
'Read more: https://waha.devlike.pro/docs/engines/noweb#store';
|
||||
|
||||
constructor() {
|
||||
this.store = makeInMemoryStore({ logger: logger });
|
||||
}
|
||||
|
||||
init(): Promise<void> {
|
||||
return;
|
||||
}
|
||||
|
||||
close(): Promise<void> {
|
||||
return;
|
||||
}
|
||||
|
||||
get presences() {
|
||||
return this.store.presences;
|
||||
}
|
||||
|
||||
bind(ev: any, socket: any) {
|
||||
this.store.bind(ev);
|
||||
}
|
||||
|
||||
loadMessage(jid: string, id: string): Promise<proto.IWebMessageInfo> {
|
||||
return this.store.loadMessage(jid, id);
|
||||
}
|
||||
|
||||
getMessagesByJid(chatId: string, limit: number): Promise<any> {
|
||||
throw new BadRequestException(this.errorMessage);
|
||||
}
|
||||
|
||||
getChats(limit?: number, offset?: number): Promise<Chat[]> {
|
||||
throw new BadRequestException(this.errorMessage);
|
||||
}
|
||||
|
||||
getContacts(): Promise<Contact[]> {
|
||||
throw new BadRequestException(this.errorMessage);
|
||||
}
|
||||
|
||||
getContactById(jid: string): Promise<Contact> {
|
||||
throw new BadRequestException(this.errorMessage);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,306 @@
|
||||
import {
|
||||
BaileysEventEmitter,
|
||||
Chat,
|
||||
ChatUpdate,
|
||||
Contact,
|
||||
isRealMessage,
|
||||
jidNormalizedUser,
|
||||
proto,
|
||||
updateMessageWithReaction,
|
||||
updateMessageWithReceipt,
|
||||
} from '@adiwajshing/baileys';
|
||||
import { ConsoleLogger } from '@nestjs/common';
|
||||
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 { INowebStorage } from './INowebStorage';
|
||||
import { INowebStore } from './INowebStore';
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
const AsyncLock = require('async-lock');
|
||||
|
||||
export class NowebPersistentStore implements INowebStore {
|
||||
private socket: any;
|
||||
private chatRepo: IChatRepository;
|
||||
private contactRepo: IContactRepository;
|
||||
private messagesRepo: IMessagesRepository;
|
||||
public presences: any;
|
||||
private lock: any;
|
||||
|
||||
constructor(
|
||||
private logger: Logger,
|
||||
public storage: INowebStorage,
|
||||
) {
|
||||
this.socket = null;
|
||||
this.chatRepo = storage.getChatRepository();
|
||||
this.contactRepo = storage.getContactsRepository();
|
||||
this.messagesRepo = storage.getMessagesRepository();
|
||||
this.presences = {};
|
||||
this.lock = new AsyncLock({ maxPending: Infinity });
|
||||
}
|
||||
|
||||
init(): Promise<void> {
|
||||
return this.storage.init();
|
||||
}
|
||||
|
||||
bind(ev: BaileysEventEmitter, socket: any) {
|
||||
// All
|
||||
ev.on('messaging-history.set', (data) => this.onMessagingHistorySet(data));
|
||||
// Messages
|
||||
ev.on('messages.upsert', (data) =>
|
||||
this.withLock('messages', () => this.onMessagesUpsert(data)),
|
||||
);
|
||||
ev.on('messages.update', (data) =>
|
||||
this.withLock('messages', () => this.onMessageUpdate(data)),
|
||||
);
|
||||
ev.on('messages.delete', (data) =>
|
||||
this.withLock('messages', () => this.onMessageDelete(data)),
|
||||
);
|
||||
ev.on('messages.reaction', (data) =>
|
||||
this.withLock('messages', () => this.onMessageReaction(data)),
|
||||
);
|
||||
ev.on('message-receipt.update', (data) =>
|
||||
this.withLock('messages', () => this.onMessageReceiptUpdate(data)),
|
||||
);
|
||||
// Chats
|
||||
ev.on('chats.upsert', (data) =>
|
||||
this.withLock('chats', () => this.onChatUpsert(data)),
|
||||
);
|
||||
ev.on('chats.update', (data) =>
|
||||
this.withLock('chats', () => this.onChatUpdate(data)),
|
||||
);
|
||||
ev.on('chats.delete', (data) =>
|
||||
this.withLock('chats', () => this.onChatDelete(data)),
|
||||
);
|
||||
// Contacts
|
||||
ev.on('contacts.upsert', (data) =>
|
||||
this.withLock('contacts', () => this.onContactsUpsert(data)),
|
||||
);
|
||||
ev.on('contacts.update', (data) =>
|
||||
this.withLock('contacts', () => this.onContactUpdate(data)),
|
||||
);
|
||||
// Presence
|
||||
ev.on('presence.update', (data) => this.onPresenceUpdate(data));
|
||||
this.socket = socket;
|
||||
}
|
||||
|
||||
async close(): Promise<void> {
|
||||
await this.storage?.close().catch((error) => {
|
||||
this.logger.warn(`Failed to close storage: ${error}`);
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
private async onMessagingHistorySet(history) {
|
||||
const { contacts, chats, messages, isLatest } = history;
|
||||
if (isLatest) {
|
||||
this.logger.debug(
|
||||
'history sync - clearing all entities, got latest history',
|
||||
);
|
||||
await Promise.all([
|
||||
this.withLock('contacts', () => this.contactRepo.deleteAll()),
|
||||
this.withLock('chats', () => this.chatRepo.deleteAll()),
|
||||
this.withLock('messages', () => this.messagesRepo.deleteAll()),
|
||||
]);
|
||||
}
|
||||
|
||||
await Promise.all([
|
||||
this.withLock('contacts', async () => {
|
||||
await this.onContactsUpsert(contacts);
|
||||
this.logger.info(`history sync - '${contacts.length}' synced contacts`);
|
||||
}),
|
||||
this.withLock('chats', () => this.onChatUpsert(chats)),
|
||||
this.withLock('messages', () => this.syncMessagesHistory(messages)),
|
||||
]);
|
||||
}
|
||||
|
||||
private async syncMessagesHistory(messages) {
|
||||
const realMessages = messages.filter(isRealMessage);
|
||||
await this.messagesRepo.upsert(realMessages);
|
||||
this.logger.info(
|
||||
`history sync - '${messages.length}' got messages, '${realMessages.length}' real messages`,
|
||||
);
|
||||
}
|
||||
|
||||
private async onMessagesUpsert(update) {
|
||||
const { messages, type } = update;
|
||||
if (type !== 'notify' && type !== 'append') {
|
||||
this.logger.debug(`unexpected type for messages.upsert: '${type}'`);
|
||||
return;
|
||||
}
|
||||
const realMessages = messages.filter(isRealMessage);
|
||||
await this.messagesRepo.upsert(realMessages);
|
||||
this.logger.debug(
|
||||
`messages.upsert - ${messages.length} got messages, ${realMessages.length} real messages`,
|
||||
);
|
||||
}
|
||||
|
||||
private async onMessageUpdate(updates) {
|
||||
for (const update of updates) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
const jid = jidNormalizedUser(update.key.remoteJid!);
|
||||
const message = await this.messagesRepo.getByJidById(jid, update.key.id);
|
||||
if (!message) {
|
||||
continue;
|
||||
}
|
||||
const fields = { ...update.update };
|
||||
// It can overwrite the key, so we need to delete it
|
||||
delete fields['key'];
|
||||
Object.assign(message, fields);
|
||||
// In case of revoked messages - remove it
|
||||
// TODO: May be we should save the flag instead of completely removing the message
|
||||
const isYetRealMessage =
|
||||
isRealMessage(message, this.socket?.authState?.creds?.me?.id) || false;
|
||||
if (isYetRealMessage) {
|
||||
await this.messagesRepo.upsertOne(message);
|
||||
} else {
|
||||
await this.messagesRepo.deleteByJidByIds(jid, [update.key.id]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async onMessageDelete(item) {
|
||||
if ('all' in item) {
|
||||
await this.messagesRepo.deleteAllByJid(item.jid);
|
||||
return;
|
||||
}
|
||||
const jid = jidNormalizedUser(item.keys[0].remoteJid);
|
||||
const ids = item.keys.map((key) => key.id);
|
||||
await this.messagesRepo.deleteByJidByIds(jid, ids);
|
||||
}
|
||||
|
||||
private async onChatUpsert(chats: Chat[]) {
|
||||
for (const chat of chats) {
|
||||
delete chat['messages'];
|
||||
chat.conversationTimestamp = toNumber(chat.conversationTimestamp);
|
||||
await this.chatRepo.save(chat);
|
||||
}
|
||||
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);
|
||||
delete chat['messages'];
|
||||
await this.chatRepo.save(chat);
|
||||
}
|
||||
}
|
||||
|
||||
private async onChatDelete(ids: string[]) {
|
||||
for (const id of ids) {
|
||||
await this.chatRepo.deleteById(id);
|
||||
await this.messagesRepo.deleteAllByJid(id);
|
||||
}
|
||||
}
|
||||
|
||||
private withLock(key, fn) {
|
||||
return this.lock.acquire(key, fn);
|
||||
}
|
||||
|
||||
private async onContactsUpsert(contacts: Contact[]) {
|
||||
for (const update of contacts) {
|
||||
const contact = await this.contactRepo.getById(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);
|
||||
}
|
||||
}
|
||||
|
||||
private async onContactUpdate(updates: Partial<Contact>[]) {
|
||||
for (const update of updates) {
|
||||
const contact = await this.contactRepo.getById(update.id);
|
||||
|
||||
if (!contact) {
|
||||
this.logger.warn(
|
||||
`got update for non-existent contact. update: '${JSON.stringify(
|
||||
update,
|
||||
)}'`,
|
||||
);
|
||||
continue;
|
||||
// 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
|
||||
}
|
||||
Object.assign(contact, update);
|
||||
|
||||
if (update.imgUrl === 'changed') {
|
||||
contact.imgUrl = this.socket
|
||||
? await this.socket?.profilePictureUrl(contact.id)
|
||||
: undefined;
|
||||
} else if (update.imgUrl === 'removed') {
|
||||
delete contact.imgUrl;
|
||||
}
|
||||
await this.onContactsUpsert([contact]);
|
||||
}
|
||||
}
|
||||
|
||||
private async onMessageReaction(reactions) {
|
||||
for (const { key, reaction } of reactions) {
|
||||
const msg = await this.messagesRepo.getByJidById(key.remoteJid, key.id);
|
||||
if (!msg) {
|
||||
this.logger.warn(
|
||||
`got reaction update for non-existent message. key: '${JSON.stringify(
|
||||
key,
|
||||
)}'`,
|
||||
);
|
||||
continue;
|
||||
}
|
||||
updateMessageWithReaction(msg, reaction);
|
||||
await this.messagesRepo.upsertOne(msg);
|
||||
}
|
||||
}
|
||||
|
||||
private async onMessageReceiptUpdate(updates) {
|
||||
for (const { key, receipt } of updates) {
|
||||
const msg = await this.messagesRepo.getByJidById(key.remoteJid, key.id);
|
||||
if (!msg) {
|
||||
this.logger.warn(
|
||||
`got receipt update for non-existent message. key: '${JSON.stringify(
|
||||
key,
|
||||
)}'`,
|
||||
);
|
||||
continue;
|
||||
}
|
||||
updateMessageWithReceipt(msg, receipt);
|
||||
await this.messagesRepo.upsertOne(msg);
|
||||
}
|
||||
}
|
||||
|
||||
private async onPresenceUpdate({ id, presences: update }) {
|
||||
this.presences[id] = this.presences[id] || {};
|
||||
Object.assign(this.presences[id], update);
|
||||
}
|
||||
|
||||
async loadMessage(jid: string, id: string) {
|
||||
const data = await this.messagesRepo.getByJidById(jid, id);
|
||||
if (!data) {
|
||||
return null;
|
||||
}
|
||||
return proto.WebMessageInfo.fromObject(data);
|
||||
}
|
||||
|
||||
getMessagesByJid(chatId: string, limit: number) {
|
||||
return this.messagesRepo.getAllByJid(toJID(chatId), toNumber(limit));
|
||||
}
|
||||
|
||||
getChats(limit?: number, offset?: number): Promise<Chat[]> {
|
||||
return this.chatRepo.getAllWithMessages(limit, offset);
|
||||
}
|
||||
|
||||
getContactById(jid) {
|
||||
return this.contactRepo.getById(jid);
|
||||
}
|
||||
|
||||
getContacts() {
|
||||
return this.contactRepo.getAll();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
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:');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
export class Field {
|
||||
constructor(
|
||||
public fieldName: string,
|
||||
public type: string,
|
||||
) {}
|
||||
}
|
||||
|
||||
export class Index {
|
||||
constructor(
|
||||
public name: string,
|
||||
public columns: string[],
|
||||
) {}
|
||||
}
|
||||
|
||||
export class Schema {
|
||||
constructor(
|
||||
public name: string,
|
||||
public columns: Field[],
|
||||
public indexes: Index[],
|
||||
) {}
|
||||
}
|
||||
|
||||
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']),
|
||||
],
|
||||
),
|
||||
];
|
||||
@@ -0,0 +1,23 @@
|
||||
import { Chat } from '@adiwajshing/baileys';
|
||||
|
||||
import { IChatRepository } from '../IChatRepository';
|
||||
import { Sqlite3KVRepository } from './Sqlite3KVRepository';
|
||||
|
||||
export class Sqlite3ChatRepository
|
||||
extends Sqlite3KVRepository<Chat>
|
||||
implements IChatRepository
|
||||
{
|
||||
async getAllWithMessages(limit?: number, offset?: number): Promise<Chat[]> {
|
||||
// Get chats with conversationTimestamp is not Null
|
||||
let query = this.select()
|
||||
.whereNotNull('conversationTimestamp')
|
||||
.orderBy('conversationTimestamp', 'desc');
|
||||
if (limit != null) {
|
||||
query = query.limit(limit);
|
||||
}
|
||||
if (offset != null) {
|
||||
query = query.offset(offset);
|
||||
}
|
||||
return await this.all(query);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
import { Contact } from '@adiwajshing/baileys';
|
||||
|
||||
import { IContactRepository } from '../IContactRepository';
|
||||
import { Sqlite3KVRepository } from './Sqlite3KVRepository';
|
||||
|
||||
export class Sqlite3ContactRepository
|
||||
extends Sqlite3KVRepository<Contact>
|
||||
implements IContactRepository {}
|
||||
@@ -0,0 +1,155 @@
|
||||
import { BufferJSON } from '@adiwajshing/baileys/lib/Utils';
|
||||
import {
|
||||
convertProtobufToPlainObject,
|
||||
replaceLongsWithNumber,
|
||||
} from '@waha/core/engines/noweb/utils';
|
||||
import { Database } from 'better-sqlite3';
|
||||
import Knex from 'knex';
|
||||
|
||||
import { Field, Schema } from '../Schema';
|
||||
|
||||
/**
|
||||
* Key value repository with extra metadata
|
||||
*/
|
||||
export class Sqlite3KVRepository<Entity> {
|
||||
private UPSERT_BATCH_SIZE = 100;
|
||||
protected db: Database;
|
||||
|
||||
private readonly metadata: Map<string, (entity: Entity) => any>;
|
||||
protected readonly table: string;
|
||||
private readonly columns: Field[];
|
||||
private knex: Knex.Knex;
|
||||
|
||||
constructor(
|
||||
db: Database,
|
||||
schema: Schema,
|
||||
metadata: Map<string, (entity: Entity) => any> | null = null,
|
||||
) {
|
||||
this.db = db;
|
||||
this.columns = schema.columns;
|
||||
this.table = schema.name;
|
||||
this.metadata = metadata || new Map();
|
||||
|
||||
// sqlite does not support inserting default values. Set the `useNullAsDefault` flag to hide this warning. (see docs https://knexjs.org/guide/query-builder.html#insert).
|
||||
this.knex = Knex({ client: 'better-sqlite3', useNullAsDefault: true });
|
||||
}
|
||||
|
||||
getAll() {
|
||||
return this.all(this.select());
|
||||
}
|
||||
|
||||
protected async getBy(filters: any) {
|
||||
const query = this.select().where(filters).limit(1);
|
||||
return this.get(query);
|
||||
}
|
||||
|
||||
private dump(entity: Entity) {
|
||||
const data = {};
|
||||
const raw = convertProtobufToPlainObject(entity);
|
||||
replaceLongsWithNumber(raw);
|
||||
for (const field of this.columns) {
|
||||
const fn = this.metadata.get(field.fieldName);
|
||||
if (fn) {
|
||||
data[field.fieldName] = fn(raw);
|
||||
} else if (field.fieldName == 'data') {
|
||||
data['data'] = JSON.stringify(raw, BufferJSON.replacer);
|
||||
} else {
|
||||
data[field.fieldName] = raw[field.fieldName];
|
||||
}
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
save(entity: Entity) {
|
||||
return this.upsertOne(entity);
|
||||
}
|
||||
|
||||
async getById(id: string): Promise<Entity | null> {
|
||||
return this.getBy({ id: id });
|
||||
}
|
||||
|
||||
async upsertOne(entity: Entity): Promise<void> {
|
||||
await this.upsertMany([entity]);
|
||||
}
|
||||
|
||||
async upsertMany(entities: Entity[]): Promise<void> {
|
||||
if (entities.length === 0) {
|
||||
return;
|
||||
}
|
||||
const batchSize = this.UPSERT_BATCH_SIZE;
|
||||
for (let i = 0; i < entities.length; i += batchSize) {
|
||||
const batch = entities.slice(i, i + batchSize);
|
||||
await this.upsertBatch(batch);
|
||||
}
|
||||
}
|
||||
|
||||
private async upsertBatch(entities: Entity[]): Promise<void> {
|
||||
const data = entities.map((entity) => this.dump(entity));
|
||||
const keys = this.columns.map((c) => c.fieldName);
|
||||
const values = data.map((d) => Object.values(d)).flat();
|
||||
try {
|
||||
this.db
|
||||
.prepare(
|
||||
`INSERT INTO ${this.table} (${keys.join(', ')})
|
||||
VALUES ${data
|
||||
.map(() => `(${keys.map(() => '?').join(', ')})`)
|
||||
.join(', ')}
|
||||
ON CONFLICT(id) DO UPDATE
|
||||
SET ${keys.map((key) => `${key} = excluded.${key}`).join(', ')}`,
|
||||
)
|
||||
.run(values);
|
||||
} catch (err) {
|
||||
console.error(`Error upserting data: ${err}, values: ${values}`);
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
protected async deleteBy(filters: any) {
|
||||
const query = this.delete().where(filters);
|
||||
await this.run(query);
|
||||
}
|
||||
|
||||
async deleteAll() {
|
||||
const query = this.delete();
|
||||
await this.run(query);
|
||||
}
|
||||
|
||||
async deleteById(id: string) {
|
||||
await this.deleteBy({ id: id });
|
||||
}
|
||||
|
||||
/**
|
||||
* SQL helpers
|
||||
*/
|
||||
|
||||
protected select() {
|
||||
return this.knex.select().from(this.table);
|
||||
}
|
||||
|
||||
protected delete() {
|
||||
return this.knex.delete().from(this.table);
|
||||
}
|
||||
|
||||
protected async all(query: any) {
|
||||
const sql = query.toSQL().sql;
|
||||
const bind = query.toSQL().bindings;
|
||||
const rows: any[] = this.db.prepare(sql).all(bind);
|
||||
return rows.map((row) => JSON.parse(row.data, BufferJSON.reviver));
|
||||
}
|
||||
|
||||
protected async get(query: any) {
|
||||
const sql = query.toSQL().sql;
|
||||
const bind = query.toSQL().bindings;
|
||||
const row: any = this.db.prepare(sql).get(bind);
|
||||
if (!row) {
|
||||
return null;
|
||||
}
|
||||
return JSON.parse(row.data, BufferJSON.reviver);
|
||||
}
|
||||
|
||||
protected async run(query: any) {
|
||||
const sql = query.toSQL().sql;
|
||||
const bind = query.toSQL().bindings;
|
||||
return this.db.prepare(sql).run(bind);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
import { IMessagesRepository } from '../IMessagesRepository';
|
||||
import { Sqlite3KVRepository } from './Sqlite3KVRepository';
|
||||
|
||||
export class Sqlite3MessagesRepository
|
||||
extends Sqlite3KVRepository<any>
|
||||
implements IMessagesRepository
|
||||
{
|
||||
upsert(messages: any[]): Promise<void> {
|
||||
return this.upsertMany(messages);
|
||||
}
|
||||
|
||||
getAllByJid(jid: string, limit: number): Promise<any[]> {
|
||||
const query = this.select()
|
||||
.where({ jid: jid })
|
||||
.limit(limit)
|
||||
.orderBy('messageTimestamp', 'DESC');
|
||||
return this.all(query);
|
||||
}
|
||||
|
||||
async getByJidById(jid: string, id: string): Promise<any> {
|
||||
return this.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.upsertOne(entity);
|
||||
}
|
||||
|
||||
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]);
|
||||
}
|
||||
|
||||
deleteAllByJid(jid: string): Promise<void> {
|
||||
return this.deleteBy({ jid: jid });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
import { Schema } from '../Schema';
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
const Database = require('better-sqlite3');
|
||||
|
||||
export class Sqlite3SchemaValidation {
|
||||
constructor(
|
||||
private table: Schema,
|
||||
private db,
|
||||
) {}
|
||||
|
||||
validate() {
|
||||
const table = this.table;
|
||||
|
||||
// Check table has the columns
|
||||
const columns = this.db.prepare(`PRAGMA table_info(${table.name})`).all();
|
||||
// Check exact number of columns
|
||||
if (columns.length !== table.columns.length) {
|
||||
throw new Error(
|
||||
`Table '${table.name}' does not have expected number of columns. Expected ${table.columns.length}, got ${columns.length}`,
|
||||
);
|
||||
}
|
||||
|
||||
// Check column + type
|
||||
for (const column of table.columns) {
|
||||
const columnInfo = columns.find((c) => c.name === column.fieldName);
|
||||
if (!columnInfo) {
|
||||
throw new Error(
|
||||
`Table '${table.name}' does not have column '${column.fieldName}'`,
|
||||
);
|
||||
}
|
||||
if (columnInfo.type !== column.type) {
|
||||
throw new Error(
|
||||
`Table '${table.name}' column '${column.fieldName}' has type '${columnInfo.type}' but expected '${column.type}'`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Check table has expected indexes
|
||||
const indexes = this.db.prepare(`PRAGMA index_list(${table.name})`).all();
|
||||
const indexNames = indexes.map((index) => index.name);
|
||||
for (const index of table.indexes) {
|
||||
if (!indexNames.includes(index.name)) {
|
||||
throw new Error(
|
||||
`Table '${table.name}' does not have index '${index.name}'`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
import { WAMessage } from '@adiwajshing/baileys';
|
||||
|
||||
import { INowebStorage } from '../INowebStorage';
|
||||
import { Field, Index, NOWEB_STORE_SCHEMA, Schema } from '../Schema';
|
||||
import { Sqlite3ChatRepository } from './Sqlite3ChatRepository';
|
||||
import { Sqlite3ContactRepository } from './Sqlite3ContactRepository';
|
||||
import { Sqlite3MessagesRepository } from './Sqlite3MessagesRepository';
|
||||
import { Sqlite3SchemaValidation } from './Sqlite3SchemaValidation';
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
const Database = require('better-sqlite3');
|
||||
|
||||
export class Sqlite3Storage implements INowebStorage {
|
||||
private readonly db: any;
|
||||
private readonly tables: Schema[];
|
||||
|
||||
constructor(filePath: string) {
|
||||
this.db = new Database(filePath);
|
||||
this.tables = NOWEB_STORE_SCHEMA;
|
||||
}
|
||||
|
||||
async init() {
|
||||
this.db.pragma('journal_mode = WAL;');
|
||||
this.migrate();
|
||||
this.validateSchema();
|
||||
}
|
||||
|
||||
private migrate() {
|
||||
this.migration0001init();
|
||||
}
|
||||
|
||||
private validateSchema() {
|
||||
for (const table of this.tables) {
|
||||
new Sqlite3SchemaValidation(table, this.db).validate();
|
||||
}
|
||||
}
|
||||
|
||||
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)',
|
||||
);
|
||||
}
|
||||
|
||||
async close() {
|
||||
this.db.close();
|
||||
}
|
||||
|
||||
getContactsRepository() {
|
||||
return new Sqlite3ContactRepository(this.db, this.getSchema('contacts'));
|
||||
}
|
||||
|
||||
getChatRepository() {
|
||||
return new Sqlite3ChatRepository(this.db, this.getSchema('chats'));
|
||||
}
|
||||
|
||||
getMessagesRepository() {
|
||||
const metadata = new Map()
|
||||
.set('jid', (msg: WAMessage) => msg.key.remoteJid)
|
||||
.set('id', (msg: WAMessage) => msg.key.id)
|
||||
.set('messageTimestamp', (msg: WAMessage) => msg.messageTimestamp);
|
||||
return new Sqlite3MessagesRepository(
|
||||
this.db,
|
||||
this.getSchema('messages'),
|
||||
metadata,
|
||||
);
|
||||
}
|
||||
|
||||
getSchema(name: string) {
|
||||
const schema = this.tables.find((table) => table.name === name);
|
||||
if (!schema) {
|
||||
throw new Error(`Schema not found: ${name}`);
|
||||
}
|
||||
return schema;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
import {
|
||||
AuthenticationCreds,
|
||||
AuthenticationState,
|
||||
proto,
|
||||
} from '@adiwajshing/baileys';
|
||||
import { BufferJSON, initAuthCreds } from '@adiwajshing/baileys/lib/Utils';
|
||||
import { mkdir, readFile, stat, unlink, writeFile } from 'fs/promises';
|
||||
import { join } from 'path';
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
const AsyncLock = require('async-lock');
|
||||
|
||||
// We need to lock files due to the fact that we are using async functions to read and write files
|
||||
// https://github.com/WhiskeySockets/Baileys/issues/794
|
||||
// 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 });
|
||||
|
||||
/**
|
||||
* stores the full authentication state in a single folder.
|
||||
* Far more efficient than singlefileauthstate
|
||||
*
|
||||
* Again, I wouldn't endorse this for any production level use other than perhaps a bot.
|
||||
* Would recommend writing an auth state for use with a proper SQL or No-SQL DB
|
||||
* */
|
||||
export const useMultiFileAuthState = async (
|
||||
folder: string,
|
||||
): Promise<{
|
||||
state: AuthenticationState;
|
||||
saveCreds: () => 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)),
|
||||
);
|
||||
};
|
||||
|
||||
const readData = async (file: string) => {
|
||||
try {
|
||||
const filePath = join(folder, fixFileName(file));
|
||||
const data = await fileLock.acquire(filePath, () =>
|
||||
readFile(filePath, { encoding: 'utf-8' }),
|
||||
);
|
||||
return JSON.parse(data, BufferJSON.reviver);
|
||||
} catch (error) {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
const removeData = async (file: string) => {
|
||||
try {
|
||||
const filePath = join(folder, fixFileName(file));
|
||||
await fileLock.acquire(filePath, () => unlink(filePath));
|
||||
} catch {}
|
||||
};
|
||||
|
||||
const folderInfo = await stat(folder).catch(() => {
|
||||
return null;
|
||||
});
|
||||
if (folderInfo) {
|
||||
if (!folderInfo.isDirectory()) {
|
||||
throw new Error(
|
||||
`found something that is not a directory at ${folder}, either delete it or specify a different location`,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
await mkdir(folder, { recursive: true });
|
||||
}
|
||||
|
||||
const fixFileName = (file?: string) =>
|
||||
file?.replace(/\//g, '__')?.replace(/:/g, '-') || '';
|
||||
|
||||
const creds: AuthenticationCreds =
|
||||
(await readData('creds.json')) || initAuthCreds();
|
||||
|
||||
return {
|
||||
state: {
|
||||
creds,
|
||||
keys: {
|
||||
get: async (type, ids) => {
|
||||
const data = {};
|
||||
await Promise.all(
|
||||
ids.map(async (id) => {
|
||||
let value = await readData(`${type}-${id}.json`);
|
||||
if (type === 'app-state-sync-key' && value) {
|
||||
value = proto.Message.AppStateSyncKeyData.fromObject(value);
|
||||
}
|
||||
|
||||
data[id] = value;
|
||||
}),
|
||||
);
|
||||
|
||||
return data;
|
||||
},
|
||||
set: async (data) => {
|
||||
const tasks: Promise<void>[] = [];
|
||||
for (const category in data) {
|
||||
for (const id in data[category]) {
|
||||
const value = data[category][id];
|
||||
const file = `${category}-${id}.json`;
|
||||
tasks.push(value ? writeData(value, file) : removeData(file));
|
||||
}
|
||||
}
|
||||
|
||||
await Promise.all(tasks);
|
||||
},
|
||||
},
|
||||
},
|
||||
saveCreds: () => {
|
||||
return writeData(creds, 'creds.json');
|
||||
},
|
||||
};
|
||||
};
|
||||
@@ -12,3 +12,74 @@ export function extractMediaContent(
|
||||
content?.stickerMessage;
|
||||
return mediaContent;
|
||||
}
|
||||
|
||||
interface Long {
|
||||
low: number;
|
||||
high: number;
|
||||
unsigned: boolean;
|
||||
|
||||
toNumber?(): number;
|
||||
}
|
||||
|
||||
type AnyObject = { [key: string]: any };
|
||||
|
||||
export const replaceLongsWithNumber = (obj: AnyObject): void => {
|
||||
for (const key in obj) {
|
||||
if (obj.hasOwnProperty(key)) {
|
||||
if (isObjectALong(obj[key])) {
|
||||
obj[key] = toNumber(obj[key]);
|
||||
} else if (typeof obj[key] === 'object' && obj[key] !== null) {
|
||||
replaceLongsWithNumber(obj[key]);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export function convertProtobufToPlainObject(obj: any): any {
|
||||
if (typeof obj !== 'object' || obj === null) {
|
||||
return obj;
|
||||
}
|
||||
|
||||
// Ignore Buffer
|
||||
if (
|
||||
Buffer.isBuffer(obj) ||
|
||||
obj?.type == 'Buffer' ||
|
||||
obj instanceof Uint8Array
|
||||
) {
|
||||
return obj;
|
||||
}
|
||||
|
||||
// Remove empty array
|
||||
if (Array.isArray(obj) && obj.length === 0) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (Array.isArray(obj)) {
|
||||
return obj.map((item) => convertProtobufToPlainObject(item));
|
||||
}
|
||||
|
||||
const plainObject: any = {};
|
||||
|
||||
Object.keys(obj).forEach((key) => {
|
||||
const value = obj[key];
|
||||
plainObject[key] = convertProtobufToPlainObject(value);
|
||||
});
|
||||
|
||||
return { ...plainObject };
|
||||
}
|
||||
|
||||
const isObjectALong = (value: any): value is Long => {
|
||||
return (
|
||||
value &&
|
||||
typeof value === 'object' &&
|
||||
'low' in value &&
|
||||
'high' in value &&
|
||||
'unsigned' in value
|
||||
);
|
||||
};
|
||||
|
||||
const toNumber = (longValue: Long): number => {
|
||||
const { low, high, unsigned } = longValue;
|
||||
const result = unsigned ? low >>> 0 : low + high * 0x100000000;
|
||||
return result;
|
||||
};
|
||||
@@ -69,7 +69,7 @@ export class WhatsappSessionVenomCore extends WhatsappSession {
|
||||
this.qr.save(base64Qrimg, urlCode);
|
||||
this.printQR(this.qr);
|
||||
this.status = WAHASessionStatus.SCAN_QR_CODE;
|
||||
this.log.debug('Number of attempts to read the qrcode: ', attempts);
|
||||
this.logger.debug('Number of attempts to read the qrcode: ', attempts);
|
||||
};
|
||||
}
|
||||
|
||||
@@ -79,7 +79,7 @@ export class WhatsappSessionVenomCore extends WhatsappSession {
|
||||
this.whatsapp = await this.buildClient();
|
||||
} catch (error) {
|
||||
this.status = WAHASessionStatus.FAILED;
|
||||
this.log.error(error);
|
||||
this.logger.error(error);
|
||||
this.qr.save('');
|
||||
return;
|
||||
}
|
||||
@@ -90,8 +90,9 @@ export class WhatsappSessionVenomCore extends WhatsappSession {
|
||||
}
|
||||
|
||||
async stop() {
|
||||
await this.whatsapp.close();
|
||||
this.status = WAHASessionStatus.STOPPED;
|
||||
this.events.removeAllListeners();
|
||||
await this.whatsapp.close();
|
||||
}
|
||||
|
||||
subscribeEngineEvent(event: WAHAEvents | string, handler: (message) => void) {
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
<!DOCTYPE html><html id="whatsapp-web" class="" lang="en" dir="ltr"><head><link data-default-icon="https://static.whatsapp.net/rsrc.php/v3/yP/r/rYZqPCBaG70.png" rel="shortcut icon" href="https://static.whatsapp.net/rsrc.php/v3/yP/r/rYZqPCBaG70.png" /><meta name="bingbot" content="noarchive" /><meta name="viewport" content="width=device-width,initial-scale=1,maximum-scale=2,shrink-to-fit=no" /><link rel="manifest" href="/data/manifest.json" crossorigin="use-credentials" /><meta name="google" content="notranslate" /><meta name="format-detection" content="telephone=no" /><meta name="description" content="Quickly send and receive WhatsApp messages right from your computer." /><meta name="og:description" content="Quickly send and receive WhatsApp messages right from your computer." /><meta name="og:url" content="https://web.whatsapp.com/" /><meta name="og:title" content="WhatsApp Web" /><meta name="og:image" content="https://static.whatsapp.net/rsrc.php/v3/yR/r/y8-PTBaP90a.png" /><meta name="theme-color" content="#111b21" media="(prefers-color-scheme: dark)" /><meta name="theme-color" content="#f0f2f5" /><link rel="apple-touch-icon" sizes="192x192" href="https://static.whatsapp.net/rsrc.php/v3/yc/r/hUUuVTz6ZVi.png" /><style nonce="6GV2UyzI">:root{--splashscreen-startup-background:#f0f2f5;--splashscreen-startup-background-rgb:240, 242, 245;--splashscreen-startup-icon:#bbc5cb;--splashscreen-secondary-lighter:#8696a0;--splashscreen-primary-title:#4125d;--splashscreen-progress-primary:#00c298;--splashscreen-progress-background:#e9edef;}@media (prefers-color-scheme: dark){:root{--splashscreen-startup-background:#111b21;--splashscreen-startup-background-rgb:17,27, 33;--splashscreen-startup-icon:#676f73;--splashscreen-primary-title:rgba(233, 237, 239, 0.88);--splashscreen-secondary-lighter:#667781;--splashscreen-progress-primary:#0b846d;--splashscreen-progress-background:#233138;}}.__fb-light-mode{--splashscreen-startup-background:#f0f2f5;--splashscreen-startup-background-rgb:240, 242, 245;--splashscreen-startup-icon:#bbc5cb;--splashscreen-secondary-lighter:#8696a0;--splashscreen-primary-title:#4125d;--splashscreen-progress-primary:#00c298;--splashscreen-progress-background:#e9edef;}.__fb-dark-mode{--splashscreen-startup-background:#111b21;--splashscreen-startup-background-rgb:17,27, 33;--splashscreen-startup-icon:#676f73;--splashscreen-primary-title:rgba(233, 237, 239, 0.88);--splashscreen-secondary-lighter:#667781;--splashscreen-progress-primary:#0b846d;--splashscreen-progress-background:#233138;}</style><style nonce="6GV2UyzI"></style><script id="envjson" type="application/json" nonce="6GV2UyzI">{"useTrustedTypes":false,"isTrustedTypesReportOnly":false,"use_fbt_virtual_modules":true,"compat_iframe_token":null}</script><script src="data:application/x-javascript; charset=utf-8;base64,Oy8qRkJfUEtHX0RFTElNKi8KCnZhciBkYXRhRWxlbWVudD1kb2N1bWVudC5nZXRFbGVtZW50QnlJZCgiZW52anNvbiIpO2lmKGRhdGFFbGVtZW50IT1udWxsKXt2YXIgY29weVZhcmlhYmxlcz1mdW5jdGlvbihhKXtmb3IodmFyIGIgaW4gdmFyaWFibGVzKWFbYl09dmFyaWFibGVzW2JdfSx2YXJpYWJsZXM9SlNPTi5wYXJzZShkYXRhRWxlbWVudC50ZXh0Q29udGVudCk7d2luZG93LnJlcXVpcmVMYXp5P3dpbmRvdy5yZXF1aXJlTGF6eShbIkVudiJdLGNvcHlWYXJpYWJsZXMpOih3aW5kb3cuRW52PXdpbmRvdy5FbnZ8fHt9LGNvcHlWYXJpYWJsZXMod2luZG93LkVudikpfQovLyMgc291cmNlVVJMPWh0dHBzOi8vc3RhdGljLndoYXRzYXBwLm5ldC9yc3JjLnBocC92My95TC9yL2RiZ3JNc25EMThwLmpzP19uY194PUlqM1dwOGxnNUt6Cg==" data-bootloader-hash="nxRKQO6" data-p=":0" data-btmanifest="1014522270_main" data-c="1" nonce="6GV2UyzI"></script><script src="data:application/x-javascript; charset=utf-8;base64,Oy8qRkJfUEtHX0RFTElNKi8KCl9fYW5ub3RhdG9yPWZ1bmN0aW9uKGEpe3JldHVybiBhfSxfX2Rfc3R1Yj1bXSxfX2Q9ZnVuY3Rpb24oYSxiLGMsZCl7X19kX3N0dWIucHVzaChbYSxiLGMsZF0pfSxfX3JsX3N0dWI9W10scmVxdWlyZUxhenk9ZnVuY3Rpb24oKXtfX3JsX3N0dWIucHVzaChhcmd1bWVudHMpfTsKLy8jIHNvdXJjZVVSTD1odHRwczovL3N0YXRpYy53aGF0c2FwcC5uZXQvcnNyYy5waHAvdjMveTIvci9jTWRfU1pvdjlFaS5qcz9fbmNfeD1JajNXcDhsZzVLego=" data-bootloader-hash="W7CcKyc" data-p=":0" data-btmanifest="1014522270_main" data-c="1" nonce="6GV2UyzI"></script><script src="data:application/x-javascript; charset=utf-8;base64,Oy8qRkJfUEtHX0RFTElNKi8KCl9idGxkcj17fTsKLy8jIHNvdXJjZVVSTD1odHRwczovL3N0YXRpYy53aGF0c2FwcC5uZXQvcnNyYy5waHAvdjMveUgvci9aTGNLT2JOdU8xYi5qcz9fbmNfeD1JajNXcDhsZzVLego=" data-bootloader-hash="678jA6i" data-p=":0" data-btmanifest="1014522270_main" data-c="1" nonce="6GV2UyzI"></script><script src="data:application/x-javascript; charset=utf-8;base64,Oy8qRkJfUEtHX0RFTElNKi8KCihmdW5jdGlvbihhKXtmdW5jdGlvbiBiKGIpe2lmKCF3aW5kb3cub3BlbkRhdGFiYXNlKXJldHVybjtiLklfQU1fSU5DT0dOSVRPX0FORF9JX1JFQUxMWV9ORUVEX1dFQlNRTD1mdW5jdGlvbihhLGIsYyxkKXtyZXR1cm4gd2luZG93Lm9wZW5EYXRhYmFzZShhLGIsYyxkKX07d2luZG93Lm9wZW5EYXRhYmFzZT1mdW5jdGlvbigpe3Rocm93IG5ldyBFcnJvcigpfX1iKGEpfSkodGhpcyk7Ci8vIyBzb3VyY2VVUkw9aHR0cHM6Ly9zdGF0aWMud2hhdHNhcHAubmV0L3JzcmMucGhwL3YzL3lSL3IvLXBWQzBQRjQ0ZFkuanM/X25jX3g9SWozV3A4bGc1S3oK" data-bootloader-hash="k6/EjOG" data-p=":0" data-btmanifest="1014522270_main" data-c="1" nonce="6GV2UyzI"><Line truncated
|
||||
<script type="application/json" data-content-len="92" data-sjs>{"require":[["qplTimingsServerJS",null,null,["7385063454178867264-server","htmlStart",10]]]}</script>
|
||||
|
||||
<link type="text/css" rel="stylesheet" href="https://static.whatsapp.net/rsrc.php/v3/yv/l/0,cross/C-WCp2i_C8v1k_iwjHwlnlCsHvfmgKJ5t.css?_nc_x=Ij3Wp8lg5Kz" data-bootloader-hash="yyAsU+g" crossorigin="anonymous" data-p=":19,14,8" data-c="1" />
|
||||
<link rel="preload" href="https://static.whatsapp.net/rsrc.php/v3/yH/r/ht8oIC_2H5Y.js?_nc_x=Ij3Wp8lg5Kz" as="script" crossorigin="anonymous" nonce="6GV2UyzI" /><script src="https://static.whatsapp.net/rsrc.php/v3/yH/r/ht8oIC_2H5Y.js?_nc_x=Ij3Wp8lg5Kz" data-bootloader-hash="VwZQdEs" async="1" crossorigin="anonymous" data-p=":1" data-btmanifest="1014522270_main" data-c="1" nonce="6GV2UyzI"></script>
|
||||
<link rel="preload" href="https://static.whatsapp.net/rsrc.php/v3/yV/r/yJJyU7535wL.js?_nc_x=Ij3Wp8lg5Kz" as="script" crossorigin="anonymous" nonce="6GV2UyzI" /><script src="https://static.whatsapp.net/rsrc.php/v3/yV/r/yJJyU7535wL.js?_nc_x=Ij3Wp8lg5Kz" data-bootloader-hash="Ji4B5gK" async="1" crossorigin="anonymous" data-p=":10" data-btmanifest="1014522270_main" data-c="1" nonce="6GV2UyzI"></script>
|
||||
<link rel="preload" href="https://static.whatsapp.net/rsrc.php/v3/y3/r/GpR8XYkiC6brtOgQ7kvhNmryQSgyNOW8X.js?_nc_x=Ij3Wp8lg5Kz" as="script" crossorigin="anonymous" nonce="6GV2UyzI" /><script src="https://static.whatsapp.net/rsrc.php/v3/y3/r/GpR8XYkiC6brtOgQ7kvhNmryQSgyNOW8X.js?_nc_x=Ij3Wp8lg5Kz" data-bootloader-hash="JoAZh6h" async="1" crossorigin="anonymous" data-p=":5,20,4" data-btmanifest="1014522270_main" data-c="1" nonce="6GV2UyzI"></script>
|
||||
<link rel="preload" href="https://static.whatsapp.net/rsrc.php/v3/y3/r/y6YE2mWjTXs.js?_nc_x=Ij3Wp8lg5Kz" as="script" crossorigin="anonymous" nonce="6GV2UyzI" /><script src="https://static.whatsapp.net/rsrc.php/v3/y3/r/y6YE2mWjTXs.js?_nc_x=Ij3Wp8lg5Kz" data-bootloader-hash="DbA7bqQ" async="1" crossorigin="anonymous" data-p=":3" data-btmanifest="1014522270_main" data-c="1" nonce="6GV2UyzI"></script>
|
||||
<link rel="preload" href="https://static.whatsapp.net/rsrc.php/v3iPAE4/yj/l/rt/3A4_-xOlNcy.js?_nc_x=Ij3Wp8lg5Kz" as="script" crossorigin="anonymous" nonce="6GV2UyzI" /><script src="https://static.whatsapp.net/rsrc.php/v3iPAE4/yj/l/rt/3A4_-xOlNcy.js?_nc_x=Ij3Wp8lg5Kz" data-bootloader-hash="EMb+P2t" async="1" crossorigin="anonymous" data-tsrc="https://static.whatsapp.net/rsrc-translations.php/v6iPAE4/y5/l/en_US/3A4_-xOlNcy.js?_nc_x=Ij3Wp8lg5Kz" data-p=":12" data-btmanifest="1014522270_main" data-c="1" nonce="6GV2UyzI"></script><link rel="preload" href="https://static.whatsapp.net/rsrc-translations.php/v6iPAE4/y5/l/en_US/3A4_-xOlNcy.js?_nc_x=Ij3Wp8lg5Kz" as="fetch" crossorigin="anonymous" />
|
||||
<link rel="preload" href="https://static.whatsapp.net/rsrc.php/v3iL1C4/yq/l/rt/D6yUOPmld1J.js?_nc_x=Ij3Wp8lg5Kz" as="script" crossorigin="anonymous" nonce="6GV2UyzI" /><script src="https://static.whatsapp.net/rsrc.php/v3iL1C4/yq/l/rt/D6yUOPmld1J.js?_nc_x=Ij3Wp8lg5Kz" data-bootloader-hash="7mVyh6g" async="1" crossorigin="anonymous" data-tsrc="https://static.whatsapp.net/rsrc-translations.php/v6iL1C4/y1/l/en_US/D6yUOPmld1J.js?_nc_x=Ij3Wp8lg5Kz" data-p=":7" data-btmanifest="1014522270_main" data-c="1" nonce="6GV2UyzI"></script><link rel="preload" href="https://static.whatsapp.net/rsrc-translations.php/v6iL1C4/y1/l/en_US/D6yUOPmld1J.js?_nc_x=Ij3Wp8lg5Kz" as="fetch" crossorigin="anonymous" />
|
||||
<link rel="preload" href="https://static.whatsapp.net/rsrc.php/v3iD_74/yY/l/rt/OC6RHC1oEdtsQ3d6kRL04kvsWqehdEtWw.js?_nc_x=Ij3Wp8lg5Kz" as="script" crossorigin="anonymous" nonce="6GV2UyzI" /><script src="https://static.whatsapp.net/rsrc.php/v3iD_74/yY/l/rt/OC6RHC1oEdtsQ3d6kRL04kvsWqehdEtWw.js?_nc_x=Ij3Wp8lg5Kz" data-bootloader-hash="l5bRwHF" async="1" crossorigin="anonymous" data-tsrc="https://static.whatsapp.net/rsrc-translations.php/v6iD_74/y0/l/en_US/OC6RHC1oEdtsQ3d6kRL04kvsWqehdEtWw.js?_nc_x=Ij3Wp8lg5Kz" data-p=":18,2,11" data-btmanifest="1014522270_main" data-c="1" nonce="6GV2UyzI"></script><link rel="preload" href="https://static.whatsapp.net/rsrc-translations.php/v6iD_74/y0/l/en_US/OC6RHC1oEdtsQ3d6kRL04kvsWqehdEtWw.js?_nc_x=Ij3Wp8lg5Kz" as="fetch" crossorigin="anonymous" />
|
||||
<link rel="preload" href="https://static.whatsapp.net/rsrc.php/v3/yD/r/mY5slrRbdpI.js?_nc_x=Ij3Wp8lg5Kz" as="script" crossorigin="anonymous" nonce="6GV2UyzI" /><script src="https://static.whatsapp.net/rsrc.php/v3/yD/r/mY5slrRbdpI.js?_nc_x=Ij3Wp8lg5Kz" data-bootloader-hash="l3EfnwR" async="1" crossorigin="anonymous" data-p=":6" data-btmanifest="1014522270_main" data-c="1" nonce="6GV2UyzI"></script>
|
||||
<link rel="preload" href="https://static.whatsapp.net/rsrc.php/v3/yt/r/fyeayg1M_Ow.js?_nc_x=Ij3Wp8lg5Kz" as="script" crossorigin="anonymous" nonce="6GV2UyzI" /><script src="https://static.whatsapp.net/rsrc.php/v3/yt/r/fyeayg1M_Ow.js?_nc_x=Ij3Wp8lg5Kz" data-bootloader-hash="lkjhq5M" async="1" crossorigin="anonymous" data-p=":13" data-btmanifest="1014522270_main" data-c="1" nonce="6GV2UyzI"></script>
|
||||
<script type="application/json" data-content-len="469" data-sjs>{"require":[["MakeHasteTranslations","fetchTranslationsForEarlyFlush",null,[{"EMb+P2t":"https:\/\/static.whatsapp.net\/rsrc-translations.php\/v6iPAE4\/y5\/l\/en_US\/3A4_-xOlNcy.js?_nc_x=Ij3Wp8lg5Kz","7mVyh6g":"https:\/\/static.whatsapp.net\/rsrc-translations.php\/v6iL1C4\/y1\/l\/en_US\/D6yUOPmld1J.js?_nc_x=Ij3Wp8lg5Kz","l5bRwHF":"https:\/\/static.whatsapp.net\/rsrc-translations.php\/v6iD_74\/y0\/l\/en_US\/OC6RHC1oEdtsQ3d6kRL04kvsWqehdEtWw.js?_nc_x=Ij3Wp8lg5Kz"}]]]}</script>
|
||||
|
||||
|
||||
<!--EF-->
|
||||
|
||||
<title>WhatsApp Web</title>
|
||||
<script type="application/json" data-content-len="921" data-sjs>{"require":[["HasteSupportData","handle",null,[{"gkxData":{"2199":{"result":false,"hash":null},"7742":{"result":false,"hash":null},"20935":{"result":true,"hash":null},"20936":{"result":false,"hash":null},"209":{"result":false,"hash":null},"4377":{"result":true,"hash":null},"21075":{"result":false,"hash":null},"21076":{"result":true,"hash":null},"22792":{"result":false,"hash":null},"3867":{"result":false,"hash":null},"4341":{"result":false,"hash":null},"9861":{"result":false,"hash":null},"21062":{"result":false,"hash":null},"21063":{"result":false,"hash":null},"21069":{"result":false,"hash":null},"21071":{"result":false,"hash":null},"21072":{"result":false,"hash":null},"33056":{"result":false,"hash":null}},"qexData":{"526":{"r":null},"538":{"r":null},"543":{"r":null},"104":{"r":null},"128":{"r":null},"344":{"r":null},"372":{"r":null},"388":{"r":null},"1604":{"r":null}},"justknobxData":{"1806":{"r":true}}}]]]}</script>
|
||||
<link type="text/css" rel="stylesheet" href="data:text/css; charset=utf-8,._apdl{align-items:center;background-color:var(--splashscreen-startup-background);display:flex;flex-direction:column;height:100%;justify-content:center;left:0;position:fixed;top:0;-webkit-user-select:none;width:100%}._apdl ._apdm{margin-top:-40px;color:var(--splashscreen-startup-icon)}._apdl ._apdm::after{animation:shimmer 1.5s linear .6s infinite;background:linear-gradient( to right, rgba(var(--splashscreen-startup-background-rgb), .5) 0%, rgba(var(--splashscreen-startup-background-rgb), .5) 33.33%, rgba(var(--splashscreen-startup-background-rgb), 0) 44.1%, rgba(var(--splashscreen-startup-background-rgb), 0) 55.8%, rgba(var(--splashscreen-startup-background-rgb), .5) 66.66%, rgba(var(--splashscreen-startup-background-rgb), .5) 100% );content:'';position:relative;top:-100%;display:block;height:100%;left:calc(50% - 72px);opacity:1;width:calc(72px * 3)}@keyframes shimmer{from{left:calc(50% - 72px)}to{left:calc(50% + 72px)}}._apdl ._apdq{height:3px;margin-top:40px;position:relative;width:320px}._apdl ._apdq progress{vertical-align:top}._apdl ._apdr{margin-top:40px;font-size:17px;color:var(--splashscreen-primary-title)}._apdl ._apds{margin-top:12px;font-size:14px;color:var(--splashscreen-secondary-lighter)}._apdl ._apds span{display:inline-block;margin-bottom:2px;vertical-align:middle}._apdl ._apdq progress{-webkit-appearance:none;background-color:var(--splashscreen-progress-background);border:none;color:var(--splashscreen-progress-primary);height:3px;margin:0;vertical-align:-0.2em;width:100%}._apdl ._apdq progress[value]::-webkit-progress-bar{background-color:var(--splashscreen-progress-background)}._apdl ._apdq progress[value]::-webkit-progress-value{background-color:var(--splashscreen-progress-primary);transition:width .45s ease}%23bootloader_z3kVBe5{height:42px;}.bootloader_z3kVBe5{display:block!important;}" data-bootloader-hash="z3kVBe5" data-p=":0" data-c="1" />
|
||||
</head><body class=""><div id="wa_web_initial_startup" class="_apdl"><div style="transform: translate(calc(50% - 52px / 2))" class="_apdm"><span><svg width="250" height="52" xmlns="http://www.w3.org/2000/svg"><path d="M37.7 31.2c-.6-.4-3.8-2-4.4-2.1-.6-.2-1-.4-1.4.3l-2 2.5c-.4.4-.8.5-1.5.2-.6-.3-2.7-1-5.1-3.2-2-1.7-3.2-3.8-3.6-4.5-.4-.6 0-1 .3-1.3l1-1.1.6-1.1c.2-.4 0-.8 0-1.1l-2-4.8c-.6-1.3-1.1-1-1.5-1.1h-1.2c-.5 0-1.2.1-1.8.8-.5.6-2.2 2.2-2.2 5.3 0 3.2 2.3 6.3 2.6 6.7.3.4 4.6 7 11 9.7l3.7 1.4c1.5.5 3 .4 4 .2 1.3-.1 3.9-1.5 4.4-3 .5-1.5.5-2.8.4-3-.2-.4-.6-.5-1.3-.8M26 47.2c-3.9 0-7.6-1-11-3l-.7-.4-8.1 2L8.4 38l-.6-.8A21.4 21.4 0 0126 4.4a21.3 21.3 0 0121.4 21.4c0 11.8-9.6 21.4-21.4 21.4M44.2 7.6a25.8 25.8 0 00-40.6 31L0 52l13.7-3.6A25.8 25.8 0 0044.3 7.5" fill="currentColor"></path></svg></span></div><div class="_apdq"><progress value="0" max="100" dir="ltr"></progress></div><div class="_apdr">WhatsApp</div><div class="_apds"><span><svg width="10" height="12" xmlns="http://www.w3.org/2000/svg"><path d="M5 1.6c1.4 0 2.5 1 2.6 2.4v1.5h.2c.5 0 1 .4 1 1V10c0 .6-.5 1-1 1H2.3a1 1 0 01-1.1-1V6.5c0-.6.5-1 1-1h.2V4.2c0-1.4 1-2.5 2.4-2.6H5zm0 1.2c-.7 0-1.3.6-1.3 1.3v1.4h2.6V4.2c0-.7-.4-1.2-1-1.3H5z" fill="currentColor"></path></svg></span><span> </span>End-to-end encrypted</div></div><script type="application/json" data-content-len="91" data-sjs>{"require":[["qplTimingsServerJS",null,null,["7385063454178867264","splash_screen_show"]]]}</script>
|
||||
<div id="mount_0_0_Zy"></div>
|
||||
<script type="application/json" data-content-len="53" data-sjs>{"require":[["maybeDisableAnimations",null,null,[]]]}</script>
|
||||
<script type="application/json" data-content-len="49" data-sjs>{"require":[["replaceNativeTimer",null,null,[]]]}</script>
|
||||
<script type="application/json" data-content-len="60" data-sjs>{"require":[["bootstrapWebSession",null,null,[1719469077]]]}</script>
|
||||
<script type="application/json" data-content-len="108" data-sjs>{"require":[["qplTagServerJS",null,null,[["comet_aa_coinflip:true","should_use_bt_safe_js","logged_out"]]]]}</script>
|
||||
<script type="application/json" data-content-len="157" data-sjs>{"require":[["qplTimingsServerJS",null,null,["7385063454178867264","tierOne"]],["qplTimingsServerJS",null,null,["7385063454178867264-server","tierOne",15]]]}</script>
|
||||
<script type="application/json" data-content-len="76" data-sjs>{"require":[["JSScheduler","makeSchedulerGlobalEntry",null,["null",false]]]}</script>
|
||||
<script type="application/json" data-content-len="1883" data-sjs>{"require":[["ScheduledServerJS","handle",null,[{"__bbox":{"define":[["cr:734",[],{"__rc":[null,null]},-1],["cr:1293",["ReactDOM.classic"],{"__rc":["ReactDOM.classic",null]},-1],["cr:4874",[],{"__rc":[null,null]},-1],["cr:6640",[],{"__rc":[null,null]},-1],["cr:7162",["ReactDOMCompatibilityLayer"],{"__rc":["ReactDOMCompatibilityLayer",null]},-1],["cr:7451",[],{"__rc":[null,null]},-1],["cr:1064332",[],{"__rc":[null,null]},-1],["cr:1106516",[],{"__rc":[null,null]},-1],["cr:1108857",[],{"__rc":[null,null]},-1],["cr:1294158",["React.classic"],{"__rc":["React.classic",null]},-1],["cr:1294159",["ReactDOM.classic"],{"__rc":["ReactDOM.classic",null]},-1],["UriNeedRawQuerySVConfig",[],{"uris":["dms.netmng.com","doubleclick.net","r.msn.com","watchit.sky.com","graphite.instagram.com","www.kfc.co.th","learn.pantheon.io","www.landmarkshops.in","www.ncl.com","s0.wp.com","www.tatacliq.com","bs.serving-sys.com","kohls.com","lazada.co.th","xg4ken.com","technopark.ru","officedepot.com.mx","bestbuy.com.mx","booking.com","nibio.no","myworkdayjobs.com","united-united.com","gcc.gnu.org"]},3871],["cr:5277",["ReactDOM.classic.prod-or-profiling"],{"__rc":["ReactDOM.classic.prod-or-profiling",null]},-1],["cr:1292365",["React-prod.classic"],{"__rc":["React-prod.classic",null]},-1],["cr:5278",["ReactDOM-prod.classic"],{"__rc":["ReactDOM-prod.classic",null]},-1],["cr:5695",["EventListenerWWW"],{"__rc":["EventListenerWWW",null]},-1],["cr:8909",["ReactFiberErrorDialogWWW"],{"__rc":["ReactFiberErrorDialogWWW",null]},-1],["cr:1353359",["CometEventListener"],{"__rc":["CometEventListener",null]},-1]],"require":[["WAWebCometRoot","init",[],[{"elementId":"mount_0_0_Zy"}]]]}},{"__bbox":{"require":[["qplTimingsServerJS",null,null,["7385063454178867264","tierOneBeforeScheduler"]]]}},{"__bbox":{"require":[["qplTimingsServerJS",null,null,["7385063454178867264","tierOneInsideScheduler"]]]}}]]]}</script>
|
||||
<script type="application/json" data-content-len="83" data-sjs>{"require":[["qplTimingsServerJS",null,null,["7385063454178867264","tierOneEnd"]]]}</script>
|
||||
|
||||
<script type="application/json" data-content-len="157" data-sjs>{"require":[["qplTimingsServerJS",null,null,["7385063454178867264","tierTwo"]],["qplTimingsServerJS",null,null,["7385063454178867264-server","tierTwo",33]]]}</script>
|
||||
<script type="application/json" data-content-len="2275" data-sjs>{"require":[["HasteSupportData","handle",null,[{"bxData":{"9547":{"uri":"https:\/\/static.whatsapp.net\/rsrc.php\/v3\/yl\/r\/gi_DckOUM5a.png"},"9548":{"uri":"https:\/\/static.whatsapp.net\/rsrc.php\/v3\/y_\/r\/TPtFe9SpDSX.png"},"9555":{"uri":"https:\/\/static.whatsapp.net\/rsrc.php\/v3\/y4\/r\/XEPQuYy90FJ.png"},"9556":{"uri":"https:\/\/static.whatsapp.net\/rsrc.php\/v3\/ye\/r\/fWOSgX_xpHG.png"},"9561":{"uri":"https:\/\/static.whatsapp.net\/rsrc.php\/yW\/r\/BS_BUUXbKq5.mp3"},"9562":{"uri":"https:\/\/static.whatsapp.net\/rsrc.php\/yu\/r\/lsK0FC6wi8C.m4a"},"10639":{"uri":"https:\/\/static.whatsapp.net\/rsrc.php\/v3\/y3\/r\/AZwgE-H9Ubq.png"},"11319":{"uri":"https:\/\/static.whatsapp.net\/rsrc.php\/v3\/yX\/r\/JYPizEwERE4.png"},"11370":{"uri":"https:\/\/static.whatsapp.net\/rsrc.php\/v3\/y-\/r\/_8J_xqERSbv.png"}},"clpData":{"6476":{"r":1000,"s":1},"1744178":{"r":1,"s":1},"1838142":{"r":1,"s":1}},"gkxData":{"1624":{"result":false,"hash":null},"2160":{"result":false,"hash":null},"20836":{"result":false,"hash":null},"20940":{"result":false,"hash":null},"21043":{"result":false,"hash":null},"21050":{"result":false,"hash":null},"21051":{"result":false,"hash":null},"21052":{"result":false,"hash":null},"21053":{"result":false,"hash":null},"21054":{"result":false,"hash":null},"21055":{"result":false,"hash":null},"21056":{"result":false,"hash":null},"21057":{"result":false,"hash":null},"21058":{"result":true,"hash":null},"21106":{"result":true,"hash":null},"21107":{"result":false,"hash":null},"21112":{"result":true,"hash":null},"26256":{"result":false,"hash":null},"26258":{"result":true,"hash":null},"26259":{"result":false,"hash":null},"26260":{"result":false,"hash":null},"26261":{"result":false,"hash":null},"26262":{"result":true,"hash":null},"26264":{"result":true,"hash":null},"26265":{"result":false,"hash":null}},"qplData":{"106":{}},"justknobxData":{"371":{"r":true},"819":{"r":true},"860":{"r":100},"1617":{"r":155},"1618":{"r":8},"1619":{"r":1},"1620":{"r":2},"1621":{"r":4},"1622":{"r":0},"1623":{"r":6},"1624":{"r":1},"1662":{"r":2},"1663":{"r":14},"1664":{"r":2},"1784":{"r":false},"1854":{"r":false},"2086":{"r":3},"2148":{"r":false},"2157":{"r":true},"2237":{"r":false},"2268":{"r":true},"2269":{"r":true},"2434":{"r":true},"2552":{"r":false}}}]]]}</script>
|
||||
<script type="application/json" data-content-len="30862" data-sjs>{"require":[["Bootloader","handlePayload",null,[{"consistency":{"rev":1014522270},"rsrcMap":{"4miG4t2":{"type":"js","src":"https:\/\/static.whatsapp.net\/rsrc.php\/v3\/yV\/r\/0gGYzfda2Tu.js?_nc_x=Ij3Wp8lg5Kz","c":1,"p":":22","m":"1014522270_main"},"xFqg8AH":{"type":"js","src":"https:\/\/static.whatsapp.net\/rsrc.php\/v3\/yL\/r\/BlIuoNYtWPV.js?_nc_x=Ij3Wp8lg5Kz","c":1,"p":":130","m":"1014522270_main"},"N4JDUAh":{"type":"js","src":"https:\/\/static.whatsapp.net\/rsrc.php\/v3\/ye\/r\/bgg-9nYiLie.js?_nc_x=Ij3Wp8lg5Kz","c":1,"p":":129","m":"1014522270_main"},"KBlGRAN":{"type":"js","src":"https:\/\/static.whatsapp.net\/rsrc.php\/v3\/yJ\/r\/nZUOmP0lYjZ.js?_nc_x=Ij3Wp8lg5Kz","c":1,"p":":38","m":"1014522270_main"},"Kyj0DuM":{"type":"js","src":"https:\/\/static.whatsapp.net\/rsrc.php\/v3\/yE\/r\/CtlyoX5avh8.js?_nc_x=Ij3Wp8lg5Kz","c":1,"p":":39","m":"1014522270_main"},"SWdcOen":{"type":"js","src":"https:\/\/static.whatsapp.net\/rsrc.php\/v3\/yk\/r\/tbbvhRiHhRw.js?_nc_x=Ij3Wp8lg5Kz","c":1,"p":":71","m":"1014522270_main"},"1qIb1yr":{"type":"js","src":"https:\/\/static.whatsapp.net\/rsrc.php\/v3ihVQ4\/ye\/l\/rt\/0FlCLfP5vYk.js?_nc_x=Ij3Wp8lg5Kz","tsrc":"https:\/\/static.whatsapp.net\/rsrc-translations.php\/v6ihVQ4\/yS\/l\/en_US\/0FlCLfP5vYk.js?_nc_x=Ij3Wp8lg5Kz","c":1,"p":":139","m":"1014522270_main"},"\/t7V6uL":{"type":"js","src":"https:\/\/static.whatsapp.net\/rsrc.php\/v3\/yI\/r\/Btaw39H8ma_.js?_nc_x=Ij3Wp8lg5Kz","c":1,"p":":137","m":"1014522270_main"},"oStldiG":{"type":"js","src":"https:\/\/static.whatsapp.net\/rsrc.php\/v3\/yQ\/r\/Q4Zwg9r5_k1.js?_nc_x=Ij3Wp8lg5Kz","c":1,"p":":68","m":"1014522270_main"},"jqmgzqL":{"type":"js","src":"https:\/\/static.whatsapp.net\/rsrc.php\/v3\/yG\/r\/ujDtbDfS_gy.js?_nc_x=Ij3Wp8lg5Kz","c":1,"p":":69","m":"1014522270_main"},"BYy6KKX":{"type":"js","src":"https:\/\/static.whatsapp.net\/rsrc.php\/v3\/yO\/r\/_WxPYKNWneH.js?_nc_x=Ij3Wp8lg5Kz","c":1,"p":":33","m":"1014522270_main"},"aoCs1Zg":{"type":"js","src":"https:\/\/static.whatsapp.net\/rsrc.php\/v3\/yH\/r\/XAD9_Dadb7D.js?_nc_x=Ij3Wp8lg5Kz","c":1,"p":":88","m":"1014522270_main"},"aOYtth2":{"type":"js","src":"https:\/\/static.whatsapp.net\/rsrc.php\/v3\/ye\/r\/iTqPNvdt0YL.js?_nc_x=Ij3Wp8lg5Kz","c":1,"p":":42","m":"1014522270_main"},"NFGWH2r":{"type":"js","src":"https:\/\/static.whatsapp.net\/rsrc.php\/v3\/yw\/r\/bfsDWnduEyW.js?_nc_x=Ij3Wp8lg5Kz","c":1,"p":":103","m":"1014522270_main"},"4QR59xK":{"type":"js","src":"https:\/\/static.whatsapp.net\/rsrc.php\/v3\/yP\/r\/6naoD9b2OZ5.js?_nc_x=Ij3Wp8lg5Kz","c":1,"p":":67","m":"1014522270_main"},"06faHSS":{"type":"js","src":"https:\/\/static.whatsapp.net\/rsrc.php\/v3\/yh\/r\/uvxX-bsXSXt.js?_nc_x=Ij3Wp8lg5Kz","c":1,"p":":155","m":"1014522270_main"},"onUpzfh":{"type":"js","src":"https:\/\/static.whatsapp.net\/rsrc.php\/v3\/yF\/r\/K6cSPvkmzMT.js?_nc_x=Ij3Wp8lg5Kz","c":1,"p":":124","m":"1014522270_main"},"RHKXYV8":{"type":"js","src":"https:\/\/static.whatsapp.net\/rsrc.php\/v3\/yq\/r\/oucl0iRauKC.js?_nc_x=Ij3Wp8lg5Kz","c":1,"p":":107","m":"1014522270_main"},"oVqtAlG":{"type":"js","src":"https:\/\/static.whatsapp.net\/rsrc.php\/v3\/yf\/r\/E8FehGQkWo7.js?_nc_x=Ij3Wp8lg5Kz","c":1,"p":":106","m":"1014522270_main"},"P+W1C4N":{"type":"js","src":"https:\/\/static.whatsapp.net\/rsrc.php\/v3\/yY\/r\/hu7wNvLGe08.js?_nc_x=Ij3Wp8lg5Kz","c":1,"p":":98","m":"1014522270_main"},"tw8mmSb":{"type":"js","src":"https:\/\/static.whatsapp.net\/rsrc.php\/v3\/yR\/r\/FZvPKCUdiMg.js?_nc_x=Ij3Wp8lg5Kz","c":1,"p":":164","m":"1014522270_main"},"TmxCppI":{"type":"js","src":"https:\/\/static.whatsapp.net\/rsrc.php\/v3\/yT\/r\/vIKHDl0uM89.js?_nc_x=Ij3Wp8lg5Kz","c":1,"p":":49","m":"1014522270_main"},"MkjpUky":{"type":"js","src":"https:\/\/static.whatsapp.net\/rsrc.php\/v3\/yx\/r\/daSMG4f170B.js?_nc_x=Ij3Wp8lg5Kz","c":1,"p":":92","m":"1014522270_main"},"IDB3BEc":{"type":"js","src":"https:\/\/static.whatsapp.net\/rsrc.php\/v3\/y-\/r\/zoLfZ0IIEoQ.js?_nc_x=Ij3Wp8lg5Kz","c":1,"p":":222","m":"1014522270_main"},"S25OcLy":{"type":"js","src":"https:\/\/static.whatsapp.net\/rsrc.php\/v3\/y9\/r\/ehQ6J4BH-Y5.js?_nc_x=Ij3Wp8lg5Kz","c":1,"p":":146","m":"1014522270_main"},"7yT1nwq":{"type":"js","src":"https:\/\/static.whatsapp.net\/rsrc.php\/v3\/ya\/r\/eeqAy7Pc9tB.js?_nc_x=Ij3Wp8lg5Kz","c":1,"p":":30493","m":"1014522270_longtail"},"hxw7aRX":{"type":"js","src":"https:\/\/static.whatsapp.net\/rsrc.php\/v3\/yW\/r\/o0te43Nno_1.js?_nc_x=Ij3Wp8lg5Kz","c":1,"p":":125","m":"1014522270_main"},"dax04J6":{"type":"js","src":"https:\/\/static.whatsapp.net\/rsrc.php\/v3\/yp\/r\/UuEvisQ2dNi.js?_nc_x=Ij3Wp8lg5Kz","c":1,"p":":236","m":"1014522270_main"},"bhIV+ni":{"type":"js","src":"https:\/\/static.whatsapp.net\/rsrc.php\/v3\/yR\/r\/_57wqNSBfQp.js?_nc_x=Ij3Wp8lg5Kz","c":1,"p":":244","m":"1014522270_main"},"9XxF5Q5":{"type":"js","src":"https:\/\/static.whatsapp.net\/rsrc.php\/v3\/y-\/r\/tU8GWxL8l8P.js?_nc_x=Ij3Wp8lg5Kz","c":1,"p":":192","m":"1014522270_main"},"AT3ZkSW":{"type":"js","src":"https:\Line truncated
|
||||
<script type="application/json" data-content-len="20293" data-sjs>{"require":[["ScheduledServerJS","handle",null,[{"__bbox":{"define":[["cr:310",["RunWWW"],{"__rc":["RunWWW",null]},-1],["cr:686",[],{"__rc":[null,null]},-1],["cr:1078",[],{"__rc":[null,null]},-1],["cr:1080",["unexpectedUseInComet"],{"__rc":["unexpectedUseInComet",null]},-1],["cr:1126",["TimeSliceSham"],{"__rc":["TimeSliceSham",null]},-1],["cr:3128",[],{"__rc":[null,null]},-1],["cr:3979",[],{"__rc":[null,null]},-1],["cr:4066",[],{"__rc":[null,null]},-1],["cr:4122",[],{"__rc":[null,null]},-1],["cr:4324",[],{"__rc":[null,null]},-1],["cr:4330",[],{"__rc":[null,null]},-1],["cr:4533",[],{"__rc":[null,null]},-1],["cr:4722",[],{"__rc":[null,null]},-1],["cr:4857",[],{"__rc":[null,null]},-1],["cr:4940",[],{"__rc":[null,null]},-1],["cr:4941",[],{"__rc":[null,null]},-1],["cr:4943",[],{"__rc":[null,null]},-1],["cr:5151",[],{"__rc":[null,null]},-1],["cr:5291",[],{"__rc":[null,null]},-1],["cr:5292",[],{"__rc":[null,null]},-1],["cr:5411",[],{"__rc":[null,null]},-1],["cr:5624",["WAWebCleanAssets"],{"__rc":["WAWebCleanAssets",null]},-1],["cr:5664",[],{"__rc":[null,null]},-1],["cr:5981",[],{"__rc":[null,null]},-1],["cr:6000",[],{"__rc":[null,null]},-1],["cr:6007",[],{"__rc":[null,null]},-1],["cr:6009",[],{"__rc":[null,null]},-1],["cr:6013",[],{"__rc":[null,null]},-1],["cr:6022",["WAWebFeatureDetectionSwSupport"],{"__rc":["WAWebFeatureDetectionSwSupport",null]},-1],["cr:7385",["clearIntervalWWW"],{"__rc":["clearIntervalWWW",null]},-1],["cr:7386",["clearTimeoutWWW"],{"__rc":["clearTimeoutWWW",null]},-1],["cr:7389",["setIntervalAcrossTransitionsWWW"],{"__rc":["setIntervalAcrossTransitionsWWW",null]},-1],["cr:7390",["setTimeoutWWW"],{"__rc":["setTimeoutWWW",null]},-1],["cr:7391",["setTimeoutAcrossTransitionsWWW"],{"__rc":["setTimeoutAcrossTransitionsWWW",null]},-1],["cr:7730",["getFbtResult"],{"__rc":["getFbtResult",null]},-1],["cr:7907",[],{"__rc":[null,null]},-1],["cr:8958",["FBJSON"],{"__rc":["FBJSON",null]},-1],["cr:8959",["DTSG"],{"__rc":["DTSG",null]},-1],["cr:8960",["DTSG_ASYNC"],{"__rc":["DTSG_ASYNC",null]},-1],["cr:9713",[],{"__rc":[null,null]},-1],["cr:696703",["JSScheduler"],{"__rc":["JSScheduler",null]},-1],["cr:951783",[],{"__rc":[null,null]},-1],["cr:1984081",[],{"__rc":[null,null]},-1],["IntlCurrentLocale",[],{"code":"en_US"},5954],["WAMediaWasmWorkerResource",[],{"url":"https:\/\/static.whatsapp.net\/rsrc.php\/v3\/yz\/r\/iqQa0jgcX_h.js?_nc_x=Ij3Wp8lg5Kz","name":"WAMediaWasmWorkerBundle","dynamicModules":{"cr:6640":{"__bbox":{"__rc":[null,null]}}},"hsdp":{"bxData":{"236":{"uri":"https:\/\/static.whatsapp.net\/rsrc.php\/y_\/r\/IbbOXFaNJTy.wasm"},"6945":{"uri":"https:\/\/static.whatsapp.net\/rsrc.php\/yO\/r\/tL59TT3QfUA.wasm"},"6946":{"uri":"https:\/\/static.whatsapp.net\/rsrc.php\/yf\/r\/rogm88TRRiw.wasm"}},"qplData":{"106":{}}}},7438],["CookiePrivacySandboxConfig",[],{"is_affected_by_samesite_lax":false},7723],["WAWebMediaWorkerResource",[],{"url":"https:\/\/static.whatsapp.net\/rsrc.php\/v3\/yK\/r\/PzZ2siM8S-2.js?_nc_x=Ij3Wp8lg5Kz","name":"WAMediaWorkerBundle","useInitScript":true},7034],["CometPersistQueryParams",[],{"relative":{},"domain":{}},6231],["CookieDomain",[],{"domain":"whatsapp.com"},6421],["IntlVariationHoldout",[],{"disable_variation":false},6533],["IntlNumberTypeProps",["IntlCLDRNumberType05"],{"module":{"__m":"IntlCLDRNumberType05"}},7027],["WAWebRgbaToWebpWorkerResource",[],{"url":"https:\/\/static.whatsapp.net\/rsrc.php\/v3\/y-\/r\/numw29EV6UR.js?_nc_x=Ij3Wp8lg5Kz","name":"WAWebRgbaToWebpWorkerBundle","dynamicModules":{"cr:6640":{"__bbox":{"__rc":[null,null]}}},"hsdp":{}},7480],["WAWebFtsWorkerResource",[],{"url":"https:\/\/static.whatsapp.net\/rsrc.php\/v3\/yc\/r\/lRajA4A_zrm.js?_nc_x=Ij3Wp8lg5Kz","name":"WAWebFtsWorkerBundle","dynamicModules":{"cr:6640":{"__bbox":{"__rc":[null,null]}}},"hsdp":{"gkxData":{"26258":{"result":true,"hash":null},"26259":{"result":false,"hash":null}},"justknobxData":{"1617":{"r":155},"1618":{"r":8},"1619":{"r":1},"1620":{"r":2},"1621":{"r":4},"1622":{"r":0},"1623":{"r":6},"1624":{"r":1},"1662":{"r":2},"1663":{"r":14},"1664":{"r":2},"1854":{"r":false}}}},7301],["WAWebOpusRecorderWorkerResource",[],{"url":"https:\/\/static.whatsapp.net\/rsrc.php\/v3\/yw\/r\/_OjcNvgJb7R.js?_nc_x=Ij3Wp8lg5Kz","name":"WAWebOpusRecorderWorkerBundle","hsdp":{}},7347],["WAWebWebpToRgbaWorkerResource",[],{"url":"https:\/\/static.whatsapp.net\/rsrc.php\/v3\/yi\/r\/e0-X3rIGNEC.js?_nc_x=Ij3Wp8lg5Kz","name":"WAWebWebpToRgbaWorkerBundle","dynamicModules":{"cr:6640":{"__bbox":{"__rc":[null,null]}}},"hsdp":{}},7481],["GetAsyncParamsExtraData",[],{"extra_data":{}},7511],["BootloaderConfig",[],{"deferBootloads":false,"jsRetries":[200,500],"jsRetryAbortNum":2,"jsRetryAbortTime":5,"silentDups":true,"timeout":60000,"tieredLoadingFromTier":100,"hypStep4":true,"phdOn":false,"btCutoffIndex":259,"fastPathForAlreadyRequired":true,"earlyRequireLazy":false,"enableTimeoutLoggingForNonComet":false,"deferLongTailManifest":true,"lazySoT":false,"transLine truncated
|
||||
<script type="application/json" data-content-len="83" data-sjs>{"require":[["qplTimingsServerJS",null,null,["7385063454178867264","tierTwoEnd"]]]}</script>
|
||||
|
||||
<script type="application/json" data-content-len="161" data-sjs>{"require":[["qplTimingsServerJS",null,null,["7385063454178867264","tierThree"]],["qplTimingsServerJS",null,null,["7385063454178867264-server","tierThree",51]]]}</script>
|
||||
<link rel="preload" href="https://static.whatsapp.net/rsrc.php/v3/yw/l/0,cross/J-dbIEvJjBeMKI_FluK_QX.css?_nc_x=Ij3Wp8lg5Kz" as="style" crossorigin="anonymous" />
|
||||
<script type="application/json" data-content-len="2043" data-sjs>{"require":[["HasteSupportData","handle",null,[{"bxData":{"482":{"uri":"https:\/\/static.whatsapp.net\/rsrc.php\/v3\/yS\/r\/aGcVD59xVTb.png"},"484":{"uri":"https:\/\/static.whatsapp.net\/rsrc.php\/v3\/y6\/r\/wa669aeJeom.png"},"9544":{"uri":"https:\/\/static.whatsapp.net\/rsrc.php\/yv\/r\/6Ib-MkrweJ6.ttf"},"9545":{"uri":"https:\/\/static.whatsapp.net\/rsrc.php\/yk\/r\/ln-uwGsChQW.ttf"},"9546":{"uri":"https:\/\/static.whatsapp.net\/rsrc.php\/v3\/yf\/r\/4l5Jzvrg84p.png"},"9551":{"uri":"https:\/\/static.whatsapp.net\/rsrc.php\/v3\/yq\/r\/xCHrap4ZvgY.png"},"9552":{"uri":"https:\/\/static.whatsapp.net\/rsrc.php\/v3\/y6\/r\/R6k6NrF2Cbl.png"},"9553":{"uri":"https:\/\/static.whatsapp.net\/rsrc.php\/v3\/yd\/r\/jLlIEwASpgj.png"},"9554":{"uri":"https:\/\/static.whatsapp.net\/rsrc.php\/v3\/y5\/r\/_U7GO8EnPFR.png"},"9559":{"uri":"https:\/\/static.whatsapp.net\/rsrc.php\/v3\/yy\/r\/noBSk9ItwJJ.png"},"9563":{"uri":"https:\/\/static.whatsapp.net\/rsrc.php\/yQ\/r\/WElxnwAGYY2.mp3"},"9564":{"uri":"https:\/\/static.whatsapp.net\/rsrc.php\/yv\/r\/ze2kHBOq8T0.mp3"},"9583":{"uri":"https:\/\/static.whatsapp.net\/rsrc.php\/v3\/yP\/r\/re6de1QF8pQ.png"},"10047":{"uri":"https:\/\/static.whatsapp.net\/rsrc.php\/v1\/y6\/r\/kNeGAbVVWRf.jpg"},"10591":{"uri":"https:\/\/static.whatsapp.net\/rsrc.php\/v3\/yp\/r\/ReKDgNfJEFB.png"},"14943":{"uri":"https:\/\/static.whatsapp.net\/rsrc.php\/v3\/ye\/r\/W2MDyeo0zkf.png"},"9557":{"uri":"https:\/\/static.whatsapp.net\/rsrc.php\/v3\/y5\/r\/cJXjAoffU5Q.png"},"9558":{"uri":"https:\/\/static.whatsapp.net\/rsrc.php\/v3\/yB\/r\/7Y1jh45L_8V.png"},"9565":{"uri":"https:\/\/static.whatsapp.net\/rsrc.php\/yn\/r\/6swO8kJM8z2.mp4"},"9566":{"uri":"https:\/\/static.whatsapp.net\/rsrc.php\/yk\/r\/GoZyw2bTK6s.mp4"},"9560":{"uri":"https:\/\/static.whatsapp.net\/rsrc.php\/v3\/yo\/r\/Oh4t5oP-H5G.png"}},"clpData":{"1837559":{"r":1},"1848815":{"r":10000,"s":1},"1828945":{"r":100,"s":1},"5779":{"r":100,"s":1}},"justknobxData":{"2454":{"r":false},"2450":{"r":true},"2451":{"r":true},"2452":{"r":false},"2453":{"r":false}}}]]]}</script>
|
||||
<script type="application/json" data-content-len="53047" data-sjs>{"require":[["Bootloader","handlePayload",null,[{"consistency":{"rev":1014522270},"rsrcMap":{"XGxTRzz":{"type":"js","src":"https:\/\/static.whatsapp.net\/rsrc.php\/v3iMK54\/yW\/l\/rt\/bMaImlcHRUG.js?_nc_x=Ij3Wp8lg5Kz","tsrc":"https:\/\/static.whatsapp.net\/rsrc-translations.php\/v6iMK54\/y2\/l\/en_US\/bMaImlcHRUG.js?_nc_x=Ij3Wp8lg5Kz","c":1,"p":":32","m":"1014522270_main"},"mRZc\/dz":{"type":"js","src":"https:\/\/static.whatsapp.net\/rsrc.php\/v3\/yx\/r\/m96dxlCrt6Y.js?_nc_x=Ij3Wp8lg5Kz","c":1,"p":":26","m":"1014522270_main"},"eGE9FvR":{"type":"js","src":"https:\/\/static.whatsapp.net\/rsrc.php\/v3i0ta4\/yi\/l\/rt\/Ad_ijr7_dvd.js?_nc_x=Ij3Wp8lg5Kz","tsrc":"https:\/\/static.whatsapp.net\/rsrc-translations.php\/v6i0ta4\/ym\/l\/en_US\/Ad_ijr7_dvd.js?_nc_x=Ij3Wp8lg5Kz","c":1,"p":":23","m":"1014522270_main"},"ssxIBpF":{"type":"js","src":"https:\/\/static.whatsapp.net\/rsrc.php\/v3\/yZ\/r\/FPlNguXyB9e0gGYzfda2TufH5Ma2uwGKhY6Mk0m90rCLDQVEH-g-FDC.js?_nc_x=Ij3Wp8lg5Kz","c":1,"p":":30,22,15,16,9","m":"1014522270_main"},"GkupZaS":{"type":"js","src":"https:\/\/static.whatsapp.net\/rsrc.php\/v3iCqe4\/yL\/l\/rt\/2gPPVV2aJv5.js?_nc_x=Ij3Wp8lg5Kz","tsrc":"https:\/\/static.whatsapp.net\/rsrc-translations.php\/v6iCqe4\/yu\/l\/en_US\/2gPPVV2aJv5.js?_nc_x=Ij3Wp8lg5Kz","c":1,"p":":25","m":"1014522270_main"},"EKhm2GG":{"type":"css","src":"https:\/\/static.whatsapp.net\/rsrc.php\/v3\/yw\/l\/0,cross\/J-dbIEvJjBeMKI_FluK_QX.css?_nc_x=Ij3Wp8lg5Kz","c":1,"p":":27,31"},"IYqO5iZ":{"type":"js","src":"https:\/\/static.whatsapp.net\/rsrc.php\/v3\/yt\/r\/ZqmYe3zRs7G.js?_nc_x=Ij3Wp8lg5Kz","c":1,"p":":17","m":"1014522270_main"},"tslua06":{"type":"js","src":"https:\/\/static.whatsapp.net\/rsrc.php\/v3i9SF4\/yS\/l\/rt\/Z3qyCROs6-7.js?_nc_x=Ij3Wp8lg5Kz","tsrc":"https:\/\/static.whatsapp.net\/rsrc-translations.php\/v6i9SF4\/yp\/l\/en_US\/Z3qyCROs6-7.js?_nc_x=Ij3Wp8lg5Kz","c":1,"p":":24","m":"1014522270_main"},"aQxqT5q":{"type":"js","src":"https:\/\/static.whatsapp.net\/rsrc.php\/v3ifY44\/yE\/l\/rt\/YaSkaFGEr-I.js?_nc_x=Ij3Wp8lg5Kz","tsrc":"https:\/\/static.whatsapp.net\/rsrc-translations.php\/v6ifY44\/y6\/l\/en_US\/YaSkaFGEr-I.js?_nc_x=Ij3Wp8lg5Kz","c":1,"p":":35","m":"1014522270_main"},"DrHIMI8":{"type":"js","src":"https:\/\/static.whatsapp.net\/rsrc.php\/v3iteW4\/yc\/l\/rt\/3apowRxtrqR.js?_nc_x=Ij3Wp8lg5Kz","tsrc":"https:\/\/static.whatsapp.net\/rsrc-translations.php\/v6iteW4\/yE\/l\/en_US\/3apowRxtrqR.js?_nc_x=Ij3Wp8lg5Kz","c":1,"p":":131","m":"1014522270_main"},"sevwJOE":{"type":"js","src":"https:\/\/static.whatsapp.net\/rsrc.php\/v3iOwq4\/y0\/l\/rt\/k5EhubSdxpm.js?_nc_x=Ij3Wp8lg5Kz","tsrc":"https:\/\/static.whatsapp.net\/rsrc-translations.php\/v6iOwq4\/y6\/l\/en_US\/k5EhubSdxpm.js?_nc_x=Ij3Wp8lg5Kz","c":1,"p":":21","m":"1014522270_main"},"DcG2ejT":{"type":"js","src":"https:\/\/static.whatsapp.net\/rsrc.php\/v3ipMd4\/y0\/l\/rt\/GquHjbFn54u.js?_nc_x=Ij3Wp8lg5Kz","tsrc":"https:\/\/static.whatsapp.net\/rsrc-translations.php\/v6ipMd4\/yL\/l\/en_US\/GquHjbFn54u.js?_nc_x=Ij3Wp8lg5Kz","c":1,"p":":153","m":"1014522270_main"},"YS\/UrnR":{"type":"js","src":"https:\/\/static.whatsapp.net\/rsrc.php\/v3iXDa4\/yV\/l\/rt\/Zz-Ouubt7zy.js?_nc_x=Ij3Wp8lg5Kz","tsrc":"https:\/\/static.whatsapp.net\/rsrc-translations.php\/v6iXDa4\/yS\/l\/en_US\/Zz-Ouubt7zy.js?_nc_x=Ij3Wp8lg5Kz","c":1,"p":":34","m":"1014522270_main"},"zG5Hlng":{"type":"js","src":"https:\/\/static.whatsapp.net\/rsrc.php\/v3\/yy\/r\/OkuaRXwmGgU.js?_nc_x=Ij3Wp8lg5Kz","c":1,"p":":28","m":"1014522270_main"},"0SXrVmz":{"type":"js","src":"https:\/\/static.whatsapp.net\/rsrc.php\/v3iuDY4\/y0\/l\/rt\/vA2-0lRViF_.js?_nc_x=Ij3Wp8lg5Kz","tsrc":"https:\/\/static.whatsapp.net\/rsrc-translations.php\/v6iuDY4\/yM\/l\/en_US\/vA2-0lRViF_.js?_nc_x=Ij3Wp8lg5Kz","c":1,"p":":29","m":"1014522270_main"},"kLGrgwC":{"type":"js","src":"https:\/\/static.whatsapp.net\/rsrc.php\/v3inji4\/yG\/l\/rt\/vWzRuWZmn1W.js?_nc_x=Ij3Wp8lg5Kz","tsrc":"https:\/\/static.whatsapp.net\/rsrc-translations.php\/v6inji4\/yj\/l\/en_US\/vWzRuWZmn1W.js?_nc_x=Ij3Wp8lg5Kz","c":1,"p":":174","m":"1014522270_main"},"hikr3RH":{"type":"js","src":"https:\/\/static.whatsapp.net\/rsrc.php\/v3iYv84\/yJ\/l\/rt\/N41CKm1lGBa.js?_nc_x=Ij3Wp8lg5Kz","tsrc":"https:\/\/static.whatsapp.net\/rsrc-translations.php\/v6iYv84\/yj\/l\/en_US\/N41CKm1lGBa.js?_nc_x=Ij3Wp8lg5Kz","c":1,"p":":57","m":"1014522270_main"},"fY+RSol":{"type":"js","src":"https:\/\/static.whatsapp.net\/rsrc.php\/v3iNj84\/y9\/l\/rt\/6rn8EUgGGpB.js?_nc_x=Ij3Wp8lg5Kz","tsrc":"https:\/\/static.whatsapp.net\/rsrc-translations.php\/v6iNj84\/yI\/l\/en_US\/6rn8EUgGGpB.js?_nc_x=Ij3Wp8lg5Kz","c":1,"p":":122","m":"1014522270_main"},"\/9QLqMI":{"type":"js","src":"https:\/\/static.whatsapp.net\/rsrc.php\/v3\/yb\/r\/qXYvgBWQS1q.js?_nc_x=Ij3Wp8lg5Kz","c":1,"p":":113","m":"1014522270_main"},"rfKxJtC":{"type":"js","src":"https:\/\/static.whatsapp.net\/rsrc.php\/v3iJyQ4\/yM\/l\/rt\/Ul0GyaekHhI.js?_nc_x=Ij3Wp8lg5Kz","tsrc":"https:\/\/static.whatsapp.net\/Line truncated
|
||||
<script type="application/json" data-content-len="130" data-sjs>{"require":[["DeferredJSResourceScheduler",null,null,[["XGxTRzz","mRZc\/dz","eGE9FvR","ssxIBpF","GkupZaS","IYqO5iZ","tslua06"]]]]}</script>
|
||||
<script type="application/json" data-content-len="7789" data-sjs>{"require":[["ScheduledServerJSWithCSS","handle",null,[{"__bbox":{"define":[["cr:104",[],{"__rc":[null,null]},-1],["cr:291",["ReactDOMServer-prod.modern"],{"__rc":["ReactDOMServer-prod.modern",null]},-1],["cr:293",["LexicalHistoryPlugin.prod"],{"__rc":["LexicalHistoryPlugin.prod",null]},-1],["cr:370",["Lexical.prod"],{"__rc":["Lexical.prod",null]},-1],["cr:427",["LexicalComposer.prod"],{"__rc":["LexicalComposer.prod",null]},-1],["cr:509",["LexicalComposerContext.prod"],{"__rc":["LexicalComposerContext.prod",null]},-1],["cr:629",[],{"__rc":[null,null]},-1],["cr:739",["LexicalPlainTextPlugin.prod"],{"__rc":["LexicalPlainTextPlugin.prod",null]},-1],["cr:988",["LexicalTreeView.prod"],{"__rc":["LexicalTreeView.prod",null]},-1],["cr:1066",[],{"__rc":[null,null]},-1],["cr:2015",[],{"__rc":[null,null]},-1],["cr:2120",["LexicalClearEditorPlugin.prod"],{"__rc":["LexicalClearEditorPlugin.prod",null]},-1],["cr:2180",["LexicalSelection.prod"],{"__rc":["LexicalSelection.prod",null]},-1],["cr:2260",["LexicalErrorBoundary.prod"],{"__rc":["LexicalErrorBoundary.prod",null]},-1],["cr:2774",["LexicalUtils.prod"],{"__rc":["LexicalUtils.prod",null]},-1],["cr:3107",[],{"__rc":[null,null]},-1],["cr:5037",[],{"__rc":[null,null]},-1],["cr:5685",[],{"__rc":[null,null]},-1],["cr:5735",[],{"__rc":[null,null]},-1],["cr:5736",[],{"__rc":[null,null]},-1],["cr:5923",[],{"__rc":[null,null]},-1],["cr:5932",[],{"__rc":[null,null]},-1],["cr:5959",[],{"__rc":[null,null]},-1],["cr:5990",[],{"__rc":[null,null]},-1],["cr:5993",[],{"__rc":[null,null]},-1],["cr:5994",[],{"__rc":[null,null]},-1],["cr:5997",[],{"__rc":[null,null]},-1],["cr:6003",[],{"__rc":[null,null]},-1],["cr:6004",[],{"__rc":[null,null]},-1],["cr:6005",[],{"__rc":[null,null]},-1],["cr:6006",[],{"__rc":[null,null]},-1],["cr:6010",[],{"__rc":[null,null]},-1],["cr:6018",[],{"__rc":[null,null]},-1],["cr:6019",[],{"__rc":[null,null]},-1],["cr:6842",[],{"__rc":[null,null]},-1],["cr:6862",[],{"__rc":[null,null]},-1],["cr:7456",[],{"__rc":[null,null]},-1],["cr:8723",[],{"__rc":[null,null]},-1],["cr:8750",[],{"__rc":[null,null]},-1],["cr:8751",[],{"__rc":[null,null]},-1],["cr:8752",[],{"__rc":[null,null]},-1],["cr:8753",[],{"__rc":[null,null]},-1],["cr:9051",[],{"__rc":[null,null]},-1],["cr:30",[],{"__rc":[null,null]},-1],["cr:8828",[],{"__rc":[null,null]},-1],["cr:1094907",[],{"__rc":[null,null]},-1],["WAWebWebpToAnimationFramesWorkerResource",[],{"url":"https:\/\/static.whatsapp.net\/rsrc.php\/v3\/yj\/r\/keURb6s4gBJ.js?_nc_x=Ij3Wp8lg5Kz","name":"WAWebWebpToAnimationFramesWorkerBundle","dynamicModules":{"cr:6640":{"__bbox":{"__rc":[null,null]}}},"hsdp":{}},7485],["cr:4986",[],{"__rc":[null,null]},-1],["cr:7004",["ReactReconcilerConstants.classic"],{"__rc":["ReactReconcilerConstants.classic",null]},-1],["cr:7007",["ReactReconciler.classic"],{"__rc":["ReactReconciler.classic",null]},-1],["cr:2695",[],{"__rc":[null,null]},-1],["cr:6008",[],{"__rc":[null,null]},-1],["cr:2904",["LexicalHistory.prod"],{"__rc":["LexicalHistory.prod",null]},-1],["cr:2181",["LexicalText.prod"],{"__rc":["LexicalText.prod",null]},-1],["cr:2903",["LexicalDragon.prod"],{"__rc":["LexicalDragon.prod",null]},-1],["cr:2909",["LexicalPlainText.prod"],{"__rc":["LexicalPlainText.prod",null]},-1],["cr:4793",["useLexicalEditable.prod"],{"__rc":["useLexicalEditable.prod",null]},-1],["cr:9917",["LexicalDevtoolsCore.prod"],{"__rc":["LexicalDevtoolsCore.prod",null]},-1],["cr:7002",["ReactReconcilerConstants-prod.classic"],{"__rc":["ReactReconcilerConstants-prod.classic",null]},-1],["cr:6994",["ReactReconciler-prod.classic"],{"__rc":["ReactReconciler-prod.classic",null]},-1],["cr:862",["LexicalClipboard.prod"],{"__rc":["LexicalClipboard.prod",null]},-1],["cr:1497",["LexicalHtml.prod"],{"__rc":["LexicalHtml.prod",null]},-1],["cr:1950",["LexicalTable.prod"],{"__rc":["LexicalTable.prod",null]},-1],["cr:2905",["LexicalLink.prod"],{"__rc":["LexicalLink.prod",null]},-1],["cr:3295",["LexicalMark.prod"],{"__rc":["LexicalMark.prod",null]},-1]],"require":[["CometSuspenseFalcoEvent"],["WAWebMainWrapper"],["WAFtsMultiLangTokenizer"],["WAWebEmojiAssetLoaderConfig"],["asn1js-2.1.1"],["WAWebEmojiAssetLoader"],["WAWebSetWorkerSafeHandlerApi"],["WAWebSetFrontendHandlerApi"],["WAWebPipVideoStreaming"],["WAWebLinkDevice.react"],["WAWebClearAppStates"],["FbtLogging"],["IntlQtEventFalcoEvent"],["emptyFunction","thatReturns",["RequireDeferredReference"],[[{"__dr":"CometSuspenseFalcoEvent"},{"__dr":"WAWebMainWrapper"},{"__dr":"WAFtsMultiLangTokenizer"},{"__dr":"WAWebEmojiAssetLoaderConfig"},{"__dr":"asn1js-2.1.1"},{"__dr":"WAWebEmojiAssetLoader"},{"__dr":"WAWebSetWorkerSafeHandlerApi"},{"__dr":"WAWebSetFrontendHandlerApi"},{"__dr":"WAWebPipVideoStreaming"},{"__dr":"WAWebLinkDevice.react"},{"__dr":"WAWebClearAppStates"},{"__dr":"FbtLogging"},{"__dr":"IntlQtEventFalcoEvent"}]]],["CometBTManifestLoader","init",[],[]],["ServiceWorkerCacheManagement","manageCache",[],[]],["WAWebEmojiSetup"],["BLine truncated
|
||||
<script type="application/json" data-content-len="85" data-sjs>{"require":[["qplTimingsServerJS",null,null,["7385063454178867264","tierThreeEnd"]]]}</script>
|
||||
|
||||
<script type="application/json" data-content-len="3705" data-sjs>{"require":[["CometQPLPayloadStore","storePayloadMap",null,[{"7385063454178867264":{"css\/7f2fjyjrk0cocg0s.pkg,css\/dkp7c20r8cg08o40.pkg,css\/a2k3bgidd4gsc8g0.pkg.__composite__.css":{"url":"https:\/\/static.whatsapp.net\/rsrc.php\/v3\/yv\/l\/0,cross\/C-WCp2i_C8v1k_iwjHwlnlCsHvfmgKJ5t.css?_nc_x=Ij3Wp8lg5Kz","refs":["htmlStart","tierOne","tierTwo","tierThree"]},"js\/1uuwsb5hks5c8c8s.pkg.js":{"url":"https:\/\/static.whatsapp.net\/rsrc.php\/v3\/yH\/r\/ht8oIC_2H5Y.js?_nc_x=Ij3Wp8lg5Kz","refs":["htmlStart","tierOne","tierTwo","tierThree"]},"js\/cuwb8pci4r48scww.pkg.js":{"url":"https:\/\/static.whatsapp.net\/rsrc.php\/v3\/yV\/r\/yJJyU7535wL.js?_nc_x=Ij3Wp8lg5Kz","refs":["htmlStart","tierOne","tierTwo"]},"js\/74ff0y83uq044k80.pkg,js\/7l01ghsx2tc04kcc.pkg,js\/boe4n1nexrwc0ws8.pkg.__composite__.js":{"url":"https:\/\/static.whatsapp.net\/rsrc.php\/v3\/y3\/r\/GpR8XYkiC6brtOgQ7kvhNmryQSgyNOW8X.js?_nc_x=Ij3Wp8lg5Kz","refs":["htmlStart","tierOne","tierTwo","tierThree"]},"js\/bjjs1pmj96gwss4w.pkg.js":{"url":"https:\/\/static.whatsapp.net\/rsrc.php\/v3\/y3\/r\/y6YE2mWjTXs.js?_nc_x=Ij3Wp8lg5Kz","refs":["htmlStart","tierOne","tierTwo","tierThree"]},"js\/ckuz7ky8y9cssc8s.pkg.js":{"url":"https:\/\/static.whatsapp.net\/rsrc.php\/v3iPAE4\/yj\/l\/rt\/3A4_-xOlNcy.js?_nc_x=Ij3Wp8lg5Kz","refs":["htmlStart","tierTwo","tierThree"]},"js\/45xcw3rbngqo884k.pkg.js":{"url":"https:\/\/static.whatsapp.net\/rsrc.php\/v3iL1C4\/yq\/l\/rt\/D6yUOPmld1J.js?_nc_x=Ij3Wp8lg5Kz","refs":["htmlStart","tierTwo","tierThree"]},"js\/93xi0q6qcx0ksosc.pkg,js\/ajfm1uik02woo48s.pkg,js\/csa9qir353c4goc0.pkg.__composite__.js":{"url":"https:\/\/static.whatsapp.net\/rsrc.php\/v3iD_74\/yY\/l\/rt\/OC6RHC1oEdtsQ3d6kRL04kvsWqehdEtWw.js?_nc_x=Ij3Wp8lg5Kz","refs":["htmlStart","tierTwo","tierThree"]},"js\/cq8i5dizka0oow8o.pkg.js":{"url":"https:\/\/static.whatsapp.net\/rsrc.php\/v3\/yD\/r\/mY5slrRbdpI.js?_nc_x=Ij3Wp8lg5Kz","refs":["htmlStart","tierTwo","tierThree"]},"js\/bm1z2ax0ttkwg8w8.pkg.js":{"url":"https:\/\/static.whatsapp.net\/rsrc.php\/v3\/yt\/r\/fyeayg1M_Ow.js?_nc_x=Ij3Wp8lg5Kz","refs":["htmlStart","tierTwo","tierThree"]},"css\/whatsapp\/web\/WhatsAppWebSplashScreen.css":{"url":"https:\/\/static.whatsapp.net\/rsrc.php\/v3\/yt\/l\/0,cross\/ZrtiQW1FOc2.css?_nc_x=Ij3Wp8lg5Kz","refs":["tierOne"]},"js\/3iilb6qol88wkw0k.pkg.js":{"url":"https:\/\/static.whatsapp.net\/rsrc.php\/v3iMK54\/yW\/l\/rt\/bMaImlcHRUG.js?_nc_x=Ij3Wp8lg5Kz","refs":["tierThree"]},"js\/38kxevuv39q8kgkc.pkg.js":{"url":"https:\/\/static.whatsapp.net\/rsrc.php\/v3\/yx\/r\/m96dxlCrt6Y.js?_nc_x=Ij3Wp8lg5Kz","refs":["tierThree"]},"js\/dy1urilp36ogkcc8.pkg.js":{"url":"https:\/\/static.whatsapp.net\/rsrc.php\/v3i0ta4\/yi\/l\/rt\/Ad_ijr7_dvd.js?_nc_x=Ij3Wp8lg5Kz","refs":["tierThree"]},"js\/cdkr9wbfnr40ssg0.pkg,js\/7mvpi199fpwc44ss.pkg,js\/3121sq3e8pmo44k0.pkg,js\/7y8xjqkbm0w0kks8.pkg,js\/529myxqorq4gkcgw.pkg.__composite__.js":{"url":"https:\/\/static.whatsapp.net\/rsrc.php\/v3\/yZ\/r\/FPlNguXyB9e0gGYzfda2TufH5Ma2uwGKhY6Mk0m90rCLDQVEH-g-FDC.js?_nc_x=Ij3Wp8lg5Kz","refs":["tierThree"]},"js\/bc0aut869zk84448.pkg.js":{"url":"https:\/\/static.whatsapp.net\/rsrc.php\/v3iCqe4\/yL\/l\/rt\/2gPPVV2aJv5.js?_nc_x=Ij3Wp8lg5Kz","refs":["tierThree"]},"css\/ezxomokk1nw4w04k.pkg,css\/7bsaf1zhofk8c440.pkg.__composite__.css":{"url":"https:\/\/static.whatsapp.net\/rsrc.php\/v3\/yw\/l\/0,cross\/J-dbIEvJjBeMKI_FluK_QX.css?_nc_x=Ij3Wp8lg5Kz","refs":["tierThree"]},"js\/dt5kj0sja4o4kwos.pkg.js":{"url":"https:\/\/static.whatsapp.net\/rsrc.php\/v3\/yt\/r\/ZqmYe3zRs7G.js?_nc_x=Ij3Wp8lg5Kz","refs":["tierThree"]},"js\/8v1j52evupkwsc0o.pkg.js":{"url":"https:\/\/static.whatsapp.net\/rsrc.php\/v3i9SF4\/yS\/l\/rt\/Z3qyCROs6-7.js?_nc_x=Ij3Wp8lg5Kz","refs":["tierThree"]}}}]]]}</script>
|
||||
<script type="application/json" data-content-len="69" data-sjs>{"require":[["qplTagServerJS",null,null,[["lastServerTagFlushed"]]]]}</script>
|
||||
<script type="application/json" data-content-len="1381" data-sjs>{"require":[["qplTimingsServerJS",null,null,["7385063454178867264-server","earlyFlushStart",6]],["qplTimingsServerJS",null,null,["7385063454178867264-server","earlyFlushEnd",12]],["qplTimingsServerJS",null,null,["7385063454178867264-server","earlyFlush",12]],["qplTimingsServerJS",null,null,["7385063454178867264-server","cometPageRender",13]],["qplTimingsServerJS",null,null,["7385063454178867264-server","genTierOneHasteResponseStart",14]],["qplTimingsServerJS",null,null,["7385063454178867264-server","genTierOneHasteResponseEnd",15]],["qplTimingsServerJS",null,null,["7385063454178867264-server","tierOnePreloadersFinished",16]],["qplTimingsServerJS",null,null,["7385063454178867264-server","genTierTwoHasteResponseStart",26]],["qplTimingsServerJS",null,null,["7385063454178867264-server","genTierTwoHasteResponseEnd",32]],["qplTimingsServerJS",null,null,["7385063454178867264-server","tierTwoPreloadersFinished",34]],["qplTimingsServerJS",null,null,["7385063454178867264-server","genTierThreeHasteResponseStart",39]],["qplTimingsServerJS",null,null,["7385063454178867264-server","genTierThreeHasteResponseEnd",50]],["qplTimingsServerJS",null,null,["7385063454178867264-server","tierThreePreloadersFinished",52]],["qplTimingsServerJS",null,null,["7385063454178867264-server","responseStart",1]],["qplTimingsServerJS",null,null,["7385063454178867264-server","responseEnd",53]]]}</script>
|
||||
<script type="application/json" data-content-len="2" data-sjs>{}</script>
|
||||
<script type="application/json" data-content-len="167" data-sjs>{"require":[["CometQPLPayloadStore","storePayloadBytesSent",null,[{"htmlStart":[15482,4730],"tierOne":[7727,2381],"tierTwo":[54050,14562],"tierThree":[63872,9826]}]]]}</script>
|
||||
<div id='has-finished-comet-page' hidden style='display: none;'></div></body></html>
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
exports.LoadUtils = () => {
|
||||
window.WAHA = {};
|
||||
|
||||
window.WAHA.getChats = async (limit, offset) => {
|
||||
let chats = window.Store.Chat.getModelsArray();
|
||||
if (limit || offset) {
|
||||
offset = offset || 0;
|
||||
limit = limit || Infinity;
|
||||
chats = chats.slice(offset, offset + limit);
|
||||
}
|
||||
|
||||
const chatPromises = chats.map((chat) => window.WWebJS.getChatModel(chat));
|
||||
return await Promise.all(chatPromises);
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,35 @@
|
||||
import { Client } from 'whatsapp-web.js';
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
const { LoadUtils } = require('./WahaInjected');
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
const ChatFactory = require('whatsapp-web.js/src/factories/ChatFactory');
|
||||
|
||||
export class WebjsClient extends Client {
|
||||
async initialize() {
|
||||
await super.initialize();
|
||||
//Load util functions (serializers, helper functions)
|
||||
await this.pupPage.evaluate(LoadUtils);
|
||||
}
|
||||
|
||||
async getChats(limit?: number, offset?: number) {
|
||||
if (limit == null && offset == null) {
|
||||
return await super.getChats();
|
||||
}
|
||||
return await this.getChats2(limit, offset);
|
||||
}
|
||||
|
||||
private async getChats2(limit?: number, offset?: number) {
|
||||
const chats = await this.pupPage.evaluate(
|
||||
async (limit, offset) => {
|
||||
// @ts-ignore
|
||||
return await window.WAHA.getChats(limit, offset);
|
||||
},
|
||||
limit,
|
||||
offset,
|
||||
);
|
||||
|
||||
return chats.map((chat) => ChatFactory.create(this, chat));
|
||||
}
|
||||
}
|
||||
@@ -1,17 +1,24 @@
|
||||
import { UnprocessableEntityException } from '@nestjs/common/exceptions/unprocessable-entity.exception';
|
||||
import { IEngineMediaProcessor } from '@waha/core/abc/media.abc';
|
||||
import {
|
||||
Chat,
|
||||
Client,
|
||||
ClientOptions,
|
||||
Contact,
|
||||
Events,
|
||||
GroupChat,
|
||||
Location,
|
||||
Message,
|
||||
Reaction,
|
||||
} from 'whatsapp-web.js';
|
||||
import { Message as MessageInstance } from 'whatsapp-web.js/src/structures';
|
||||
|
||||
getChannelInviteLink,
|
||||
WAHAInternalEvent,
|
||||
WhatsappSession,
|
||||
} from '@waha/core/abc/session.abc';
|
||||
import { WebjsClient } from '@waha/core/engines/webjs/WebjsClient';
|
||||
import {
|
||||
AvailableInPlusVersion,
|
||||
NotImplementedByEngineError,
|
||||
} from '@waha/core/exceptions';
|
||||
import { QR } from '@waha/core/QR';
|
||||
import { parseBool } from '@waha/helpers';
|
||||
import {
|
||||
Channel,
|
||||
ChannelRole,
|
||||
CreateChannelRequest,
|
||||
ListChannelsQuery,
|
||||
} from '@waha/structures/channels.dto';
|
||||
import { GetChatsQuery } from '@waha/structures/chats.dto';
|
||||
import {
|
||||
ChatRequest,
|
||||
CheckNumberStatusQuery,
|
||||
@@ -27,55 +34,61 @@ import {
|
||||
MessageVoiceRequest,
|
||||
SendSeenRequest,
|
||||
WANumberExistResult,
|
||||
} from '../../../structures/chatting.dto';
|
||||
import { ContactQuery, ContactRequest } from '../../../structures/contacts.dto';
|
||||
} from '@waha/structures/chatting.dto';
|
||||
import { ContactQuery, ContactRequest } from '@waha/structures/contacts.dto';
|
||||
import {
|
||||
ACK_UNKNOWN,
|
||||
SECOND,
|
||||
WAHAEngine,
|
||||
WAHAEvents,
|
||||
WAHAPresenceStatus,
|
||||
WAHASessionStatus,
|
||||
WAMessageAck,
|
||||
} from '../../../structures/enums.dto';
|
||||
} from '@waha/structures/enums.dto';
|
||||
import {
|
||||
CreateGroupRequest,
|
||||
ParticipantsRequest,
|
||||
SettingsSecurityChangeInfo,
|
||||
} from '../../../structures/groups.dto';
|
||||
} from '@waha/structures/groups.dto';
|
||||
import { WAMessage, WAMessageReaction } from '@waha/structures/responses.dto';
|
||||
import { MeInfo } from '@waha/structures/sessions.dto';
|
||||
import { WAMessageRevokedBody } from '@waha/structures/webhooks.dto';
|
||||
import {
|
||||
WAMessage,
|
||||
WAMessageReaction,
|
||||
} from '../../../structures/responses.dto';
|
||||
import { MeInfo } from '../../../structures/sessions.dto';
|
||||
import { WAMessageRevokedBody } from '../../../structures/webhooks.dto';
|
||||
import { IEngineMediaProcessor } from '../../abc/media.abc';
|
||||
import { WAHAInternalEvent, WhatsappSession } from '../../abc/session.abc';
|
||||
import {
|
||||
AvailableInPlusVersion,
|
||||
NotImplementedByEngineError,
|
||||
} from '../../exceptions';
|
||||
import { QR } from '../../QR';
|
||||
Channel as WEBJSChannel,
|
||||
Chat,
|
||||
ClientOptions,
|
||||
Contact,
|
||||
Events,
|
||||
GroupChat,
|
||||
Location,
|
||||
Message,
|
||||
Reaction,
|
||||
} from 'whatsapp-web.js';
|
||||
import { Message as MessageInstance } from 'whatsapp-web.js/src/structures';
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
const QRCode = require('qrcode');
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
const qrcode = require('qrcode-terminal');
|
||||
|
||||
export interface WebJSConfig {
|
||||
webVersion?: string;
|
||||
}
|
||||
|
||||
export class WhatsappSessionWebJSCore extends WhatsappSession {
|
||||
private START_ATTEMPT_DELAY_SECONDS = 2;
|
||||
|
||||
engine = WAHAEngine.WEBJS;
|
||||
protected engineConfig?: WebJSConfig;
|
||||
|
||||
whatsapp: Client;
|
||||
private startTimeoutId: null | ReturnType<typeof setTimeout> = null;
|
||||
private shouldRestart: boolean;
|
||||
|
||||
whatsapp: WebjsClient;
|
||||
protected qr: QR;
|
||||
|
||||
public constructor(config) {
|
||||
super(config);
|
||||
this.qr = new QR();
|
||||
this.shouldRestart = true;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -87,19 +100,21 @@ export class WhatsappSessionWebJSCore extends WhatsappSession {
|
||||
|
||||
protected getClientOptions(): ClientOptions {
|
||||
const path = this.getClassDirName();
|
||||
const webVersion = this.engineConfig?.webVersion || '2.2412.54';
|
||||
this.log.debug(`Using web version: '${webVersion}'`);
|
||||
const webVersion =
|
||||
this.engineConfig?.webVersion || '2.3000.1014522270-alpha';
|
||||
// this.log.debug(`Using web version: '${webVersion}'`);
|
||||
return {
|
||||
puppeteer: {
|
||||
headless: true,
|
||||
executablePath: this.getBrowserExecutablePath(),
|
||||
args: this.getBrowserArgsForPuppeteer(),
|
||||
dumpio: this.isDebugEnabled(),
|
||||
},
|
||||
webVersion: webVersion,
|
||||
webVersionCache: {
|
||||
type: 'local',
|
||||
path: path,
|
||||
strict: true,
|
||||
type: 'none',
|
||||
// path: path,
|
||||
// strict: true,
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -107,7 +122,30 @@ export class WhatsappSessionWebJSCore extends WhatsappSession {
|
||||
protected async buildClient() {
|
||||
const clientOptions = this.getClientOptions();
|
||||
this.addProxyConfig(clientOptions);
|
||||
return new Client(clientOptions);
|
||||
return new WebjsClient(clientOptions);
|
||||
}
|
||||
|
||||
private restartClient() {
|
||||
if (!this.shouldRestart) {
|
||||
this.logger.debug(
|
||||
"Shouldn't restart the client, ignoring restart request",
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.startTimeoutId) {
|
||||
const msg =
|
||||
'Request to restart is already in progress, ignoring restart request';
|
||||
this.logger.warn(msg);
|
||||
return;
|
||||
}
|
||||
this.logger.info(
|
||||
`Setting up client start in ${this.START_ATTEMPT_DELAY_SECONDS} seconds...`,
|
||||
);
|
||||
this.startTimeoutId = setTimeout(() => {
|
||||
this.startTimeoutId = undefined;
|
||||
this.start();
|
||||
}, this.START_ATTEMPT_DELAY_SECONDS * SECOND);
|
||||
}
|
||||
|
||||
protected addProxyConfig(clientOptions: ClientOptions) {
|
||||
@@ -127,8 +165,9 @@ export class WhatsappSessionWebJSCore extends WhatsappSession {
|
||||
}
|
||||
}
|
||||
|
||||
async start() {
|
||||
this.status = WAHASessionStatus.STARTING;
|
||||
protected async init() {
|
||||
await this.end();
|
||||
this.shouldRestart = true;
|
||||
this.whatsapp = await this.buildClient();
|
||||
this.whatsapp
|
||||
.initialize()
|
||||
@@ -136,17 +175,21 @@ export class WhatsappSessionWebJSCore extends WhatsappSession {
|
||||
// Listen for browser disconnected event
|
||||
this.whatsapp.pupBrowser.on('disconnected', () => {
|
||||
this.status = WAHASessionStatus.FAILED;
|
||||
this.log.error('The browser has been disconnected');
|
||||
this.logger.error('The browser has been disconnected');
|
||||
this.restartClient();
|
||||
});
|
||||
// Listen for page close event
|
||||
this.whatsapp.pupPage.on('close', () => {
|
||||
this.status = WAHASessionStatus.FAILED;
|
||||
this.log.error('The WhatsApp Web page has been closed');
|
||||
this.logger.error('The WhatsApp Web page has been closed');
|
||||
this.restartClient();
|
||||
});
|
||||
|
||||
// Listen for page error event
|
||||
if (this.isDebugEnabled()) {
|
||||
this.log.debug("Logging 'console' event for web page");
|
||||
this.logger.debug("Logging 'console' event for web page");
|
||||
this.whatsapp.pupPage.on('console', (msg) =>
|
||||
this.log.debug(`WEBJS page log: ${msg.text()}`),
|
||||
this.logger.debug(`WEBJS page log: ${msg.text()}`),
|
||||
);
|
||||
this.whatsapp.pupPage.evaluate(() =>
|
||||
console.log(`url is ${location.href}`),
|
||||
@@ -155,7 +198,8 @@ export class WhatsappSessionWebJSCore extends WhatsappSession {
|
||||
})
|
||||
.catch((error) => {
|
||||
this.status = WAHASessionStatus.FAILED;
|
||||
this.log.error(error);
|
||||
this.logger.error(error);
|
||||
this.restartClient();
|
||||
return;
|
||||
});
|
||||
if (this.isDebugEnabled()) {
|
||||
@@ -163,25 +207,43 @@ export class WhatsappSessionWebJSCore extends WhatsappSession {
|
||||
}
|
||||
this.listenConnectionEvents();
|
||||
this.events.emit(WAHAInternalEvent.ENGINE_START);
|
||||
}
|
||||
|
||||
async start() {
|
||||
this.status = WAHASessionStatus.STARTING;
|
||||
await this.init();
|
||||
return this;
|
||||
}
|
||||
|
||||
async stop() {
|
||||
await this.whatsapp.destroy();
|
||||
this.shouldRestart = false;
|
||||
this.status = WAHASessionStatus.STOPPED;
|
||||
this.events.removeAllListeners();
|
||||
await this.end();
|
||||
}
|
||||
|
||||
async getSessionMeInfo(): Promise<MeInfo | null> {
|
||||
private async end() {
|
||||
try {
|
||||
this.whatsapp?.removeAllListeners();
|
||||
clearInterval(this.startTimeoutId);
|
||||
this.whatsapp?.destroy().catch((error) => {
|
||||
this.logger.debug('Failed to destroy the client', error);
|
||||
});
|
||||
} catch (error) {
|
||||
this.logger.error(error);
|
||||
}
|
||||
}
|
||||
|
||||
getSessionMeInfo(): MeInfo | null {
|
||||
const clientInfo = this.whatsapp?.info;
|
||||
if (!clientInfo) {
|
||||
return null;
|
||||
}
|
||||
const wid = clientInfo.wid;
|
||||
const meInfo: MeInfo = {
|
||||
id: wid._serialized,
|
||||
pushName: clientInfo.pushname,
|
||||
return {
|
||||
id: wid?._serialized,
|
||||
pushName: clientInfo?.pushname,
|
||||
};
|
||||
return meInfo;
|
||||
}
|
||||
|
||||
protected listenEngineEventsInDebugMode() {
|
||||
@@ -191,14 +253,14 @@ export class WhatsappSessionWebJSCore extends WhatsappSession {
|
||||
const event = Events[key];
|
||||
this.whatsapp.on(event, (...data: any[]) => {
|
||||
const log = { event: event, data: data };
|
||||
this.log.debug(`WEBJS event: ${JSON.stringify(log)}`);
|
||||
this.logger.debug({ event: log }, `WEBJS event`);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
protected listenConnectionEvents() {
|
||||
this.whatsapp.on(Events.QR_RECEIVED, async (qr) => {
|
||||
this.log.debug('QR received');
|
||||
this.logger.debug('QR received');
|
||||
// Convert to image and save
|
||||
const url = await QRCode.toDataURL(qr);
|
||||
this.qr.save(url, qr);
|
||||
@@ -209,13 +271,25 @@ export class WhatsappSessionWebJSCore extends WhatsappSession {
|
||||
this.whatsapp.on(Events.READY, () => {
|
||||
this.status = WAHASessionStatus.WORKING;
|
||||
this.qr.save('');
|
||||
this.log.log(`Session '${this.name}' has been authenticated!`);
|
||||
this.logger.info(`Session '${this.name}' is ready!`);
|
||||
});
|
||||
|
||||
this.whatsapp.on(Events.AUTHENTICATED, () => {
|
||||
this.status = WAHASessionStatus.WORKING;
|
||||
this.qr.save('');
|
||||
this.logger.info(`Session '${this.name}' has been authenticated!`);
|
||||
});
|
||||
|
||||
this.whatsapp.on(Events.AUTHENTICATION_FAILURE, () => {
|
||||
this.status = WAHASessionStatus.FAILED;
|
||||
this.qr.save('');
|
||||
this.logger.info(`Session '${this.name}' has been disconnected!`);
|
||||
});
|
||||
|
||||
this.whatsapp.on(Events.DISCONNECTED, () => {
|
||||
this.status = WAHASessionStatus.FAILED;
|
||||
this.qr.save('');
|
||||
this.log.log(`Session '${this.name}' has been disconnected!`);
|
||||
this.logger.info(`Session '${this.name}' has been disconnected!`);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -363,11 +437,12 @@ export class WhatsappSessionWebJSCore extends WhatsappSession {
|
||||
/**
|
||||
* Chats methods
|
||||
*/
|
||||
getChats() {
|
||||
return this.whatsapp.getChats();
|
||||
getChats(query: GetChatsQuery) {
|
||||
return this.whatsapp.getChats(query.limit, query.offset);
|
||||
}
|
||||
|
||||
async getChatMessages(chatId: string, limit: number, downloadMedia: boolean) {
|
||||
downloadMedia = parseBool(downloadMedia);
|
||||
const chat: Chat = await this.whatsapp.getChatById(
|
||||
this.ensureSuffix(chatId),
|
||||
);
|
||||
@@ -553,6 +628,109 @@ export class WhatsappSessionWebJSCore extends WhatsappSession {
|
||||
return groupChat.demoteParticipants(participantIds);
|
||||
}
|
||||
|
||||
/**
|
||||
* Channels methods
|
||||
*/
|
||||
protected ChatToChannel(chat: WEBJSChannel): Channel {
|
||||
// @ts-ignore
|
||||
const metadata = chat.channelMetadata;
|
||||
let role = metadata.membershipType.toUpperCase();
|
||||
if (role === 'VIEWER') {
|
||||
role = ChannelRole.GUEST;
|
||||
}
|
||||
return {
|
||||
id: chat.id._serialized,
|
||||
name: chat.name,
|
||||
description: chat.description,
|
||||
invite: getChannelInviteLink(metadata.inviteCode),
|
||||
preview: null,
|
||||
picture: null,
|
||||
verified: metadata.verified,
|
||||
role: role,
|
||||
};
|
||||
}
|
||||
|
||||
protected ChannelMetadataToChannel(metadata: any): Channel {
|
||||
let role = metadata.membershipType.toUpperCase();
|
||||
if (role === 'VIEWER') {
|
||||
role = ChannelRole.GUEST;
|
||||
}
|
||||
return {
|
||||
id: metadata.id,
|
||||
name: metadata.titleMetadata.title,
|
||||
description: metadata.descriptionMetadata.description,
|
||||
invite: metadata.inviteLink,
|
||||
preview: metadata.pictureUrl,
|
||||
picture: metadata.pictureUrl,
|
||||
verified: metadata.isVerified,
|
||||
role: role,
|
||||
};
|
||||
}
|
||||
|
||||
public async channelsList(query: ListChannelsQuery): Promise<Channel[]> {
|
||||
const data = await this.whatsapp.getChannels();
|
||||
let channels = data.map(this.ChatToChannel);
|
||||
if (query.role) {
|
||||
// @ts-ignore
|
||||
channels = channels.filter((channel) => channel.role === query.role);
|
||||
}
|
||||
|
||||
const promises = channels.map(async (channel) =>
|
||||
this.whatsapp.getProfilePicUrl(channel.id),
|
||||
);
|
||||
const pictures = await Promise.all(promises);
|
||||
channels = channels.map((channel, index) => {
|
||||
channel.picture = pictures[index] || null;
|
||||
channel.preview = channel.picture;
|
||||
return channel;
|
||||
});
|
||||
return channels;
|
||||
}
|
||||
|
||||
public channelsCreateChannel(
|
||||
request: CreateChannelRequest,
|
||||
): Promise<Channel> {
|
||||
throw new NotImplementedByEngineError();
|
||||
}
|
||||
|
||||
public async channelsGetChannel(id: string): Promise<Channel> {
|
||||
return await this.channelsGetChannelByInviteCode(id);
|
||||
}
|
||||
|
||||
public async channelsGetChannelByInviteCode(
|
||||
inviteCode: string,
|
||||
): Promise<Channel> {
|
||||
const metadata = await this.whatsapp.getChannelByInviteCode(inviteCode);
|
||||
const channel = this.ChannelMetadataToChannel(metadata);
|
||||
channel.preview =
|
||||
(await this.whatsapp.getProfilePicUrl(channel.id)) || null;
|
||||
channel.picture = channel.preview;
|
||||
return channel;
|
||||
}
|
||||
|
||||
public channelsDeleteChannel(id: string): Promise<void> {
|
||||
throw new NotImplementedByEngineError();
|
||||
}
|
||||
|
||||
public channelsFollowChannel(id: string): Promise<void> {
|
||||
throw new NotImplementedByEngineError();
|
||||
}
|
||||
|
||||
public channelsUnfollowChannel(id: string): Promise<void> {
|
||||
throw new NotImplementedByEngineError();
|
||||
}
|
||||
|
||||
public channelsMuteChannel(id: string): Promise<void> {
|
||||
throw new NotImplementedByEngineError();
|
||||
}
|
||||
|
||||
public channelsUnmuteChannel(id: string): Promise<void> {
|
||||
throw new NotImplementedByEngineError();
|
||||
}
|
||||
|
||||
/**
|
||||
* Presences methods
|
||||
*/
|
||||
public async setPresence(presence: WAHAPresenceStatus, chatId?: string) {
|
||||
let chat: Chat;
|
||||
switch (presence) {
|
||||
@@ -643,8 +821,8 @@ export class WhatsappSessionWebJSCore extends WhatsappSession {
|
||||
try {
|
||||
message = await this.downloadMedia(message);
|
||||
} catch (e) {
|
||||
this.log.error('Failed when tried to download media for a message');
|
||||
this.log.error(e, e.stack);
|
||||
this.logger.error('Failed when tried to download media for a message');
|
||||
this.logger.error(e, e.stack);
|
||||
}
|
||||
}
|
||||
return await this.toWAMessage(message);
|
||||
@@ -691,6 +869,7 @@ export class WhatsappSessionWebJSCore extends WhatsappSession {
|
||||
}
|
||||
|
||||
public async getEngineInfo() {
|
||||
// Add 1 seconds timeout
|
||||
return {
|
||||
WWebVersion: await this.whatsapp?.getWWebVersion(),
|
||||
state: await this.whatsapp?.getState(),
|
||||
|
||||
+41
-22
@@ -5,10 +5,17 @@ import {
|
||||
NotFoundException,
|
||||
UnprocessableEntityException,
|
||||
} from '@nestjs/common';
|
||||
import { getPinoLogLevel, LoggerBuilder } from '@waha/utils/logging';
|
||||
import { promiseTimeout } from '@waha/utils/promiseTimeout';
|
||||
import { EventEmitter } from 'events';
|
||||
import { PinoLogger } from 'nestjs-pino';
|
||||
|
||||
import { WhatsappConfigService } from '../config.service';
|
||||
import { getLogLevels } from '../helpers';
|
||||
import { WAHAEngine, WAHASessionStatus } from '../structures/enums.dto';
|
||||
import {
|
||||
WAHAEngine,
|
||||
WAHAEvents,
|
||||
WAHASessionStatus,
|
||||
} from '../structures/enums.dto';
|
||||
import {
|
||||
ProxyConfig,
|
||||
SessionDTO,
|
||||
@@ -40,10 +47,6 @@ export class OnlyDefaultSessionIsAllowed extends UnprocessableEntityException {
|
||||
}
|
||||
}
|
||||
|
||||
export function buildLogger(name: string, levels: LogLevel[]) {
|
||||
return new ConsoleLogger(name, { logLevels: levels });
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class SessionManagerCore extends SessionManager {
|
||||
private session?: WhatsappSession;
|
||||
@@ -55,14 +58,14 @@ export class SessionManagerCore extends SessionManager {
|
||||
|
||||
constructor(
|
||||
private config: WhatsappConfigService,
|
||||
private log: ConsoleLogger,
|
||||
private engineConfigService: EngineConfigService,
|
||||
private log: PinoLogger,
|
||||
) {
|
||||
super();
|
||||
|
||||
this.log.setContext('SessionManager');
|
||||
this.events = new EventEmitter();
|
||||
this.log.setContext(SessionManagerCore.name);
|
||||
this.session = undefined;
|
||||
const engineName = this.config.getDefaultEngineName();
|
||||
const engineName = this.engineConfigService.getDefaultEngineName();
|
||||
this.EngineClass = this.getEngine(engineName);
|
||||
this.store = new LocalStoreCore(engineName.toLowerCase());
|
||||
this.sessionAuthRepository = new LocalSessionAuthRepository(this.store);
|
||||
@@ -95,7 +98,7 @@ export class SessionManagerCore extends SessionManager {
|
||||
}
|
||||
}
|
||||
|
||||
async onApplicationShutdown(signal?: string) {
|
||||
async beforeApplicationShutdown(signal?: string) {
|
||||
if (!this.session) {
|
||||
return;
|
||||
}
|
||||
@@ -114,20 +117,21 @@ export class SessionManagerCore extends SessionManager {
|
||||
}
|
||||
|
||||
const name = request.name;
|
||||
this.log.log(`'${name}' - starting session...`);
|
||||
const levels = getLogLevels(request.config?.debug);
|
||||
const log = buildLogger(`WhatsappSession - ${name}`, levels);
|
||||
this.log.info(`'${name}' - starting session...`);
|
||||
const mediaManager = new CoreMediaManager(
|
||||
new MediaStorageCore(),
|
||||
this.config.mimetypes,
|
||||
);
|
||||
const webhookLog = buildLogger(`Webhook - ${name}`, levels);
|
||||
const webhook = new this.WebhookConductorClass(webhookLog);
|
||||
const logger = this.log.logger.child({ session: name });
|
||||
logger.level = getPinoLogLevel(request.config?.debug);
|
||||
const loggerBuilder: LoggerBuilder = logger;
|
||||
|
||||
const webhook = new this.WebhookConductorClass(loggerBuilder);
|
||||
const proxyConfig = this.getProxyConfig(request);
|
||||
const sessionConfig: SessionParams = {
|
||||
name,
|
||||
mediaManager,
|
||||
log,
|
||||
loggerBuilder,
|
||||
printQR: this.engineConfigService.shouldPrintQR,
|
||||
sessionStore: this.store,
|
||||
proxyConfig: proxyConfig,
|
||||
@@ -142,6 +146,12 @@ export class SessionManagerCore extends SessionManager {
|
||||
const webhooks = this.getWebhooks(request);
|
||||
webhook.configure(session, webhooks);
|
||||
|
||||
// configure events
|
||||
session.events.on(
|
||||
WAHAEvents.SESSION_STATUS,
|
||||
this.handleSessionEvent(WAHAEvents.SESSION_STATUS, session),
|
||||
);
|
||||
|
||||
// start session
|
||||
await session.start();
|
||||
return {
|
||||
@@ -186,10 +196,10 @@ export class SessionManagerCore extends SessionManager {
|
||||
this.onlyDefault(request.name);
|
||||
|
||||
const name = request.name;
|
||||
this.log.log(`Stopping ${name} session...`);
|
||||
this.log.info(`Stopping ${name} session...`);
|
||||
const session = this.getSession(name);
|
||||
await session.stop();
|
||||
this.log.log(`"${name}" has been stopped.`);
|
||||
this.log.info(`"${name}" has been stopped.`);
|
||||
this.session = undefined;
|
||||
}
|
||||
|
||||
@@ -198,7 +208,7 @@ export class SessionManagerCore extends SessionManager {
|
||||
this.onlyDefault(request.name);
|
||||
this.stop({ name: name, logout: false })
|
||||
.then(() => {
|
||||
this.log.log(`Session '${name}' has been stopped.`);
|
||||
this.log.info(`Session '${name}' has been stopped.`);
|
||||
})
|
||||
.catch((err) => {
|
||||
this.log.error(
|
||||
@@ -234,10 +244,19 @@ export class SessionManagerCore extends SessionManager {
|
||||
},
|
||||
];
|
||||
}
|
||||
const me = await this.session.getSessionMeInfo().catch((err) => null);
|
||||
const me = this.session.getSessionMeInfo();
|
||||
// Get engine info
|
||||
let engineInfo = {};
|
||||
if (this.session) {
|
||||
try {
|
||||
engineInfo = await promiseTimeout(10, this.session.getEngineInfo());
|
||||
} catch (e) {
|
||||
this.log.warn('Error while getting engine info', e);
|
||||
}
|
||||
}
|
||||
const engine = {
|
||||
engine: this.session?.engine,
|
||||
...(await this.session?.getEngineInfo().catch((err) => ({}))),
|
||||
...engineInfo,
|
||||
};
|
||||
return [
|
||||
{
|
||||
|
||||
@@ -52,8 +52,7 @@ export class LocalSessionConfigRepository extends ISessionConfigRepository {
|
||||
}
|
||||
|
||||
private getFilePath(sessionName): string {
|
||||
const folder = this.store.getSessionDirectory(sessionName);
|
||||
return path.join(folder, this.FILENAME);
|
||||
return this.store.getFilePath(sessionName, this.FILENAME);
|
||||
}
|
||||
|
||||
async delete(sessionName: string): Promise<void> {
|
||||
|
||||
@@ -17,4 +17,9 @@ export abstract class LocalStore extends DataStore {
|
||||
* Get the directory where the session data is stored
|
||||
*/
|
||||
abstract getSessionDirectory(name: string): string;
|
||||
|
||||
/**
|
||||
* Get the file path for a session
|
||||
*/
|
||||
abstract getFilePath(session: string, file: string): string;
|
||||
}
|
||||
@@ -45,6 +45,10 @@ export class LocalStoreCore extends LocalStore {
|
||||
return this.getDirectoryPath(name);
|
||||
}
|
||||
|
||||
getFilePath(session: string, file: string): string {
|
||||
return path.join(this.getSessionDirectory(session), file);
|
||||
}
|
||||
|
||||
protected getDirectoryPath(name: string): string {
|
||||
const suffix = crypto.createHash('md5').update(name).digest('hex');
|
||||
return path.join(this.getEngineDirectory(), `${name}-${suffix}`);
|
||||
|
||||
+20
-14
@@ -7,30 +7,36 @@ import { VERSION } from '../version';
|
||||
import { WAHAInternalEvent, WhatsappSession } from './abc/session.abc';
|
||||
import { WebhookConductor, WebhookSender } from './abc/webhooks.abc';
|
||||
import request = require('request');
|
||||
import { LoggerBuilder } from '@waha/utils/logging';
|
||||
import { Logger } from 'pino';
|
||||
|
||||
export class WebhookSenderCore extends WebhookSender {
|
||||
send(json) {
|
||||
this.log.log(`Sending POST to ${this.url}...`);
|
||||
this.log.verbose(`POST DATA: ${JSON.stringify(json)}`);
|
||||
this.logger.info({ url: this.url }, `Sending POST ...`);
|
||||
this.logger.debug({ data: json }, `POST DATA`);
|
||||
|
||||
request.post(this.url, { json: json }, (error, res, body) => {
|
||||
if (error) {
|
||||
this.log.error(error);
|
||||
this.logger.error(error);
|
||||
return;
|
||||
}
|
||||
this.log.log(`POST request was sent with status code: ${res.statusCode}`);
|
||||
this.log.debug(`Response: ${body}`);
|
||||
this.logger.info(
|
||||
`POST request was sent with status code: ${res.statusCode}`,
|
||||
);
|
||||
this.logger.debug({ body: body }, `Response`);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export class WebhookConductorCore implements WebhookConductor {
|
||||
constructor(protected log: ConsoleLogger) {
|
||||
this.log = log;
|
||||
private logger: Logger;
|
||||
|
||||
constructor(protected loggerBuilder: LoggerBuilder) {
|
||||
this.logger = loggerBuilder.child({ name: WebhookConductor.name });
|
||||
}
|
||||
|
||||
protected buildSender(webhookConfig: WebhookConfig): WebhookSender {
|
||||
return new WebhookSenderCore(this.log, webhookConfig);
|
||||
return new WebhookSenderCore(this.loggerBuilder, webhookConfig);
|
||||
}
|
||||
|
||||
private getSuitableEvents(events: WAHAEvents[] | string[]): WAHAEvents[] {
|
||||
@@ -47,7 +53,7 @@ export class WebhookConductorCore implements WebhookConductor {
|
||||
for (const event of events) {
|
||||
// @ts-ignore
|
||||
if (!allEvents.includes(event)) {
|
||||
this.log.error(`Unknown event for webhook: '${event}'`);
|
||||
this.logger.error(`Unknown event for webhook: '${event}'`);
|
||||
continue;
|
||||
}
|
||||
rightEvents.push(event);
|
||||
@@ -70,7 +76,7 @@ export class WebhookConductorCore implements WebhookConductor {
|
||||
}
|
||||
|
||||
const url = webhook.url;
|
||||
this.log.log(`Configuring webhooks for ${url}...`);
|
||||
this.logger.info(`Configuring webhooks for ${url}...`);
|
||||
const events = this.getSuitableEvents(webhook.events);
|
||||
const sender = this.buildSender(webhook);
|
||||
for (const event of events) {
|
||||
@@ -93,14 +99,14 @@ export class WebhookConductorCore implements WebhookConductor {
|
||||
url,
|
||||
);
|
||||
if (!found) {
|
||||
this.log.error(
|
||||
this.logger.error(
|
||||
`Engine does not support webhook event: '${event}' for url '${url}'`,
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
this.log.log(`Webhooks were configured for ${url}.`);
|
||||
this.logger.info(`Webhooks were configured for ${url}.`);
|
||||
}
|
||||
|
||||
private configureSingleEvent(
|
||||
@@ -117,7 +123,7 @@ export class WebhookConductorCore implements WebhookConductor {
|
||||
if (!found) {
|
||||
return false;
|
||||
}
|
||||
this.log.log(`Event '${event}' is enabled for url: ${url}`);
|
||||
this.logger.info(`Event '${event}' is enabled for url: ${url}`);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -127,7 +133,7 @@ export class WebhookConductorCore implements WebhookConductor {
|
||||
data: any,
|
||||
sender: WebhookSender,
|
||||
) {
|
||||
const me = await session.getSessionMeInfo().catch((err) => null);
|
||||
const me = session.getSessionMeInfo();
|
||||
const json: WAHAWebhook = {
|
||||
event: event,
|
||||
session: session.name,
|
||||
|
||||
Whitespace-only changes.
Binary file not shown.
|
Before Width: | Height: | Size: 15 KiB |
@@ -1,289 +0,0 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Dashboard | WAHA</title>
|
||||
<link rel="icon" href="./favicon.ico" />
|
||||
<style>
|
||||
:root {
|
||||
--surface-card: #1f2937;
|
||||
--surface-border: #374151;
|
||||
--card-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25);
|
||||
}
|
||||
|
||||
body,
|
||||
html {
|
||||
height: 100%;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.bg-image {
|
||||
/* The image used */
|
||||
background-image: url('./waha-dashboard.png');
|
||||
|
||||
/* Add the blur effect */
|
||||
filter: blur(2px);
|
||||
-webkit-filter: blur(2px);
|
||||
|
||||
/* Full height */
|
||||
height: 100%;
|
||||
|
||||
/* Center and scale the image nicely */
|
||||
background-position: center;
|
||||
background-repeat: no-repeat;
|
||||
background-size: cover;
|
||||
}
|
||||
|
||||
body {
|
||||
background-color: #111827;
|
||||
color: rgba(255, 255, 255, 0.87);
|
||||
}
|
||||
|
||||
svg {
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
}
|
||||
|
||||
.message {
|
||||
font-size: 20px;
|
||||
margin: 20px;
|
||||
}
|
||||
|
||||
a {
|
||||
color: rgb(158, 158, 255);
|
||||
text-decoration-color: rgb(158, 158, 255);
|
||||
}
|
||||
|
||||
.upgrade-link {
|
||||
text-decoration: none;
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
.link-button {
|
||||
background-color: #4ade80;
|
||||
padding: 10px 20px;
|
||||
border-radius: 5px;
|
||||
color: #052e16;
|
||||
text-decoration: none;
|
||||
margin-top: 50px;
|
||||
}
|
||||
|
||||
.card {
|
||||
min-width: 25rem;
|
||||
background: var(--surface-card);
|
||||
border: 1px solid var(--surface-border);
|
||||
box-shadow: var(--card-shadow);
|
||||
padding: 2rem;
|
||||
border-radius: 12px;
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
z-index: 2;
|
||||
text-align: center;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="bg-image"></div>
|
||||
<div class="card">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||
version="1.1"
|
||||
width="1080"
|
||||
height="1080"
|
||||
viewBox="0 0 1080 1080"
|
||||
xml:space="preserve"
|
||||
>
|
||||
<desc>Created with Fabric.js 5.2.4</desc>
|
||||
<defs></defs>
|
||||
<g
|
||||
transform="matrix(1 0 0 1 540 540)"
|
||||
id="c3cb5d1b-8350-492a-8567-8c1ee2307b9a"
|
||||
></g>
|
||||
<g
|
||||
transform="matrix(1 0 0 1 540 540)"
|
||||
id="054ae9ae-db12-4435-8648-8bfb02267690"
|
||||
>
|
||||
<rect
|
||||
style="
|
||||
stroke: none;
|
||||
stroke-width: 1;
|
||||
stroke-dasharray: none;
|
||||
stroke-linecap: butt;
|
||||
stroke-dashoffset: 0;
|
||||
stroke-linejoin: miter;
|
||||
stroke-miterlimit: 4;
|
||||
fill: rgb(255, 255, 255);
|
||||
fill-rule: nonzero;
|
||||
opacity: 1;
|
||||
visibility: hidden;
|
||||
"
|
||||
vector-effect="non-scaling-stroke"
|
||||
x="-540"
|
||||
y="-540"
|
||||
rx="0"
|
||||
ry="0"
|
||||
width="1080"
|
||||
height="1080"
|
||||
/>
|
||||
</g>
|
||||
<g transform="matrix(1 0 0 1 540 540)">
|
||||
<rect
|
||||
style="
|
||||
stroke: none;
|
||||
stroke-width: 1;
|
||||
stroke-dasharray: none;
|
||||
stroke-linecap: butt;
|
||||
stroke-dashoffset: 0;
|
||||
stroke-linejoin: miter;
|
||||
stroke-miterlimit: 4;
|
||||
fill: rgb(255, 255, 255);
|
||||
fill-rule: nonzero;
|
||||
opacity: 1;
|
||||
visibility: hidden;
|
||||
"
|
||||
vector-effect="non-scaling-stroke"
|
||||
x="-540"
|
||||
y="-540"
|
||||
rx="0"
|
||||
ry="0"
|
||||
width="1080"
|
||||
height="1080"
|
||||
/>
|
||||
</g>
|
||||
<g transform="matrix(2.02 0 0 2.02 540 540)">
|
||||
<rect
|
||||
style="
|
||||
stroke: none;
|
||||
stroke-width: 1;
|
||||
stroke-dasharray: none;
|
||||
stroke-linecap: butt;
|
||||
stroke-dashoffset: 0;
|
||||
stroke-linejoin: miter;
|
||||
stroke-miterlimit: 4;
|
||||
fill: rgb(255, 255, 255);
|
||||
fill-rule: nonzero;
|
||||
opacity: 1;
|
||||
visibility: hidden;
|
||||
"
|
||||
vector-effect="non-scaling-stroke"
|
||||
x="-540"
|
||||
y="-540"
|
||||
rx="0"
|
||||
ry="0"
|
||||
width="1080"
|
||||
height="1080"
|
||||
/>
|
||||
</g>
|
||||
<g transform="matrix(3.68 0 0 3.68 540.38 540.48)">
|
||||
<path
|
||||
style="
|
||||
stroke: none;
|
||||
stroke-width: 1;
|
||||
stroke-dasharray: none;
|
||||
stroke-linecap: butt;
|
||||
stroke-dashoffset: 0;
|
||||
stroke-linejoin: miter;
|
||||
stroke-miterlimit: 4;
|
||||
fill: rgb(74, 173, 32);
|
||||
fill-rule: nonzero;
|
||||
opacity: 1;
|
||||
"
|
||||
vector-effect="non-scaling-stroke"
|
||||
transform=" translate(-392.21, -570.16)"
|
||||
d="M 392.26 705.96 C 376.77 705.96 361.28999999999996 706.0400000000001 345.8 705.95 C 318.16 705.7900000000001 297.6 685.2700000000001 297.22 657.57 C 297.15000000000003 652.24 297.07000000000005 646.9100000000001 297.25 641.59 C 297.36 638.23 296.3 636.6 292.72 636.89 C 290.24 637.09 287.73 636.98 285.23 636.9 C 277.1 636.63 271.70000000000005 631.65 271.38 623.49 C 271.06 615.1800000000001 271.06 606.83 271.37 598.52 C 271.69 590.05 277.04 585.23 285.58 585.01 C 297.21 584.72 297.19 584.72 297.21999999999997 573.01 C 297.22999999999996 567.36 297.14 561.72 297.97999999999996 556.0699999999999 C 301.25999999999993 533.93 320.72999999999996 516.4599999999999 343.09 516.04 C 355.07 515.81 367.07 515.86 379.04999999999995 516.02 C 382.72999999999996 516.0699999999999 384.02 514.87 383.96 511.15 C 383.77 500.65999999999997 383.76 490.16999999999996 383.96999999999997 479.67999999999995 C 384.04999999999995 476.00999999999993 382.96 473.65999999999997 379.92999999999995 471.37999999999994 C 372.23999999999995 465.5799999999999 369.78 456.25999999999993 373.06999999999994 447.7199999999999 C 376.18999999999994 439.5999999999999 385.13999999999993 433.7399999999999 393.4599999999999 434.3599999999999 C 402.63999999999993 435.0399999999999 410.43999999999994 441.3499999999999 412.4599999999999 450.2199999999999 C 414.4799999999999 459.0299999999999 411.7099999999999 466.4299999999999 404.4699999999999 471.7899999999999 C 401.8699999999999 473.7199999999999 400.9199999999999 475.69999999999993 400.9699999999999 478.8299999999999 C 401.1199999999999 489.1499999999999 401.2499999999999 499.4799999999999 400.9299999999999 509.79999999999995 C 400.7899999999999 514.5 402.0299999999999 516.24 406.9999999999999 516.0799999999999 C 417.8099999999999 515.7299999999999 428.6399999999999 515.91 439.4699999999999 515.9899999999999 C 466.5199999999999 516.2099999999999 486.7199999999999 536.2299999999999 487.2399999999999 563.3 C 487.3399999999999 568.29 487.2599999999999 573.29 487.2799999999999 578.29 C 487.2999999999999 585 487.3099999999999 585 493.7899999999999 585.02 C 495.94999999999993 585.03 498.1199999999999 584.96 500.2799999999999 585.06 C 507.94999999999993 585.43 512.9999999999999 590.42 513.1499999999999 598.1199999999999 C 513.3199999999998 606.6099999999999 513.3099999999998 615.1099999999999 513.1599999999999 623.5999999999999 C 513.0299999999999 631.3599999999999 508.1999999999999 636.31 500.47999999999985 636.8699999999999 C 498.15999999999985 637.0399999999998 495.77999999999986 637.2199999999999 493.49999999999983 636.8899999999999 C 488.1999999999998 636.1299999999999 486.86999999999983 638.5399999999998 487.1999999999998 643.3999999999999 C 487.54999999999984 648.5399999999998 487.3499999999998 653.7199999999999 487.23999999999984 658.8799999999999 C 486.67999999999984 685.6599999999999 466.4599999999998 705.7399999999999 439.71999999999986 705.9599999999999 C 423.9 706.06 408.08 705.95 392.26 705.96 z"
|
||||
stroke-linecap="round"
|
||||
/>
|
||||
</g>
|
||||
<g transform="matrix(-3.68 0 0 -3.68 546.38 845.01)">
|
||||
<path
|
||||
style="
|
||||
stroke: none;
|
||||
stroke-width: 1;
|
||||
stroke-dasharray: none;
|
||||
stroke-linecap: butt;
|
||||
stroke-dashoffset: 0;
|
||||
stroke-linejoin: miter;
|
||||
stroke-miterlimit: 4;
|
||||
fill: rgb(56, 134, 22);
|
||||
fill-rule: nonzero;
|
||||
opacity: 1;
|
||||
"
|
||||
vector-effect="non-scaling-stroke"
|
||||
transform=" translate(-394.07, -654.42)"
|
||||
d="M 394.26 632.93 C 404.75 632.93 415.23 632.9 425.71999999999997 632.9399999999999 C 431.92999999999995 632.9599999999999 433.52 634.7399999999999 432.90999999999997 640.7099999999999 C 430.99999999999994 659.66 414.40999999999997 675.2199999999999 395.38 675.91 C 376.87 676.5799999999999 359.57 662.65 356 644.1899999999999 C 355.72 642.7199999999999 355.33 641.26 355.19 639.78 C 354.71 634.9 356.32 633.01 361.3 632.97 C 372.29 632.87 383.28 632.93 394.26 632.93 z"
|
||||
stroke-linecap="round"
|
||||
/>
|
||||
</g>
|
||||
<g transform="matrix(3.68 0 0 3.68 673.99 594.08)">
|
||||
<path
|
||||
style="
|
||||
stroke: none;
|
||||
stroke-width: 1;
|
||||
stroke-dasharray: none;
|
||||
stroke-linecap: butt;
|
||||
stroke-dashoffset: 0;
|
||||
stroke-linejoin: miter;
|
||||
stroke-miterlimit: 4;
|
||||
fill: rgb(56, 135, 22);
|
||||
fill-rule: nonzero;
|
||||
opacity: 1;
|
||||
"
|
||||
vector-effect="non-scaling-stroke"
|
||||
transform=" translate(-428.75, -584.99)"
|
||||
d="M 428.83 602 C 419.12 601.96 411.59999999999997 594.6 411.53 585.07 C 411.46999999999997 575.4100000000001 419.21 567.9100000000001 429.16999999999996 567.98 C 438.33 568.0500000000001 445.93999999999994 575.71 445.97999999999996 584.91 C 446.02 594.33 438.29 602.04 428.83 602 z"
|
||||
stroke-linecap="round"
|
||||
/>
|
||||
</g>
|
||||
<g transform="matrix(3.68 0 0 3.68 406.92 594.18)">
|
||||
<path
|
||||
style="
|
||||
stroke: none;
|
||||
stroke-width: 1;
|
||||
stroke-dasharray: none;
|
||||
stroke-linecap: butt;
|
||||
stroke-dashoffset: 0;
|
||||
stroke-linejoin: miter;
|
||||
stroke-miterlimit: 4;
|
||||
fill: rgb(56, 135, 22);
|
||||
fill-rule: nonzero;
|
||||
opacity: 1;
|
||||
"
|
||||
vector-effect="non-scaling-stroke"
|
||||
transform=" translate(-356.06, -584.99)"
|
||||
d="M 339.05 584.57 C 339.19 574.94 346.67 567.84 356.53000000000003 567.99 C 366.26000000000005 568.14 373.21000000000004 575.46 373.07000000000005 585.42 C 372.94000000000005 595.16 365.62000000000006 602.13 355.68000000000006 602 C 346.04 601.87 338.91 594.4 339.05 584.57 z"
|
||||
stroke-linecap="round"
|
||||
/>
|
||||
</g>
|
||||
</svg>
|
||||
|
||||
<div class="message">
|
||||
Sorry,
|
||||
<a
|
||||
href="https://waha.devlike.pro/docs/how-to/waha-dashboard"
|
||||
target="_blank"
|
||||
>WAHA Dashboard</a
|
||||
>
|
||||
is not available in <strong>WAHA Core</strong> version.
|
||||
<br />
|
||||
Please consider upgrading to <strong>WAHA Plus</strong>.
|
||||
</div>
|
||||
<div style="margin-top: 2rem; margin-bottom: 1rem">
|
||||
<a
|
||||
class="upgrade-link link-button"
|
||||
href="https://waha.devlike.pro/docs/how-to/plus-version/"
|
||||
target="_blank"
|
||||
>Upgrade to WAHA Plus</a
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 74 KiB |
+1
-10
@@ -1,5 +1,3 @@
|
||||
import { LogLevel } from '@nestjs/common';
|
||||
|
||||
function parseBool(value: any): boolean {
|
||||
if (typeof value === 'boolean') {
|
||||
return value;
|
||||
@@ -37,11 +35,4 @@ function splitAt(str: string, index) {
|
||||
return [fst.join(''), snd.join('')];
|
||||
}
|
||||
|
||||
function getLogLevels(debug: boolean): LogLevel[] {
|
||||
const enableDebug = process.env.DEBUG != undefined || debug;
|
||||
return enableDebug
|
||||
? ['log', 'error', 'warn', 'debug', 'verbose']
|
||||
: ['log', 'error', 'warn'];
|
||||
}
|
||||
|
||||
export { flipObject, getLogLevels, parseBool, splitAt };
|
||||
export { flipObject, parseBool, splitAt };
|
||||
+30
-9
@@ -1,26 +1,39 @@
|
||||
import { NestFactory } from '@nestjs/core';
|
||||
import { WsAdapter } from '@nestjs/platform-ws';
|
||||
import {
|
||||
getNestJSLogLevels,
|
||||
getPinoLogLevel,
|
||||
getPinoTransport,
|
||||
} from '@waha/utils/logging';
|
||||
import { json, urlencoded } from 'express';
|
||||
import { Logger as NestJSPinoLogger } from 'nestjs-pino';
|
||||
import { Logger } from 'pino';
|
||||
import pino from 'pino';
|
||||
|
||||
import { AllExceptionsFilter } from './api/exception.filter';
|
||||
import { WhatsappConfigService } from './config.service';
|
||||
import { AppModuleCore } from './core/app.module.core';
|
||||
import { SwaggerConfiguratorCore } from './core/SwaggerConfiguratorCore';
|
||||
import { getLogLevels } from './helpers';
|
||||
import { WAHA_WEBHOOKS } from './structures/webhooks.dto';
|
||||
import { getWAHAVersion, VERSION, WAHAVersion } from './version';
|
||||
|
||||
console.log('NODE - Catching unhandled exceptions enabled');
|
||||
const logger: Logger = pino({
|
||||
level: getPinoLogLevel(),
|
||||
transport: getPinoTransport(),
|
||||
}).child({ name: 'Bootstrap' });
|
||||
|
||||
logger.info('NODE - Catching unhandled exceptions enabled');
|
||||
process.on('unhandledRejection', (reason, promise) => {
|
||||
console.error('Unhandled Rejection at:', promise, 'reason:', reason);
|
||||
logger.error('Unhandled Rejection at:', promise, 'reason:', reason);
|
||||
// @ts-ignore
|
||||
console.error(reason.stack);
|
||||
logger.error(reason.stack);
|
||||
});
|
||||
|
||||
async function loadModules(): Promise<
|
||||
[typeof AppModuleCore, typeof SwaggerConfiguratorCore]
|
||||
> {
|
||||
const version = getWAHAVersion();
|
||||
console.log(`WAHA (WhatsApp HTTP API) - Running ${version} version...`);
|
||||
logger.info(`WAHA (WhatsApp HTTP API) - Running ${version} version...`);
|
||||
|
||||
if (version === WAHAVersion.CORE) {
|
||||
const { AppModuleCore } = await import('./core/app.module.core');
|
||||
@@ -40,26 +53,34 @@ async function loadModules(): Promise<
|
||||
|
||||
async function bootstrap() {
|
||||
const [AppModule, SwaggerModule] = await loadModules();
|
||||
const httpsOptions = AppModule.getHttpsOptions(logger);
|
||||
const app = await NestFactory.create(AppModule, {
|
||||
logger: getLogLevels(false),
|
||||
logger: getNestJSLogLevels(),
|
||||
httpsOptions: httpsOptions,
|
||||
bufferLogs: true,
|
||||
});
|
||||
|
||||
app.useLogger(app.get(NestJSPinoLogger));
|
||||
app.enableShutdownHooks();
|
||||
app.useGlobalFilters(new AllExceptionsFilter());
|
||||
app.enableCors();
|
||||
// Ideally, we should apply it globally.
|
||||
// but for now we added it ValidationPipe on Controller or endpoint level
|
||||
// app.useGlobalPipes(new ValidationPipe({ transform: true }));
|
||||
|
||||
// Allow to send big body - for images and attachments
|
||||
app.use(json({ limit: '50mb' }));
|
||||
app.use(urlencoded({ limit: '50mb', extended: false }));
|
||||
app.useWebSocketAdapter(new WsAdapter(app));
|
||||
|
||||
// Configure swagger
|
||||
const swaggerConfigurator = new SwaggerModule(app);
|
||||
swaggerConfigurator.configure(WAHA_WEBHOOKS);
|
||||
|
||||
AppModule.appReady(app, logger);
|
||||
const config = app.get(WhatsappConfigService);
|
||||
await app.listen(config.port);
|
||||
console.log(`WhatsApp HTTP API is running on: ${await app.getUrl()}`);
|
||||
console.log(`Environment:`, VERSION);
|
||||
logger.info(`WhatsApp HTTP API is running on: ${await app.getUrl()}`);
|
||||
logger.info(VERSION, 'Environment');
|
||||
}
|
||||
|
||||
bootstrap();
|
||||
@@ -0,0 +1,94 @@
|
||||
import { ApiParam, ApiProperty, getSchemaPath } from '@nestjs/swagger';
|
||||
import { BinaryFile, RemoteFile } from '@waha/structures/files.dto';
|
||||
|
||||
export class CreateChannelRequest {
|
||||
@ApiProperty({
|
||||
example: 'Channel Name',
|
||||
})
|
||||
name: string;
|
||||
|
||||
@ApiProperty({
|
||||
example: 'Channel Description',
|
||||
})
|
||||
description?: string;
|
||||
|
||||
@ApiProperty({
|
||||
oneOf: [
|
||||
{ $ref: getSchemaPath(RemoteFile) },
|
||||
{ $ref: getSchemaPath(BinaryFile) },
|
||||
],
|
||||
})
|
||||
picture?: RemoteFile | BinaryFile;
|
||||
}
|
||||
|
||||
export enum ChannelRole {
|
||||
OWNER = 'OWNER',
|
||||
ADMIN = 'ADMIN',
|
||||
SUBSCRIBER = 'SUBSCRIBER',
|
||||
GUEST = 'GUEST',
|
||||
}
|
||||
|
||||
export enum ChannelRoleFilter {
|
||||
OWNER = ChannelRole.OWNER,
|
||||
ADMIN = ChannelRole.ADMIN,
|
||||
SUBSCRIBER = ChannelRole.SUBSCRIBER,
|
||||
}
|
||||
|
||||
export class ListChannelsQuery {
|
||||
role?: ChannelRoleFilter;
|
||||
}
|
||||
|
||||
export class Channel {
|
||||
@ApiProperty({
|
||||
description: 'Newsletter id',
|
||||
example: '123123123123@newsletter',
|
||||
})
|
||||
id: string;
|
||||
|
||||
name: string;
|
||||
|
||||
description?: string;
|
||||
|
||||
@ApiProperty({
|
||||
description: 'Invite link',
|
||||
example: 'https://www.whatsapp.com/channel/111111111111111111111111',
|
||||
})
|
||||
invite: string;
|
||||
|
||||
@ApiProperty({
|
||||
description: "Preview for channel's picture",
|
||||
example: 'https://mmg.whatsapp.net/m1/v/t24/An&_nc_cat=10',
|
||||
})
|
||||
preview?: string;
|
||||
|
||||
@ApiProperty({
|
||||
description: "Channel's picture",
|
||||
example: 'https://mmg.whatsapp.net/m1/v/t24/An&_nc_cat=10',
|
||||
})
|
||||
picture?: string;
|
||||
|
||||
verified: boolean;
|
||||
|
||||
role: ChannelRole;
|
||||
}
|
||||
|
||||
export const NewsletterIdApiParam = ApiParam({
|
||||
name: 'id',
|
||||
required: true,
|
||||
type: 'string',
|
||||
schema: {
|
||||
default: '123123123@newsletter',
|
||||
},
|
||||
description: 'WhatsApp Channel ID',
|
||||
});
|
||||
|
||||
export const NewsletterIdOrInviteCodeApiParam = ApiParam({
|
||||
name: 'id',
|
||||
required: true,
|
||||
type: 'string',
|
||||
schema: {
|
||||
default: '123123123@newsletter',
|
||||
},
|
||||
description:
|
||||
'WhatsApp Channel ID or invite code from invite link https://www.whatsapp.com/channel/11111',
|
||||
});
|
||||
@@ -1,6 +1,8 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { Type } from 'class-transformer';
|
||||
import { IsNumber, IsOptional } from 'class-validator';
|
||||
|
||||
import { SessionBaseRequest, SessionQuery } from './base.dto';
|
||||
import { SessionQuery } from './base.dto';
|
||||
|
||||
/**
|
||||
* Queries
|
||||
@@ -8,6 +10,7 @@ import { SessionBaseRequest, SessionQuery } from './base.dto';
|
||||
|
||||
export class GetChatMessagesQuery extends SessionQuery {
|
||||
limit: number = 100;
|
||||
|
||||
@ApiProperty({
|
||||
example: true,
|
||||
required: false,
|
||||
@@ -16,6 +19,18 @@ export class GetChatMessagesQuery extends SessionQuery {
|
||||
downloadMedia: boolean = true;
|
||||
}
|
||||
|
||||
export class GetChatsQuery {
|
||||
@IsNumber()
|
||||
@IsOptional()
|
||||
@Type(() => Number)
|
||||
limit?: number;
|
||||
|
||||
@IsNumber()
|
||||
@IsOptional()
|
||||
@Type(() => Number)
|
||||
offset?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Requests
|
||||
*/
|
||||
@@ -27,7 +27,6 @@ export enum WAHAEngine {
|
||||
VENOM = 'VENOM',
|
||||
WEBJS = 'WEBJS',
|
||||
NOWEB = 'NOWEB',
|
||||
MOBILE = 'MOBILE',
|
||||
}
|
||||
|
||||
export enum WAHAPresenceStatus {
|
||||
|
||||
@@ -20,7 +20,8 @@ export class RemoteFile {
|
||||
|
||||
@ApiProperty({
|
||||
example:
|
||||
'https://github.com/devlikeapro/whatsapp-http-api/raw/core/examples/dev.likeapro.jpg',
|
||||
process.env.WHATSAPP_SWAGGER_JPG_EXAMPLE_URL ||
|
||||
'https://github.com/devlikeapro/waha/raw/core/examples/dev.likeapro.jpg',
|
||||
})
|
||||
url: string;
|
||||
}
|
||||
@@ -73,7 +74,8 @@ export class VoiceRemoteFile {
|
||||
|
||||
@ApiProperty({
|
||||
example:
|
||||
'https://github.com/devlikeapro/whatsapp-http-api/raw/core/examples/dev.likeapro.opus',
|
||||
process.env.WHATSAPP_SWAGGER_OPUS_EXAMPLE_URL ||
|
||||
'https://github.com/devlikeapro/waha/raw/core/examples/dev.likeapro.opus',
|
||||
})
|
||||
url: string;
|
||||
}
|
||||
@@ -110,7 +112,8 @@ export class VideoRemoteFile {
|
||||
|
||||
@ApiProperty({
|
||||
example:
|
||||
'https://github.com/devlikeapro/whatsapp-http-api/raw/core/examples/video.mp4',
|
||||
process.env.WHATSAPP_SWAGGER_VIDEO_EXAMPLE_URL ||
|
||||
'https://github.com/devlikeapro/waha/raw/core/examples/video.mp4',
|
||||
})
|
||||
url: string;
|
||||
}
|
||||
@@ -37,6 +37,26 @@ export class ProxyConfig {
|
||||
password?: string;
|
||||
}
|
||||
|
||||
export class NowebStoreConfig {
|
||||
@ApiProperty({
|
||||
description:
|
||||
'Enable or disable the store for contacts, chats, and messages.',
|
||||
})
|
||||
enabled: boolean = false;
|
||||
|
||||
@ApiProperty({
|
||||
description:
|
||||
'Enable full sync on session initialization (when scanning QR code).\n' +
|
||||
'Full sync will download all contacts, chats, and messages from the phone.\n' +
|
||||
'If disabled, only messages early than 90 days will be downloaded and some contacts may be missing.',
|
||||
})
|
||||
fullSync: boolean = false;
|
||||
}
|
||||
|
||||
export class NowebConfig {
|
||||
store?: NowebStoreConfig;
|
||||
}
|
||||
|
||||
export class SessionConfig {
|
||||
webhooks?: WebhookConfig[];
|
||||
|
||||
@@ -46,6 +66,16 @@ export class SessionConfig {
|
||||
proxy?: ProxyConfig;
|
||||
|
||||
debug: boolean = false;
|
||||
|
||||
@ApiProperty({
|
||||
example: {
|
||||
store: {
|
||||
enabled: true,
|
||||
fullSync: false,
|
||||
},
|
||||
},
|
||||
})
|
||||
noweb?: NowebConfig;
|
||||
}
|
||||
|
||||
export class SessionStartRequest {
|
||||
|
||||
@@ -19,7 +19,7 @@ class StatusRequest {
|
||||
}
|
||||
|
||||
export class TextStatus extends StatusRequest {
|
||||
text = 'Have a look! https://waha.devlike.pro/';
|
||||
text = 'Have a look! https://github.com/';
|
||||
backgroundColor = '#38b42f';
|
||||
font = 1;
|
||||
}
|
||||
@@ -62,3 +62,11 @@ export class VideoStatus extends StatusRequest {
|
||||
|
||||
caption: string;
|
||||
}
|
||||
|
||||
export class DeleteStatusRequest extends StatusRequest {
|
||||
@ApiProperty({
|
||||
description: 'status message id',
|
||||
example: 'AAAAAAAAAAAAAAAAA',
|
||||
})
|
||||
id: string;
|
||||
}
|
||||
@@ -32,8 +32,10 @@ export class HmacConfiguration {
|
||||
|
||||
export class WebhookConfig {
|
||||
@ApiProperty({
|
||||
example: 'https://httpbin.org/post',
|
||||
example: 'https://webhook.site/11111111-1111-1111-1111-11111111',
|
||||
required: true,
|
||||
description:
|
||||
'You can use https://docs.webhook.site/ to test webhooks and see the payload',
|
||||
})
|
||||
url: string;
|
||||
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
import { LoggerService } from '@nestjs/common';
|
||||
import { WebSocket } from '@waha/utils/ws';
|
||||
import { WebSocketServer } from 'ws';
|
||||
|
||||
export class HeartbeatJob {
|
||||
private interval: ReturnType<typeof setInterval>;
|
||||
|
||||
constructor(
|
||||
private logger: LoggerService,
|
||||
private intervalTime: number = 10_000,
|
||||
) {}
|
||||
|
||||
start(server: WebSocketServer) {
|
||||
server.on('connection', (ws: WebSocket) => {
|
||||
ws.isAlive = true;
|
||||
ws.on('pong', this.onPong(ws));
|
||||
});
|
||||
|
||||
this.interval = setInterval(() => {
|
||||
server.clients.forEach((client: WebSocket) => {
|
||||
if (client.isAlive === false) {
|
||||
this.logger.debug(
|
||||
`Terminating client connection due to heartbeat timeout, ${client.id}`,
|
||||
);
|
||||
return client.terminate();
|
||||
}
|
||||
|
||||
client.isAlive = false;
|
||||
this.logger.debug(`Sending heartbeat (ping) to ${client.id}`);
|
||||
client.ping();
|
||||
});
|
||||
}, this.intervalTime);
|
||||
}
|
||||
|
||||
stop() {
|
||||
clearInterval(this.interval);
|
||||
}
|
||||
|
||||
private onPong(ws: WebSocket) {
|
||||
return (event: any) => {
|
||||
ws.isAlive = true;
|
||||
this.logger.debug(`Heartbeat (pong) received from ${ws.id}`);
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
import { LogLevel } from '@nestjs/common';
|
||||
import { ChildLoggerOptions, Level, Logger } from 'pino';
|
||||
|
||||
export interface LoggerBuilder {
|
||||
child(bindings: Record<string, any>, options?: ChildLoggerOptions): Logger;
|
||||
}
|
||||
|
||||
function getNestJSLogLevels(): LogLevel[] {
|
||||
const level = getPinoLogLevel();
|
||||
switch (level) {
|
||||
case 'trace':
|
||||
return ['error', 'warn', 'log', 'debug', 'verbose'];
|
||||
case 'debug':
|
||||
return ['error', 'warn', 'log', 'debug'];
|
||||
case 'info':
|
||||
return ['error', 'warn', 'log'];
|
||||
case 'warn':
|
||||
return ['error', 'warn'];
|
||||
case 'error':
|
||||
return ['error', 'warn'];
|
||||
default:
|
||||
return ['error', 'warn', 'log'];
|
||||
}
|
||||
}
|
||||
|
||||
export function getPinoLogLevel(debug: boolean = false): Level {
|
||||
const enableDebug = process.env.DEBUG != undefined || debug;
|
||||
if (enableDebug) {
|
||||
return 'debug';
|
||||
}
|
||||
return getDefaultPinoLogLevel();
|
||||
}
|
||||
|
||||
export function getDefaultPinoLogLevel(): Level {
|
||||
const logLevel = (process.env.WAHA_LOG_LEVEL || 'info').toLowerCase();
|
||||
const levels = ['fatal', 'error', 'warn', 'info', 'debug', 'trace'];
|
||||
if (!levels.includes(logLevel)) {
|
||||
console.error(
|
||||
`Unknown ${process.env.WAHA_LOG_LEVEL}' value for WAHA_LOG_LEVEL`,
|
||||
);
|
||||
return 'info';
|
||||
}
|
||||
return logLevel as Level;
|
||||
}
|
||||
|
||||
export function getPinoTransport() {
|
||||
const logFormat = (process.env.WAHA_LOG_FORMAT || 'PRETTY').toUpperCase();
|
||||
if (logFormat == 'JSON') {
|
||||
return undefined;
|
||||
}
|
||||
return {
|
||||
target: 'pino-pretty',
|
||||
options: {
|
||||
singleLine: true,
|
||||
colorize: true,
|
||||
messageFormat: '{if session} session:{session} - {end}{msg}',
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export { getNestJSLogLevels };
|
||||
@@ -0,0 +1,6 @@
|
||||
import { WebSocket as WSWebSocket } from 'ws';
|
||||
|
||||
export interface WebSocket extends WSWebSocket {
|
||||
id?: string;
|
||||
isAlive?: boolean;
|
||||
}
|
||||
+1
-1
@@ -33,7 +33,7 @@ export function getEngineName(): string {
|
||||
}
|
||||
|
||||
export const VERSION: WAHAEnvironment = {
|
||||
version: '2024.5.13',
|
||||
version: '2024.7.6',
|
||||
engine: getEngineName(),
|
||||
tier: getWAHAVersion(),
|
||||
browser: getBrowserExecutablePath(),
|
||||
|
||||
+4
-1
@@ -9,7 +9,10 @@
|
||||
"target": "es2017",
|
||||
"sourceMap": true,
|
||||
"outDir": "./dist",
|
||||
"baseUrl": "./",
|
||||
"baseUrl": "./src",
|
||||
"paths": {
|
||||
"@waha/*": ["*"]
|
||||
},
|
||||
"incremental": true,
|
||||
"resolveJsonModule": true,
|
||||
"skipLibCheck": true
|
||||
|
||||
Reference in new issue
Block a user