Compare commits

..
84 Commits
Author SHA1 Message Date
devlikepro 100d0be513 [core] 2024.7.6
Release / amd64 - chrome - chrome (push) Waiting to run
Release / amd64 - chromium - latest (push) Waiting to run
Release / linux/arm64 - chromium - arm (push) Waiting to run
Release / amd64 - none - noweb (push) Waiting to run
Release / linux/arm64 - none - noweb-arm (push) Waiting to run
2024-07-18 17:41:38 +03:00
devlikepro fc8db2d977 [core] WEBJS update 2024-07-18 17:41:31 +03:00
devlikepro e5d98d7889 [core] ignore /dashboard logs 2024-07-18 17:40:47 +03:00
devlikepro c7b1a05ed3 [core] Download dashboard 2024-07-15 18:46:43 +03:00
devlikepro 649a90bd5e [core] Keep dashboard 2024-07-15 17:56:04 +03:00
devlikepro 0fc880cf21 [core] Keep dashboard 2024-07-15 17:55:54 +03:00
devlikepro 0d82a4c2c6 [core] Remove dashboard 2024-07-15 17:54:38 +03:00
devlikepro fb3aa59683 [core] 2024.7.5
Release / amd64 - chrome - chrome (push) Waiting to run
Release / amd64 - chromium - latest (push) Waiting to run
Release / linux/arm64 - chromium - arm (push) Waiting to run
Release / amd64 - none - noweb (push) Waiting to run
Release / linux/arm64 - none - noweb-arm (push) Waiting to run
2024-07-15 16:03:08 +03:00
devlikepro b938792ca8 [core] channels 2024-07-15 16:02:51 +03:00
devlikepro bb74946d54 [core] bump 2024.7.4
Release / amd64 - chrome - chrome (push) Waiting to run
Release / amd64 - chromium - latest (push) Waiting to run
Release / linux/arm64 - chromium - arm (push) Waiting to run
Release / amd64 - none - noweb (push) Waiting to run
Release / linux/arm64 - none - noweb-arm (push) Waiting to run
2024-07-08 15:02:36 +03:00
devlikepro 3e78105509 [core] fix profile picture
closes #422
2024-07-08 15:02:36 +03:00
devlikepro c6adf0ac89 [core] console.log for webjs 2024-07-05 14:04:03 +03:00
devlikepro 219b4dca94 [core] pretty logs by default 2024-07-05 13:40:13 +03:00
devlikepro acec292b0f [core] 2024.7.3 2024-07-05 13:30:47 +03:00
devlikepro 7ba86cbd23 [core] logging 2024-07-05 13:30:08 +03:00
devlikepro 3002eaf479 [core] Init Logger in manager explicitly 2024-07-05 13:30:05 +03:00
devlikepro 15422ca504 [core] 2024.7.2
Release / amd64 - chrome - chrome (push) Waiting to run
Release / amd64 - chromium - latest (push) Waiting to run
Release / linux/arm64 - chromium - arm (push) Waiting to run
Release / amd64 - none - noweb (push) Waiting to run
Release / linux/arm64 - none - noweb-arm (push) Waiting to run
2024-07-04 12:11:43 +03:00
devlikepro 7067f00bee [core] Up WEBJS engine 2024-07-04 12:11:43 +03:00
devlikepro a0616eb251 [core] 2024.7.1
Release / amd64 - chrome - chrome (push) Waiting to run
Release / amd64 - chromium - latest (push) Waiting to run
Release / linux/arm64 - chromium - arm (push) Waiting to run
Release / amd64 - none - noweb (push) Waiting to run
Release / linux/arm64 - none - noweb-arm (push) Waiting to run
2024-07-03 10:58:33 +03:00
devlikepro 5d4da0e4c7 [core] No cache while waiting 2.3xx release 2024-07-03 10:56:33 +03:00
devlikepro 546e3475fb [core] mark GET sendText as deprecated
closes #406
2024-07-03 09:15:38 +03:00
devlikepro 56c44ebb58 [core] 2.3000
Release / amd64 - chrome - chrome (push) Waiting to run
Release / amd64 - chromium - latest (push) Waiting to run
Release / linux/arm64 - chromium - arm (push) Waiting to run
Release / amd64 - none - noweb (push) Waiting to run
Release / linux/arm64 - none - noweb-arm (push) Waiting to run
2024-06-27 15:25:33 +07:00
devlikepro bd03bc730c [core] Remove 2.3000 2024-06-27 15:25:00 +07:00
devlikepro e0e1e58104 [core] 2024.6.8 2024-06-27 15:19:41 +07:00
devlikepro 940de662b6 [core] WEBJS 2.3000 2024-06-27 15:19:41 +07:00
devlikepro d151c1192b [core] update webjs 2024-06-27 15:19:41 +07:00
devlikepro 15ae10579c [core] use fork 2024-06-27 15:19:41 +07:00
devlikepro 359696408d [core] 2024.6.7
Release / amd64 - chrome - chrome (push) Waiting to run
Release / amd64 - chromium - latest (push) Waiting to run
Release / linux/arm64 - chromium - arm (push) Waiting to run
Release / amd64 - none - noweb (push) Waiting to run
Release / linux/arm64 - none - noweb-arm (push) Waiting to run
2024-06-25 13:36:40 +07:00
devlikepro d3766368f8 [core] delete status endpoint 2024-06-25 13:36:40 +07:00
devlikepro 73af7451fc [core] Use NOWEB fork with fixes
fixes #390
2024-06-25 11:58:24 +07:00
devlikepro e27fcefe45 [core] fix #373 2024-06-24 19:50:22 +07:00
devlikepro aa5ade5517 [core] Remove link to project
Fix #373
2024-06-24 19:43:20 +07:00
devlikepro 94b6a66ce4 [core] dashboard 2024-06-24 19:16:28 +07:00
devlikepro a51e486fa6 [core] timeout 10ms 2024-06-24 19:10:07 +07:00
devlikepro 2b608daea4 update dashboard 2024-06-24 18:50:38 +07:00
devlikepro aa60c2afd1 [core] warning 2024-06-24 18:49:26 +07:00
devlikepro cff22c74a9 [core] Fix sock auth 2024-06-24 18:43:23 +07:00
devlikepro b77b8ee0da [core] timeout for getEngineInfo, logs 2024-06-24 18:41:42 +07:00
devlikepro 610aef2f14 [core] Make getSessionMeInfo sync 2024-06-24 18:41:42 +07:00
devlikepro e15ca41d6e [core] websockets 2024-06-24 17:38:44 +07:00
devlikepro 60e9e53af4 [core] issue session.status, not session.status.changed 2024-06-24 17:36:20 +07:00
devlikepro 9812000724 [core] debug by default in NOWEB 2024-06-21 12:17:12 +07:00
devlikepro d72401fc39 [core] Add msgRetryCounterCache 2024-06-20 14:21:00 +07:00
devlikepro 822eba7982 [core] keepAliveIntervalMs back to 30s 2024-06-19 16:20:01 +07:00
devlikepro 9327564e6e [core] set defaultQueryTimeoutMs to 120s 2024-06-19 16:20:01 +07:00
devlikepro 6e9fe42a83 [core] bump version 2024-06-19 12:55:14 +07:00
devlikepro a32cf28ccb [core] fix forever "typing" status
fix #379
2024-06-19 12:54:43 +07:00
devlikepro 55b101802a [core] no more ssh 2024-06-19 12:04:11 +07:00
devlikepro ecda8b4394 [core] set "trace" by default for noweb 2024-06-19 12:04:09 +07:00
devlikepro 6d4f8d4c26 [core] Add logger to NOWEB engine 2024-06-19 11:30:41 +07:00
devlikepro 26aa53f3fa Use webhook.site 2024-06-18 11:18:39 +07:00
devlikepro 2f7a94ea3f [core] dashboard update
Release / amd64 - chrome - chrome (push) Waiting to run
Release / amd64 - chromium - latest (push) Waiting to run
Release / linux/arm64 - chromium - arm (push) Waiting to run
Release / amd64 - none - noweb (push) Waiting to run
Release / linux/arm64 - none - noweb-arm (push) Waiting to run
2024-06-18 11:02:16 +07:00
devlikepro 2b622b4ee9 [core] dashboard 2024-06-18 10:55:41 +07:00
devlikepro 3e280f1e08 [core] Bump version to 2024.6.5 2024-06-18 10:51:02 +07:00
devlikepro c54d52ad45 [core] built in ssl 2024-06-18 10:50:44 +07:00
devlikepro e54d3ea96b [core] fix for-swagger 2024-06-17 10:56:04 +07:00
devlikepro e07cb8257d [core] Bump to 2024.6.4
Release / amd64 - chrome - chrome (push) Waiting to run
Release / amd64 - chromium - latest (push) Waiting to run
Release / linux/arm64 - chromium - arm (push) Waiting to run
Release / amd64 - none - noweb (push) Waiting to run
Release / linux/arm64 - none - noweb-arm (push) Waiting to run
2024-06-17 10:37:11 +07:00
devlikepro 74b6a8fdba [core] add chats limit and offset 2024-06-17 10:36:51 +07:00
devlikepro e48c76397c [core] downgrade better-sqlite3
Release / amd64 - chrome - chrome (push) Waiting to run
Release / amd64 - chromium - latest (push) Waiting to run
Release / linux/arm64 - chromium - arm (push) Waiting to run
Release / amd64 - none - noweb (push) Waiting to run
Release / linux/arm64 - none - noweb-arm (push) Waiting to run
2024-06-14 13:52:39 +07:00
devlikepro 64aff5d041 [core] fix Dockerfile 2024-06-13 16:06:00 +07:00
devlikepro 87b24e35d0 [core] update dashboard 2024-06-13 15:58:37 +07:00
devlikepro fb48c81207 [core] rm dashboard 2024-06-13 15:57:36 +07:00
devlikepro cd9f302f8b [core] 2024.6.3 2024-06-13 15:46:43 +07:00
devlikepro df3bc97988 [core] noweb store 2024-06-13 15:46:43 +07:00
devlikepro 94191a2902 [core] noweb preview video 2024-06-13 15:46:43 +07:00
devlikepro a0dd540075 [core] webjs fix download media 2024-06-13 15:46:43 +07:00
devlikepro 5684665123 [core] Add getFilePath to LocalStore 2024-06-13 15:46:43 +07:00
devlikepro 5db407d750 [core] Fix docker pulls number 2024-06-08 17:02:30 +07:00
devlikepro 27a86f0dc1 [core] waha
Release / amd64 - chrome - chrome (push) Waiting to run
Release / amd64 - chromium - latest (push) Waiting to run
Release / linux/arm64 - chromium - arm (push) Waiting to run
Release / amd64 - none - noweb (push) Waiting to run
Release / linux/arm64 - none - noweb-arm (push) Waiting to run
2024-06-04 12:55:58 +07:00
devlikepro 767ea23173 [core] bump 2024.6.2 2024-06-04 12:54:24 +07:00
devlikepro 85707508ed [core] waha 2024-06-04 12:54:03 +07:00
devlikepro e8bc59ff9e [core] Bump to 2024.6.1 2024-06-02 10:25:03 +07:00
devlikepro 7f9e622a64 [core] push version tags 2024-06-02 10:06:49 +07:00
devlikepro b22e37596b Revert "[core] Don't push each version"
This reverts commit 0e295bebde.
2024-06-02 10:04:12 +07:00
devlikepro 642e90b1c8 [core] poll response fix
fix #356
2024-06-02 10:01:40 +07:00
devlikepro 8402ab896a [core] Fix ack for status@broadcast
Release / amd64 - chrome - chrome (push) Waiting to run
Release / amd64 - chromium - latest (push) Waiting to run
Release / linux/arm64 - chromium - arm (push) Waiting to run
Release / amd64 - none - noweb (push) Waiting to run
Release / linux/arm64 - none - noweb-arm (push) Waiting to run
fix #329
2024-06-01 16:16:19 +07:00
devlikepro d6dc7edf66 [core] WEBJS enable io from chrome if debug enabled 2024-05-31 18:16:34 +07:00
devlikepro 8f6dbf70f7 [core] WEBJS auto restart on FAILED
fix #355
2024-05-31 18:16:34 +07:00
devlikepro bd14d9a71e [core] 2024.5.14 2024-05-31 13:32:57 +07:00
devlikepro 61a0a38dd2 [core] Restart NOWEB every 30 minutes 2024-05-31 12:50:38 +07:00
devlikepro 102f438a99 [core] NOWEB fix for creds.json
fix #347
2024-05-31 12:18:11 +07:00
devlikepro 586a2d1c79 [core] rm import 2024-05-31 11:54:49 +07:00
devlikepro 5b04c39107 [core] move get default engine to EngineConfigService.ts 2024-05-31 11:54:49 +07:00
devlikepro d96e91ea70 [core] Better logging 2024-05-31 11:54:49 +07:00
76 changed files with 4556 additions and 1817 deletions

No files matched your search

+2 -2
View File
@@ -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:
+1 -1
View File
@@ -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'
+1
View File
@@ -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'
+2
View File
@@ -1,12 +1,14 @@
.yarn
example.ts
.*sessions
.secrets
tokens
files
venv
.wwebjs_auth
test.js
.wwebjs_cache
src/dashboard
# compiled output
/dist
+12
View File
@@ -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
View File
@@ -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
+10 -10
View File
@@ -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:
+5 -5
View File
@@ -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!
[![Docker Pulls](https://img.shields.io/docker/pulls/devlikeapro/whatsapp-http-api)](https://hub.docker.com/r/devlikeapro/whatsapp-http-api)
[![Docker Pulls](https://img.shields.io/docker/pulls/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
View File
@@ -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
View File
@@ -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
View File
@@ -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",
+34
View File
@@ -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));
}),
);
}
}
+119
View File
@@ -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);
}
}
+10 -5
View File
@@ -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')
+1 -1
View File
@@ -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();
+1 -1
View File
@@ -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') {
+2 -2
View File
@@ -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();
}
}
+11
View File
@@ -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 };
-11
View File
@@ -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);
}
+9 -3
View File
@@ -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!',
+28 -5
View File
@@ -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
View File
@@ -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}`;
}
+6 -2
View File
@@ -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);
+135
View File
@@ -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));
});
}
}
+47 -1
View File
@@ -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');
+19 -2
View File
@@ -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);
+6 -1
View File
@@ -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) {
+458 -108
View File
@@ -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:');
}
}
+56
View File
@@ -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');
},
};
};
+71
View File
@@ -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;
};
+4 -3
View File
@@ -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&#123;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%&#125;._apdl ._apdm&#123;margin-top:-40px;color:var(--splashscreen-startup-icon)&#125;._apdl ._apdm::after&#123;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:&#039;&#039;;position:relative;top:-100%;display:block;height:100%;left:calc(50% - 72px);opacity:1;width:calc(72px * 3)&#125;&#064;keyframes shimmer&#123;from&#123;left:calc(50% - 72px)&#125;to&#123;left:calc(50% + 72px)&#125;&#125;._apdl ._apdq&#123;height:3px;margin-top:40px;position:relative;width:320px&#125;._apdl ._apdq progress&#123;vertical-align:top&#125;._apdl ._apdr&#123;margin-top:40px;font-size:17px;color:var(--splashscreen-primary-title)&#125;._apdl ._apds&#123;margin-top:12px;font-size:14px;color:var(--splashscreen-secondary-lighter)&#125;._apdl ._apds span&#123;display:inline-block;margin-bottom:2px;vertical-align:middle&#125;._apdl ._apdq progress&#123;-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%&#125;._apdl ._apdq progress[value]::-webkit-progress-bar&#123;background-color:var(--splashscreen-progress-background)&#125;._apdl ._apdq progress[value]::-webkit-progress-value&#123;background-color:var(--splashscreen-progress-primary);transition:width .45s ease&#125;%23bootloader_z3kVBe5&#123;height:42px;&#125;.bootloader_z3kVBe5&#123;display:block!important;&#125;" 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>
+15
View File
@@ -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);
};
};
+35
View File
@@ -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));
}
}
+238 -59
View File
@@ -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
View File
@@ -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> {
+5
View File
@@ -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;
}
+4
View File
@@ -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
View File
@@ -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,
View File
Whitespace-only changes.
Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

-289
View File
@@ -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
View File
@@ -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
View File
@@ -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();
+94
View File
@@ -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',
});
+16 -1
View File
@@ -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
*/
-1
View File
@@ -27,7 +27,6 @@ export enum WAHAEngine {
VENOM = 'VENOM',
WEBJS = 'WEBJS',
NOWEB = 'NOWEB',
MOBILE = 'MOBILE',
}
export enum WAHAPresenceStatus {
+6 -3
View File
@@ -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;
}
+30
View File
@@ -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 {
+9 -1
View File
@@ -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;
}
+3 -1
View File
@@ -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;
+45
View File
@@ -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}`);
};
}
}
+61
View File
@@ -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 };
+6
View File
@@ -0,0 +1,6 @@
import { WebSocket as WSWebSocket } from 'ws';
export interface WebSocket extends WSWebSocket {
id?: string;
isAlive?: boolean;
}
+1 -1
View File
@@ -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
View File
@@ -9,7 +9,10 @@
"target": "es2017",
"sourceMap": true,
"outDir": "./dist",
"baseUrl": "./",
"baseUrl": "./src",
"paths": {
"@waha/*": ["*"]
},
"incremental": true,
"resolveJsonModule": true,
"skipLibCheck": true
+1641 -1182
View File
File diff suppressed because it is too large. Load diff