Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7eeafd03b3 | ||
|
|
f88480d271 | ||
|
|
9b8cce85bd | ||
|
|
aa364b57e8 | ||
|
|
ea973d0de4 | ||
|
|
1a1b64cbb2 | ||
|
|
09c7e11dbc | ||
|
|
3450a56756 | ||
|
|
f37f4466ae | ||
|
|
451834b219 | ||
|
|
91924353e2 | ||
|
|
67cf94fc95 | ||
|
|
36a343c09f | ||
|
|
4a37aebbf1 | ||
|
|
d5fa9f8d72 | ||
|
|
08c244334b | ||
|
|
23dd1c2726 | ||
|
|
23a309d394 | ||
|
|
3c1bb4e145 | ||
|
|
87b320a3e3 | ||
|
|
e4fb9615c7 | ||
|
|
efad4de0af | ||
|
|
04f62700a2 | ||
|
|
08415f7931 | ||
|
|
8bc86af3c8 | ||
|
|
fa2c5e42dc | ||
|
|
e3d3952b81 | ||
|
|
a9171addde | ||
|
|
440ac087ad | ||
|
|
9dd5eb4eeb | ||
|
|
26c2c9abc9 | ||
|
|
663b07c07e | ||
|
|
5add6a0e73 | ||
|
|
5c4912feb4 | ||
|
|
75e115f390 | ||
|
|
8e443d23f4 | ||
|
|
c43c9c319b | ||
|
|
e32b128b0f | ||
|
|
5287421cea | ||
|
|
9bc95ef350 | ||
|
|
028de6addb | ||
|
|
a107605937 | ||
|
|
cec5d0147b | ||
|
|
e46b879fc4 | ||
|
|
b6764fbb29 | ||
|
|
e8899cc314 | ||
|
|
2b2054dd6b | ||
|
|
92705b0ae2 | ||
|
|
2ae8a36b7f | ||
|
|
899139dda1 | ||
|
|
b6b571e84b | ||
|
|
9e098fa997 | ||
|
|
13d00f954f | ||
|
|
fee75d0c8a | ||
|
|
abae3f14cd | ||
|
|
b8a98e2c6e | ||
|
|
d990572b5b | ||
|
|
2d37d392bc | ||
|
|
59015fc3bb | ||
|
|
c3be68f988 | ||
|
|
8610784958 | ||
|
|
f01d71c34a | ||
|
|
a06501e4b1 | ||
|
|
7c2c30fa1b | ||
|
|
903dc714f2 | ||
|
|
b1bd2c8805 | ||
|
|
0e4b4f6d8c | ||
|
|
d8d0eaf247 | ||
|
|
90096faef3 | ||
|
|
f794f75394 | ||
|
|
8de63e486c | ||
|
|
4308efca47 | ||
|
|
877fa292d6 | ||
|
|
df6ea6f497 | ||
|
|
5e9be391e1 | ||
|
|
49960b8234 | ||
|
|
d669dbe54b | ||
|
|
2d1859ac0c | ||
|
|
dc864055a3 | ||
|
|
bad9537aba | ||
|
|
303b1adc0a | ||
|
|
c582904d41 | ||
|
|
65eabf6153 | ||
|
|
cc9c9e2253 | ||
|
|
8747ae9564 | ||
|
|
9253e54958 | ||
|
|
fb774f2caa | ||
|
|
ba120024a6 | ||
|
|
1fe0e69914 | ||
|
|
0fe04e411c | ||
|
|
35074f959f | ||
|
|
0d698fbaf1 | ||
|
|
c9e2a52327 | ||
|
|
a58e06109b | ||
|
|
65375e4836 | ||
|
|
edc517d33f | ||
|
|
354c82dc34 | ||
|
|
fe2acd0665 | ||
|
|
19b5b1a90c | ||
|
|
e7281e2118 | ||
|
|
10f334cf15 | ||
|
|
a057fd6458 | ||
|
|
3bafabeb0a | ||
|
|
edefbe17da | ||
|
|
a65041ffcd | ||
|
|
0962c60844 | ||
|
|
3d72eceac0 | ||
|
|
57b5deff5b | ||
|
|
1163dee352 | ||
|
|
f7fe562bba | ||
|
|
3394f98a8a | ||
|
|
a575b35286 | ||
|
|
0f41e79e98 | ||
|
|
24bf22a31f | ||
|
|
05a1b8802e | ||
|
|
9c409d9507 | ||
|
|
4ee7161c36 | ||
|
|
4cd9ce0ff7 | ||
|
|
a2d0dfd8b2 | ||
|
|
d4164fe0ba | ||
|
|
6bbecdd7c0 | ||
|
|
0784c3ae05 | ||
|
|
679757ccfa | ||
|
|
63a4e693e3 | ||
|
|
b21041d9e0 | ||
|
|
f014bb4587 | ||
|
|
9030f7c4ce | ||
|
|
bde6c38bd4 | ||
|
|
0aefb6c6ec | ||
|
|
da650ccdef | ||
|
|
92d3e70c1f | ||
|
|
cb5dfaf066 | ||
|
|
cd75e5e4cc | ||
|
|
7bd6ab5ec0 | ||
|
|
92f9c89cf1 | ||
|
|
eb2632b23a | ||
|
|
8c90bc6808 | ||
|
|
66146532e1 | ||
|
|
dbf3a41c1d | ||
|
|
942bbf9c6c | ||
|
|
a9d7cc527c | ||
|
|
2d362722ed | ||
|
|
f81638e625 | ||
|
|
8ae77df0ff | ||
|
|
110bd0e5f7 | ||
|
|
1b3a366cb7 | ||
|
|
3b5d9a145f | ||
|
|
fc37fc067a | ||
|
|
bb18a28484 | ||
|
|
86ca08b9aa | ||
|
|
9522df7835 | ||
|
|
55e0fd0226 | ||
|
|
ff9a986a2e | ||
|
|
df31fe2a8b | ||
|
|
1aeaf996bc | ||
|
|
cb0e59e91a | ||
|
|
5f02f49e9d | ||
|
|
ce9f91fe2c | ||
|
|
ac37745721 | ||
|
|
e44aeefda8 | ||
|
|
dbff2b659a | ||
|
|
52ef8e8b76 | ||
|
|
b71567c754 | ||
|
|
d1670c743f | ||
|
|
009b38f471 | ||
|
|
0cf5f30c0d | ||
|
|
cd4e1f179d | ||
|
|
b89f9d6b35 | ||
|
|
c3ece96390 | ||
|
|
770739b44e | ||
|
|
84984c2cde | ||
|
|
86393c8674 | ||
|
|
d4aed3bc45 | ||
|
|
818cd77450 | ||
|
|
dc26c70400 | ||
|
|
6547d713ea | ||
|
|
e8dcb0c242 |
No files matched your search
@@ -2,13 +2,18 @@
|
||||
example.ts
|
||||
.*sessions
|
||||
.secrets
|
||||
.minio
|
||||
.media
|
||||
tokens
|
||||
files
|
||||
venv
|
||||
.wwebjs_auth
|
||||
src/test.ts
|
||||
src/test.js
|
||||
test.js
|
||||
.wwebjs_cache
|
||||
src/dashboard
|
||||
.eslintcache
|
||||
|
||||
# compiled output
|
||||
/dist
|
||||
|
||||
+15
-1
@@ -6,12 +6,21 @@ exclude: |
|
||||
)$
|
||||
|
||||
repos:
|
||||
- repo: local
|
||||
hooks:
|
||||
- id: commit-message-prefix
|
||||
name: Validate Commit Message for src/plus Changes
|
||||
entry: python ./.precommit/validate_commit_message.py
|
||||
language: python
|
||||
stages: [commit-msg]
|
||||
|
||||
- repo: local
|
||||
hooks:
|
||||
- id: lint
|
||||
stages: [pre-commit]
|
||||
name: lint
|
||||
language: system
|
||||
entry: bash -c '$HOME/.nvm/nvm-exec npm run lint'
|
||||
entry: bash -c '$HOME/.nvm/nvm-exec npm run lint-cache'
|
||||
pass_filenames: false
|
||||
files: |
|
||||
(?x)^(
|
||||
@@ -22,6 +31,7 @@ repos:
|
||||
rev: 'v3.1.0'
|
||||
hooks:
|
||||
- id: prettier
|
||||
stages: [pre-commit]
|
||||
exclude: |
|
||||
(?x)^(
|
||||
docs/.*|
|
||||
@@ -31,11 +41,13 @@ repos:
|
||||
rev: v1.1.2
|
||||
hooks:
|
||||
- id: markdown-toc
|
||||
stages: [pre-commit]
|
||||
name: README.md
|
||||
files: ^README.md$
|
||||
- repo: local
|
||||
hooks:
|
||||
- id: no-plus-in-core
|
||||
stages: [pre-commit]
|
||||
name: No "plus" in core
|
||||
language: pygrep
|
||||
entry: 'plus'
|
||||
@@ -50,11 +62,13 @@ repos:
|
||||
- repo: local
|
||||
hooks:
|
||||
- id: no-console-log
|
||||
stages: [pre-commit]
|
||||
name: No console.log() calls
|
||||
language: pygrep
|
||||
entry: 'console\.log'
|
||||
exclude: |
|
||||
(?x)^(
|
||||
entrypoint.sh|
|
||||
.pre-commit-config.yaml|
|
||||
^src/core/engines/webjs/session.webjs.core.ts|
|
||||
)$
|
||||
@@ -0,0 +1,38 @@
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
|
||||
def get_staged_files():
|
||||
result = subprocess.run(["git", "diff", "--cached", "--name-only"], stdout=subprocess.PIPE, text=True)
|
||||
files = result.stdout.splitlines()
|
||||
return files
|
||||
|
||||
|
||||
def get_commit_message(commit_msg_filepath):
|
||||
with open(commit_msg_filepath, 'r') as f:
|
||||
return f.readline().strip()
|
||||
|
||||
|
||||
def main():
|
||||
commit_msg_filepath = sys.argv[1]
|
||||
commit_message = get_commit_message(commit_msg_filepath)
|
||||
staged_files = get_staged_files()
|
||||
|
||||
has_plus_changes = any(f.startswith('src/plus') for f in staged_files)
|
||||
print(commit_message)
|
||||
starts_with_plus = commit_message.startswith('[PLUS]')
|
||||
|
||||
if has_plus_changes and not starts_with_plus:
|
||||
print("'[PLUS]' not found in commit message, but there's changes are from 'src/plus'.")
|
||||
return 1
|
||||
|
||||
if starts_with_plus and not all(f.startswith('src/plus') for f in staged_files):
|
||||
print("'[PLUS]' found in commit message, but there's changes from other directories. \n"
|
||||
"Changes MUST be only from 'src/plus'.")
|
||||
return 1
|
||||
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
+10
-3
@@ -24,7 +24,7 @@ RUN yarn build && find ./dist -name "*.d.ts" -delete
|
||||
FROM node:${NODE_VERSION} as dashboard
|
||||
|
||||
# Download WAHA Dashboard
|
||||
ENV WAHA_DASHBOARD_SHA 7bfa9269fd385af687a464882637881dfd34cfb6
|
||||
ENV WAHA_DASHBOARD_SHA c1a91f879a35f0ae32d46ddb2e061ac1819a3d4f
|
||||
RUN \
|
||||
wget https://github.com/devlikeapro/dashboard/archive/${WAHA_DASHBOARD_SHA}.zip \
|
||||
&& unzip ${WAHA_DASHBOARD_SHA}.zip -d /tmp/dashboard \
|
||||
@@ -48,6 +48,9 @@ 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 zip and unzip
|
||||
RUN apt-get update && apt-get install -y zip unzip --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 \
|
||||
@@ -68,7 +71,7 @@ RUN if [ "$USE_BROWSER" = "chromium" ]; then \
|
||||
# Install Chrome
|
||||
# Available versions:
|
||||
# https://www.ubuntuupdates.org/package/google_chrome/stable/main/base/google-chrome-stable
|
||||
ARG CHROME_VERSION="122.0.6261.128-1"
|
||||
ARG CHROME_VERSION="126.0.6478.182-1"
|
||||
RUN if [ "$USE_BROWSER" = "chrome" ]; then \
|
||||
wget --no-verbose -O /tmp/chrome.deb https://dl.google.com/linux/chrome/deb/pool/main/g/google-chrome-stable/google-chrome-stable_${CHROME_VERSION}_amd64.deb \
|
||||
&& apt-get update \
|
||||
@@ -83,11 +86,15 @@ COPY package.json ./
|
||||
COPY --from=build /src/node_modules ./node_modules
|
||||
COPY --from=build /src/dist ./dist
|
||||
COPY --from=dashboard /dashboard ./dist/dashboard
|
||||
COPY entrypoint.sh /entrypoint.sh
|
||||
|
||||
# Chokidar options to monitor file changes
|
||||
ENV CHOKIDAR_USEPOLLING=1
|
||||
ENV CHOKIDAR_INTERVAL=5000
|
||||
|
||||
# WAHA variables
|
||||
ENV WAHA_ZIPPER=ZIPUNZIP
|
||||
|
||||
# Run command, etc
|
||||
EXPOSE 3000
|
||||
CMD yarn start:prod
|
||||
CMD ["/entrypoint.sh"]
|
||||
@@ -31,10 +31,11 @@ push:
|
||||
for-swagger:
|
||||
WHATSAPP_SWAGGER_CONFIG_ADVANCED=true . ${NVM_DIR}/nvm.sh && nvm exec yarn start
|
||||
|
||||
update-dependencies:
|
||||
up-noweb:
|
||||
. ${NVM_DIR}/nvm.sh && nvm exec yarn up @adiwajshing/baileys@github:devlikeapro/Baileys#fork-master-channels-2
|
||||
|
||||
up-webjs:
|
||||
. ${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:
|
||||
docker run --rm -d --name squid-container -e TZ=UTC -p 3128:3128 ubuntu/squid:5.2-22.04_beta
|
||||
@@ -141,7 +141,7 @@ later:
|
||||
How to log in - the instruction on WhatsApp site
|
||||
</a>
|
||||
|
||||
When your ready - find `POST /api/session/start`, click on **Try it out**, then **Execute** a bit below.
|
||||
When your ready - find `POST /api/sessions`, click on **Try it out**, then **Execute** a bit below.
|
||||
|
||||
|
||||
The example payload:
|
||||
|
||||
@@ -0,0 +1,114 @@
|
||||
services:
|
||||
waha:
|
||||
# Check available Docker images:
|
||||
# https://waha.devlike.pro/docs/how-to/engines/#docker-images
|
||||
image: devlikeapro/waha
|
||||
container_name: waha
|
||||
ports:
|
||||
- '3000:3000/tcp'
|
||||
environment:
|
||||
# Environment variables from https://waha.devlike.pro/docs/how-to/config/
|
||||
# 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
|
||||
|
||||
waha-plus:
|
||||
# Check available Docker images:
|
||||
# https://waha.devlike.pro/docs/how-to/engines/#docker-images
|
||||
image: devlikeapro/waha-plus:chrome
|
||||
container_name: waha-plus
|
||||
ports:
|
||||
- '3000:3000/tcp'
|
||||
volumes:
|
||||
- './.sessions:/app/.sessions'
|
||||
environment:
|
||||
# Environment variables from https://waha.devlike.pro/docs/how-to/config/
|
||||
# - WHATSAPP_RESTART_ALL_SESSIONS=True
|
||||
# - WHATSAPP_HOOK_URL=https://webhook.site/11111111-1111-1111-1111-11111111
|
||||
# - WHATSAPP_HOOK_EVENTS=message
|
||||
# - WHATSAPP_API_HOSTNAME=localhost
|
||||
- WHATSAPP_DEFAULT_ENGINE=WEBJS
|
||||
- WHATSAPP_API_KEY=321
|
||||
# Username and password for Swagger
|
||||
- WHATSAPP_SWAGGER_USERNAME=swagger
|
||||
- WHATSAPP_SWAGGER_PASSWORD=admin
|
||||
# Username and password for WAHA Dashboard
|
||||
# you can use the same username and password for both Swagger and Dashboard
|
||||
# Default - waha / waha
|
||||
- WAHA_DASHBOARD_USERNAME=waha
|
||||
- WAHA_DASHBOARD_PASSWORD=waha
|
||||
# Engine specific environment variables
|
||||
# - WAHA_WEBJS_WEB_VERSION=2.2412.54-videofix
|
||||
restart: always
|
||||
|
||||
waha-plus--mongodb:
|
||||
# Check available Docker images:
|
||||
# https://waha.devlike.pro/docs/how-to/engines/#docker-images
|
||||
image: devlikeapro/waha-plus:chrome
|
||||
container_name: waha-plus--mongodb
|
||||
ports:
|
||||
- '3000:3000/tcp'
|
||||
volumes:
|
||||
- './.sessions:/app/.sessions'
|
||||
environment:
|
||||
# Environment variables from https://waha.devlike.pro/docs/how-to/config/
|
||||
# - WHATSAPP_RESTART_ALL_SESSIONS=True
|
||||
# - WHATSAPP_HOOK_URL=https://webhook.site/11111111-1111-1111-1111-11111111
|
||||
# - WHATSAPP_HOOK_EVENTS=message
|
||||
# - WHATSAPP_API_KEY=321
|
||||
# - WHATSAPP_SWAGGER_USERNAME=admin
|
||||
# - WHATSAPP_SWAGGER_PASSWORD=123
|
||||
# - WAHA_DASHBOARD_USERNAME=admin
|
||||
# - WAHA_DASHBOARD_PASSWORD=123
|
||||
# - WHATSAPP_API_HOSTNAME=localhost
|
||||
- WHATSAPP_DEFAULT_ENGINE=WEBJS
|
||||
- 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
|
||||
ports:
|
||||
- '27017:27017/tcp'
|
||||
volumes:
|
||||
- mongodb_data:/data/db
|
||||
restart: always
|
||||
|
||||
volumes:
|
||||
mongodb_data: {}
|
||||
+214
-86
@@ -1,107 +1,215 @@
|
||||
version: '3'
|
||||
#
|
||||
# https://waha.devlike.pro/docs/how-to/install/
|
||||
# Here's we provide a simple example of how to install WAHA using Docker Compose.
|
||||
#
|
||||
# We're assuming you have Docker and Docker Compose installed.
|
||||
# If you don't have Docker Compose installed, you can follow the official guide:
|
||||
# https://docs.docker.com/compose/install/
|
||||
#
|
||||
|
||||
services:
|
||||
waha:
|
||||
# Check available Docker images:
|
||||
# https://waha.devlike.pro/docs/how-to/engines/#docker-images
|
||||
image: devlikeapro/waha
|
||||
container_name: waha
|
||||
ports:
|
||||
- '3000:3000/tcp'
|
||||
environment:
|
||||
# Environment variables from https://waha.devlike.pro/docs/how-to/config/
|
||||
# https://docs.webhook.site/
|
||||
- WHATSAPP_HOOK_URL=https://webhook.site/11111111-1111-1111-1111-11111111
|
||||
- WHATSAPP_HOOK_EVENTS=message
|
||||
- WHATSAPP_DEFAULT_ENGINE=WEBJS
|
||||
# Always restart the container on failure
|
||||
restart: always
|
||||
|
||||
waha-plus:
|
||||
# Check available Docker images:
|
||||
#
|
||||
# Image
|
||||
#
|
||||
image: devlikeapro/waha-plus:latest # or devlikeapro/waha:latest if you're using WAHA Core
|
||||
#
|
||||
# We strongly recommend using the pinned version of WAHA
|
||||
# https://waha.devlike.pro/docs/how-to/engines/#versions
|
||||
# image: devlikeapro/waha-plus:latest-2024.7.6 # 'latest' is not mistaken here
|
||||
#
|
||||
# Multiple images are available
|
||||
# https://waha.devlike.pro/docs/how-to/engines/#docker-images
|
||||
image: devlikeapro/waha-plus:chrome
|
||||
container_name: waha-plus
|
||||
# image: devlikeapro/waha-plus:chrome
|
||||
#
|
||||
|
||||
# The first port can be any free port you want
|
||||
# The second port must be the same as WHATSAPP_API_PORT or 3000 by default
|
||||
# Consider using the same port for simplicity
|
||||
ports:
|
||||
- '3000:3000/tcp'
|
||||
# NOTE: if you're using HTTP, you can use the following port mapping
|
||||
#- '80:3000/tcp'
|
||||
# NOTE: if you're using HTTPS, you can use the following port mapping
|
||||
#- '443:3000/tcp'
|
||||
|
||||
volumes:
|
||||
# Store sessions in the .sessions folder (comment it if you're using MongoDB)
|
||||
# Doesn't work with WAHA Core
|
||||
- './.sessions:/app/.sessions'
|
||||
|
||||
# https://waha.devlike.pro/docs/how-to/storages/#save-media-files-between-the-container-restarts
|
||||
- './.media:/app/.media'
|
||||
|
||||
# If you're using certbot for HTTPS, you can use the following volume mapping
|
||||
# https://waha.devlike.pro/blog/setting-up-https-for-waha/#lets-encrypt-certbot--waha
|
||||
# - '/etc/letsencrypt:/etc/letsencrypt'
|
||||
|
||||
# WAHA Configuration
|
||||
# https://waha.devlike.pro/docs/how-to/config/
|
||||
environment:
|
||||
# Environment variables from https://waha.devlike.pro/docs/how-to/config/
|
||||
# - WHATSAPP_RESTART_ALL_SESSIONS=True
|
||||
# - WHATSAPP_HOOK_URL=https://webhook.site/11111111-1111-1111-1111-11111111
|
||||
# - WHATSAPP_HOOK_EVENTS=message
|
||||
# - WHATSAPP_API_HOSTNAME=localhost
|
||||
- WHATSAPP_DEFAULT_ENGINE=WEBJS
|
||||
#
|
||||
# Common
|
||||
#
|
||||
# If you know the right URL, you can use it here.
|
||||
# Use any port in WHATSAPP_API_PORT and domain in WHATSAPP_API_HOSTNAME
|
||||
# https://waha.devlike.pro/docs/how-to/config/#common
|
||||
#- WHATSAPP_API_SCHEMA=http
|
||||
#- WHATSAPP_API_PORT=3000
|
||||
#- WHATSAPP_API_HOSTNAME=localhost
|
||||
|
||||
#
|
||||
# Security
|
||||
#
|
||||
# https://waha.devlike.pro/docs/how-to/security/
|
||||
#
|
||||
- WHATSAPP_API_KEY=321
|
||||
# Username and password for Swagger
|
||||
- WHATSAPP_SWAGGER_USERNAME=swagger
|
||||
- WHATSAPP_SWAGGER_PASSWORD=admin
|
||||
# Username and password for WAHA Dashboard
|
||||
# you can use the same username and password for both Swagger and Dashboard
|
||||
# Default - waha / waha
|
||||
- WAHA_DASHBOARD_USERNAME=waha
|
||||
- WAHA_DASHBOARD_PASSWORD=waha
|
||||
# Engine specific environment variables
|
||||
# - WAHA_WEBJS_WEB_VERSION=2.2412.54-videofix
|
||||
restart: always
|
||||
#- WHATSAPP_SWAGGER_USERNAME=admin
|
||||
#- WHATSAPP_SWAGGER_PASSWORD=admin
|
||||
|
||||
waha-plus--mongodb:
|
||||
# Check available Docker images:
|
||||
# https://waha.devlike.pro/docs/how-to/engines/#docker-images
|
||||
image: devlikeapro/waha-plus:chrome
|
||||
container_name: waha-plus--mongodb
|
||||
ports:
|
||||
- '3000:3000/tcp'
|
||||
volumes:
|
||||
- './.sessions:/app/.sessions'
|
||||
environment:
|
||||
# Environment variables from https://waha.devlike.pro/docs/how-to/config/
|
||||
# - WHATSAPP_RESTART_ALL_SESSIONS=True
|
||||
# - WHATSAPP_HOOK_URL=https://webhook.site/11111111-1111-1111-1111-11111111
|
||||
# - WHATSAPP_HOOK_EVENTS=message
|
||||
# - WHATSAPP_API_KEY=321
|
||||
# - WHATSAPP_SWAGGER_USERNAME=admin
|
||||
# - WHATSAPP_SWAGGER_PASSWORD=123
|
||||
# - WAHA_DASHBOARD_USERNAME=admin
|
||||
# - WAHA_DASHBOARD_PASSWORD=123
|
||||
# - WHATSAPP_API_HOSTNAME=localhost
|
||||
#
|
||||
# Dashboard
|
||||
#
|
||||
# https://waha.devlike.pro/docs/how-to/waha-dashboard/#configuration
|
||||
# Enabled by default
|
||||
#- WAHA_DASHBOARD_ENABLED=true
|
||||
#- WAHA_DASHBOARD_USERNAME=waha
|
||||
#- WAHA_DASHBOARD_PASSWORD=waha
|
||||
|
||||
#
|
||||
# Logging
|
||||
# https://waha.devlike.pro/docs/how-to/observability/#logging
|
||||
#
|
||||
- WAHA_LOG_FORMAT=PRETTY
|
||||
# Set log format to JSON if you consume logs with log management systems
|
||||
#- WAHA_LOG_FORMAT=JSON
|
||||
- WAHA_LOG_LEVEL=info
|
||||
# If you want to see more logs, you can set the log level to debug
|
||||
#- WAHA_LOG_LEVEL=debug
|
||||
# DEBUG=1 # shortcut for setting log level to debug
|
||||
|
||||
#
|
||||
# Engine
|
||||
#
|
||||
# Choose the right engine for your needs (WEBJS by default)
|
||||
# https://waha.devlike.pro/docs/how-to/engines/
|
||||
- WHATSAPP_DEFAULT_ENGINE=WEBJS
|
||||
- WHATSAPP_SESSIONS_MONGO_URL=mongodb://mongodb:27017
|
||||
restart: always
|
||||
# Use NOWEB engine for the fastest performance
|
||||
#- WHATSAPP_DEFAULT_ENGINE=NOWEB
|
||||
|
||||
# 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
|
||||
#
|
||||
# Sessions
|
||||
#
|
||||
# https://waha.devlike.pro/docs/how-to/config/#sessions
|
||||
# Restart all sessions when container starts
|
||||
- WHATSAPP_RESTART_ALL_SESSIONS=True
|
||||
# Do not print QR codes in logs
|
||||
- WAHA_PRINT_QR=False
|
||||
#
|
||||
# OR you can specify sessions by name, if you have handful amount of sessions
|
||||
#- WHATSAPP_START_SESSION=session1,session2
|
||||
# Do not use both WHATSAPP_RESTART_ALL_SESSIONS and WHATSAPP_START_SESSION parameters
|
||||
#
|
||||
|
||||
# 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
|
||||
#
|
||||
# Swagger
|
||||
#
|
||||
# https://waha.devlike.pro/docs/how-to/swagger/
|
||||
#- WHATSAPP_SWAGGER_USERNAME=admin
|
||||
#- WHATSAPP_SWAGGER_PASSWORD=admin
|
||||
# Disable Swagger
|
||||
#- WHATSAPP_SWAGGER_ENABLED=false
|
||||
# Enable advanced Swagger configuration
|
||||
#- WHATSAPP_SWAGGER_CONFIG_ADVANCED=true
|
||||
#
|
||||
# Swagger White Label customization
|
||||
# https://waha.devlike.pro/docs/how-to/swagger/#white-label
|
||||
#- WHATSAPP_SWAGGER_TITLE=WAHA API
|
||||
#
|
||||
|
||||
#
|
||||
# Global Proxy Settings
|
||||
# https://waha.devlike.pro/docs/how-to/config/#global-proxy-configuration
|
||||
# You can also set proxy settings for each session later when starting it
|
||||
# https://waha.devlike.pro/docs/how-to/sessions/#configure-proxy
|
||||
#- WHATSAPP_PROXY_SERVER=proxy.example.com:3128
|
||||
#-WHATSAPP_PROXY_SERVER_USERNAME=user
|
||||
#-WHATSAPP_PROXY_SERVER_PASSWORD=pass
|
||||
#
|
||||
|
||||
#
|
||||
# HTTPS Configuration
|
||||
#
|
||||
# https://waha.devlike.pro/docs/how-to/config/#https
|
||||
# Consider using certbot for HTTPS
|
||||
# https://waha.devlike.pro/blog/setting-up-https-for-waha/#lets-encrypt-certbot--waha
|
||||
#- WAHA_HTTPS_ENABLED=true
|
||||
#- WAHA_HTTPS_PATH_KEY=/etc/letsencrypt/live/waha.example.pro/privkey.pem
|
||||
#- WAHA_HTTPS_PATH_CERT=/etc/letsencrypt/live/waha.example.pro/cert.pem \
|
||||
#- WAHA_HTTPS_PATH_CA=/etc/letsencrypt/live/waha.example.pro/chain.pem
|
||||
#- WHATSAPP_API_SCHEMA=https
|
||||
#- WHATSAPP_API_PORT=3000
|
||||
#- WHATSAPP_API_HOSTNAME=waha.example.pro
|
||||
#
|
||||
|
||||
#
|
||||
# Media Configuration
|
||||
#
|
||||
# https://waha.devlike.pro/docs/how-to/config/#files
|
||||
#
|
||||
# Disable media (images, videos, files) download for incoming messages
|
||||
#- WHATSAPP_DOWNLOAD_MEDIA=false
|
||||
#
|
||||
# Download only specific media types
|
||||
#- WHATSAPP_FILES_MIMETYPES=image/jpeg,image/png
|
||||
#
|
||||
|
||||
# Media - Local Storage
|
||||
# https://waha.devlike.pro/docs/how-to/storages/#media---local
|
||||
#
|
||||
# Set to 0 to keep media files forever
|
||||
# https://waha.devlike.pro/docs/how-to/storages/#save-media-files-between-the-container-restarts
|
||||
- WAHA_MEDIA_STORAGE=LOCAL
|
||||
- WHATSAPP_FILES_LIFETIME=0
|
||||
- WHATSAPP_FILES_FOLDER=/app/.media
|
||||
# Keep media files for 180 seconds (3 minutes)
|
||||
#- WHATSAPP_FILES_LIFETIME=180
|
||||
#
|
||||
|
||||
# Media - S3 Storage
|
||||
# https://waha.devlike.pro/docs/how-to/storages/#media---s3
|
||||
#- WAHA_MEDIA_STORAGE=S3
|
||||
#- WAHA_S3_REGION=eu-west-2
|
||||
#- WAHA_S3_BUCKET=waha
|
||||
#- WAHA_S3_ACCESS_KEY_ID=minioadmin
|
||||
#- WAHA_S3_SECRET_ACCESS_KEY=minioadmin
|
||||
#- WAHA_S3_ENDPOINT=http://minio:9000 # Not required if you're using AWS S3
|
||||
#- WAHA_S3_FORCE_PATH_STYLE=True # Required for Minio
|
||||
#- WAHA_S3_PROXY_FILES=True # Required for docker-compose setup
|
||||
|
||||
#
|
||||
# Global Webhooks
|
||||
#
|
||||
# https://waha.devlike.pro/docs/how-to/config/#webhooks
|
||||
# https://waha.devlike.pro/docs/how-to/webhooks/
|
||||
#- WHATSAPP_HOOK_URL=https://webhook.site/11111111-1111-1111-1111-11111111
|
||||
#- WHATSAPP_HOOK_EVENTS=session.status,message,message.reaction
|
||||
#
|
||||
|
||||
#
|
||||
# MongoDB Configuration
|
||||
# https://waha.devlike.pro/docs/how-to/storages/#sessions---mongodb
|
||||
#- WHATSAPP_SESSIONS_MONGO_URL=mongodb://mongodb:27017
|
||||
#
|
||||
|
||||
# NOTE: Only if you're using MongoDB
|
||||
# https://waha.devlike.pro/docs/how-to/storages/#sessions---mongodb
|
||||
# Comment this block if you're using MongoDB
|
||||
mongodb:
|
||||
image: mongo
|
||||
container_name: mongodb
|
||||
@@ -111,5 +219,25 @@ services:
|
||||
- mongodb_data:/data/db
|
||||
restart: always
|
||||
|
||||
# NOTE: Only if you're using S3 to save media files
|
||||
# https://waha.devlike.pro/docs/how-to/storages/#media---s3
|
||||
# Comment this block if you're using AWS S3
|
||||
minio:
|
||||
image: quay.io/minio/minio
|
||||
container_name: minio
|
||||
restart: always
|
||||
ports:
|
||||
- '9000:9000'
|
||||
- '9001:9001'
|
||||
environment:
|
||||
MINIO_REGION: 'eu-west-2'
|
||||
MINIO_ROOT_USER: 'minioadmin'
|
||||
MINIO_ROOT_PASSWORD: 'minioadmin'
|
||||
volumes:
|
||||
- minio_data:/data
|
||||
command: server /data --console-address ":9001"
|
||||
|
||||
volumes:
|
||||
# NOTE: Only if you're using MongoDB
|
||||
mongodb_data: {}
|
||||
minio_data: {}
|
||||
Executable
+11
@@ -0,0 +1,11 @@
|
||||
#!/bin/sh
|
||||
|
||||
# Calculate UV_THREADPOOL_SIZE based on number of CPUs
|
||||
cpus=$(node -e "const os = require('os'); console.log(os.cpus().length);")
|
||||
uv_threadpool_size=$(($cpus * 2))
|
||||
|
||||
# Set UV_THREADPOOL_SIZE as an environment variable
|
||||
export UV_THREADPOOL_SIZE="${UV_THREADPOOL_SIZE:-$uv_threadpool_size}"
|
||||
|
||||
# Start your application using yarn
|
||||
yarn start:prod
|
||||
Binary file not shown.
@@ -50,8 +50,8 @@ docker run -it --rm --network=host -e WHATSAPP_HOOK_URL=http://localhost:5000/bo
|
||||
|
||||
2. Open Swagger API in the browser http://localhost:3000/
|
||||
3. Start session and scan QR code in swagger
|
||||
1. Find `POST /api/session/start` request press **Try it out** and
|
||||
**Execute** it with `default` session name
|
||||
1. Find `POST /api/sessions/` request press **Try it out** and **Execute** it
|
||||
with `default` session name
|
||||
2. Find `GET /api/screenshot` and execute it - it must show QR code
|
||||
3. Scan QR code on your mobile WhatsApp application (that installed on your
|
||||
phone)
|
||||
|
||||
+10
-2
@@ -15,6 +15,7 @@
|
||||
"start:prod": "node dist/main",
|
||||
"start:prod-exit": "node dist/main",
|
||||
"lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix --max-warnings=0",
|
||||
"lint-cache": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix --cache --max-warnings=0",
|
||||
"test": "jest",
|
||||
"test:watch": "jest --watch",
|
||||
"test:cov": "jest --coverage",
|
||||
@@ -22,11 +23,13 @@
|
||||
"test:e2e": "jest --config ./test/jest-e2e.json"
|
||||
},
|
||||
"dependencies": {
|
||||
"@adiwajshing/baileys": "github:devlikeapro/Baileys#fork-master-channels",
|
||||
"@adiwajshing/baileys": "github:devlikeapro/Baileys#fork-master-channels-2",
|
||||
"@adiwajshing/keyed-db": "^0.2.4",
|
||||
"@aws-sdk/client-s3": "^3.633.0",
|
||||
"@aws-sdk/s3-request-presigner": "^3.633.0",
|
||||
"@nestjs/axios": "^3.0.2",
|
||||
"@nestjs/common": "^9.0.9",
|
||||
"@nestjs/config": "^0.5.0",
|
||||
"@nestjs/config": "^3.2.3",
|
||||
"@nestjs/core": "^9.0.9",
|
||||
"@nestjs/passport": "^9.0.0",
|
||||
"@nestjs/platform-express": "^9.0.9",
|
||||
@@ -38,6 +41,7 @@
|
||||
"@types/better-sqlite3": "^7.6.10",
|
||||
"@types/lodash": "^4.14.194",
|
||||
"@types/ws": "^8.5.4",
|
||||
"adm-zip": "0.5.10",
|
||||
"async-lock": "^1.4.1",
|
||||
"better-sqlite3": "9.6.0",
|
||||
"check-disk-space": "^3.4.0",
|
||||
@@ -47,7 +51,9 @@
|
||||
"del": "^6.0.0",
|
||||
"express-basic-auth": "^1.2.1",
|
||||
"file-type": "16.5.4",
|
||||
"fs-extra": "^11.2.0",
|
||||
"https-proxy-agent": "^7.0.0",
|
||||
"joi": "^17.13.3",
|
||||
"knex": "^3.1.0",
|
||||
"libphonenumber-js": "^1.10.36",
|
||||
"link-preview-js": "^3.0.4",
|
||||
@@ -59,6 +65,8 @@
|
||||
"passport-headerapikey": "^1.2.2",
|
||||
"pino-http": "^10.2.0",
|
||||
"pino-pretty": "^11.2.1",
|
||||
"pretty-bytes": "5.6.0",
|
||||
"promise-retry": "^2.0.1",
|
||||
"puppeteer": "^21.0.0",
|
||||
"qrcode": "^1.5.1",
|
||||
"qrcode-terminal": "^0.12.0",
|
||||
|
||||
Executable
+23
@@ -0,0 +1,23 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
echo "Starting WAHA..."
|
||||
|
||||
docker run -d \
|
||||
--restart=always \
|
||||
--name waha \
|
||||
-p 3000:3000/tcp \
|
||||
-v ./.sessions:/app/.sessions \
|
||||
-v ./.media:/app/.media \
|
||||
--env WHATSAPP_API_KEY=321 \
|
||||
--env WAHA_DASHBOARD_USERNAME=waha \
|
||||
--env WAHA_DASHBOARD_PASSWORD=waha \
|
||||
--env WAHA_LOG_FORMAT=PRETTY \
|
||||
--env WAHA_LOG_LEVEL=info \
|
||||
--env WHATSAPP_DEFAULT_ENGINE=WEBJS \
|
||||
--env WHATSAPP_RESTART_ALL_SESSIONS=True \
|
||||
--env WAHA_PRINT_QR=False \
|
||||
--env WHATSAPP_FILES_LIFETIME=0 \
|
||||
--env WHATSAPP_FILES_FOLDER=/app/.media \
|
||||
devlikeapro/waha-plus:latest
|
||||
|
||||
echo "WAHA started."
|
||||
Executable
+21
@@ -0,0 +1,21 @@
|
||||
set -e
|
||||
echo "Updating WAHA..."
|
||||
echo "Logging in to Docker..."
|
||||
docker login -u devlikeapro # -p {KEY} - you can hard code key here https://portal.devlike.pro/
|
||||
|
||||
echo "Pulling the latest image..."
|
||||
docker pull devlikeapro/waha-plus
|
||||
|
||||
echo "Logging out of Docker..."
|
||||
docker logout
|
||||
|
||||
# Just pull if you're using WAHA Core
|
||||
# docker pull devlikeapro/waha
|
||||
|
||||
echo "Stopping the current container..."
|
||||
docker stop waha || true
|
||||
echo "Removing the current container..."
|
||||
docker rm waha || true
|
||||
|
||||
echo "Running WAHA..."
|
||||
sh ./waha-run.sh
|
||||
@@ -8,9 +8,15 @@ import {
|
||||
} from '@nestjs/common';
|
||||
import { UnprocessableEntityException } from '@nestjs/common/exceptions/unprocessable-entity.exception';
|
||||
import { ApiOperation, ApiSecurity, ApiTags } from '@nestjs/swagger';
|
||||
import { ApiFileAcceptHeader } from '@waha/nestjs/ApiFileAcceptHeader';
|
||||
import {
|
||||
SessionApiParam,
|
||||
SessionParam,
|
||||
} from '@waha/nestjs/params/SessionApiParam';
|
||||
|
||||
import { SessionManager } from '../core/abc/manager.abc';
|
||||
import { WhatsappSession } from '../core/abc/session.abc';
|
||||
import { BufferResponseInterceptor } from '../nestjs/BufferResponseInterceptor';
|
||||
import {
|
||||
CaptchaBody,
|
||||
OTPRequest,
|
||||
@@ -21,18 +27,16 @@ import {
|
||||
} from '../structures/auth.dto';
|
||||
import { WAHASessionStatus } from '../structures/enums.dto';
|
||||
import { Base64File } from '../structures/files.dto';
|
||||
import { BufferResponseInterceptor } from './BufferResponseInterceptor';
|
||||
import { ApiFileAcceptHeader, SessionApiParam, SessionParam } from './helpers';
|
||||
|
||||
@ApiSecurity('api_key')
|
||||
@Controller('api/:session/auth')
|
||||
@ApiTags('auth')
|
||||
@ApiTags('🔑 Auth')
|
||||
class AuthController {
|
||||
constructor(private manager: SessionManager) {}
|
||||
|
||||
@Get('qr')
|
||||
@ApiOperation({
|
||||
summary: 'Get QR code for pairing WhatsApp Web.',
|
||||
summary: 'Get QR code for pairing WhatsApp API.',
|
||||
})
|
||||
@SessionApiParam
|
||||
@ApiFileAcceptHeader(Base64File, QRCodeValue)
|
||||
|
||||
@@ -8,6 +8,10 @@ import {
|
||||
Query,
|
||||
} from '@nestjs/common';
|
||||
import { ApiOperation, ApiSecurity, ApiTags } from '@nestjs/swagger';
|
||||
import {
|
||||
SessionApiParam,
|
||||
SessionParam,
|
||||
} from '@waha/nestjs/params/SessionApiParam';
|
||||
import {
|
||||
Channel,
|
||||
CreateChannelRequest,
|
||||
@@ -18,11 +22,10 @@ import {
|
||||
|
||||
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')
|
||||
@ApiTags('📢 Channels')
|
||||
export class ChannelsController {
|
||||
constructor(private manager: SessionManager) {}
|
||||
|
||||
@@ -58,7 +61,10 @@ export class ChannelsController {
|
||||
@SessionApiParam
|
||||
@NewsletterIdOrInviteCodeApiParam
|
||||
@ApiOperation({
|
||||
summary: 'Get the channel info - either by id @newsletter OR invite code.',
|
||||
summary: 'Get the channel info',
|
||||
description:
|
||||
'You can use either id (123@newsletter) ' +
|
||||
'OR invite code (https://www.whatsapp.com/channel/123)',
|
||||
})
|
||||
get(
|
||||
@SessionParam session: WhatsappSession,
|
||||
|
||||
@@ -4,28 +4,29 @@ import {
|
||||
Delete,
|
||||
Get,
|
||||
Param,
|
||||
Post,
|
||||
Put,
|
||||
Query,
|
||||
UsePipes,
|
||||
ValidationPipe,
|
||||
} from '@nestjs/common';
|
||||
import { ApiOperation, ApiSecurity, ApiTags } from '@nestjs/swagger';
|
||||
import { ChatIdApiParam } from '@waha/nestjs/params/ChatIdApiParam';
|
||||
import { MessageIdApiParam } from '@waha/nestjs/params/MessageIdApiParam';
|
||||
import {
|
||||
SessionApiParam,
|
||||
SessionParam,
|
||||
} from '@waha/nestjs/params/SessionApiParam';
|
||||
|
||||
import { SessionManager } from '../core/abc/manager.abc';
|
||||
import { WhatsappSession } from '../core/abc/session.abc';
|
||||
import { parseBool } from '../helpers';
|
||||
import { GetChatMessagesQuery, GetChatsQuery } from '../structures/chats.dto';
|
||||
import { EditMessageRequest } from '../structures/chatting.dto';
|
||||
import {
|
||||
ChatIdApiParam,
|
||||
MessageIdApiParam,
|
||||
SessionApiParam,
|
||||
SessionParam,
|
||||
} from './helpers';
|
||||
|
||||
@ApiSecurity('api_key')
|
||||
@Controller('api/:session/chats')
|
||||
@ApiTags('chats')
|
||||
@ApiTags('💬 Chats')
|
||||
@UsePipes(new ValidationPipe({ transform: true }))
|
||||
class ChatsController {
|
||||
constructor(private manager: SessionManager) {}
|
||||
@@ -43,6 +44,7 @@ class ChatsController {
|
||||
@Delete(':chatId')
|
||||
@SessionApiParam
|
||||
@ApiOperation({ summary: 'Deletes the chat' })
|
||||
@ChatIdApiParam
|
||||
deleteChat(
|
||||
@SessionParam session: WhatsappSession,
|
||||
@Param('chatId') chatId: string,
|
||||
@@ -53,6 +55,7 @@ class ChatsController {
|
||||
@Get(':chatId/messages')
|
||||
@SessionApiParam
|
||||
@ApiOperation({ summary: 'Gets messages in the chat' })
|
||||
@ChatIdApiParam
|
||||
getChatMessages(
|
||||
@Query() query: GetChatMessagesQuery,
|
||||
@SessionParam session: WhatsappSession,
|
||||
@@ -65,6 +68,7 @@ class ChatsController {
|
||||
@Delete(':chatId/messages')
|
||||
@SessionApiParam
|
||||
@ApiOperation({ summary: 'Clears all messages from the chat' })
|
||||
@ChatIdApiParam
|
||||
clearMessages(
|
||||
@SessionParam session: WhatsappSession,
|
||||
@Param('chatId') chatId: string,
|
||||
@@ -98,6 +102,28 @@ class ChatsController {
|
||||
) {
|
||||
return session.editMessage(chatId, messageId, body);
|
||||
}
|
||||
|
||||
@Post(':chatId/archive')
|
||||
@SessionApiParam
|
||||
@ChatIdApiParam
|
||||
@ApiOperation({ summary: 'Archive the chat' })
|
||||
archiveChat(
|
||||
@SessionParam session: WhatsappSession,
|
||||
@Param('chatId') chatId: string,
|
||||
) {
|
||||
return session.chatsArchiveChat(chatId);
|
||||
}
|
||||
|
||||
@Post(':chatId/unarchive')
|
||||
@SessionApiParam
|
||||
@ChatIdApiParam
|
||||
@ApiOperation({ summary: 'Unarchive the chat' })
|
||||
unarchiveChat(
|
||||
@SessionParam session: WhatsappSession,
|
||||
@Param('chatId') chatId: string,
|
||||
) {
|
||||
return session.chatsUnarchiveChat(chatId);
|
||||
}
|
||||
}
|
||||
|
||||
export { ChatsController };
|
||||
@@ -26,35 +26,10 @@ import { WAMessage } from '../structures/responses.dto';
|
||||
|
||||
@ApiSecurity('api_key')
|
||||
@Controller('api')
|
||||
@ApiTags('chatting')
|
||||
@ApiTags('📤 Chatting')
|
||||
export class ChattingController {
|
||||
constructor(private manager: SessionManager) {}
|
||||
|
||||
@Get('/checkNumberStatus')
|
||||
@ApiOperation({ summary: 'Check number status' })
|
||||
async checkNumberStatus(
|
||||
@Query() request: CheckNumberStatusQuery,
|
||||
): Promise<WANumberExistResult> {
|
||||
const whatsapp = this.manager.getSession(request.session);
|
||||
return whatsapp.checkNumberStatus(request);
|
||||
}
|
||||
|
||||
@Post('/sendContactVcard')
|
||||
sendContactVcard(@Body() request: MessageContactVcardRequest) {
|
||||
const whatsapp = this.manager.getSession(request.session);
|
||||
return whatsapp.sendContactVCard(request);
|
||||
}
|
||||
|
||||
@Get('/sendText')
|
||||
@ApiOperation({ summary: 'Send a text message', deprecated: true })
|
||||
sendTextGet(@Query() query: MessageTextQuery) {
|
||||
const whatsapp = this.manager.getSession(query.session);
|
||||
const msg = new MessageTextRequest();
|
||||
msg.chatId = query.phone;
|
||||
msg.text = query.text;
|
||||
return whatsapp.sendText(msg);
|
||||
}
|
||||
|
||||
@Post('/sendText')
|
||||
@ApiOperation({ summary: 'Send a text message' })
|
||||
sendText(@Body() request: MessageTextRequest): Promise<WAMessage> {
|
||||
@@ -62,31 +37,11 @@ export class ChattingController {
|
||||
return whatsapp.sendText(request);
|
||||
}
|
||||
|
||||
@Post('/sendPoll')
|
||||
@ApiOperation({
|
||||
summary: 'Send a poll with options. You can use it as buttons replace',
|
||||
})
|
||||
sendPoll(@Body() request: MessagePollRequest) {
|
||||
const whatsapp = this.manager.getSession(request.session);
|
||||
return whatsapp.sendPoll(request);
|
||||
}
|
||||
|
||||
@Post('/sendLocation')
|
||||
sendLocation(@Body() request: MessageLocationRequest) {
|
||||
const whatsapp = this.manager.getSession(request.session);
|
||||
return whatsapp.sendLocation(request);
|
||||
}
|
||||
|
||||
@Post('/sendLinkPreview')
|
||||
sendLinkPreview(@Body() request: MessageLinkPreviewRequest) {
|
||||
const whatsapp = this.manager.getSession(request.session);
|
||||
return whatsapp.sendLinkPreview(request);
|
||||
}
|
||||
|
||||
@Post('/sendImage')
|
||||
@ApiOperation({
|
||||
summary:
|
||||
'Send an image. Either from an URL or base64 data - look at the request schemas for details.',
|
||||
summary: 'Send an image',
|
||||
description:
|
||||
'Either from an URL or base64 data - look at the request schemas for details.',
|
||||
})
|
||||
sendImage(@Body() request: MessageImageRequest) {
|
||||
const whatsapp = this.manager.getSession(request.session);
|
||||
@@ -95,8 +50,9 @@ export class ChattingController {
|
||||
|
||||
@Post('/sendFile')
|
||||
@ApiOperation({
|
||||
summary:
|
||||
'Send a file. Either from an URL or base64 data - look at the request schemas for details.',
|
||||
summary: 'Send a file',
|
||||
description:
|
||||
'Either from an URL or base64 data - look at the request schemas for details.',
|
||||
})
|
||||
sendFile(@Body() request: MessageFileRequest) {
|
||||
const whatsapp = this.manager.getSession(request.session);
|
||||
@@ -105,8 +61,9 @@ export class ChattingController {
|
||||
|
||||
@Post('/sendVoice')
|
||||
@ApiOperation({
|
||||
summary:
|
||||
'Send an voice message. Either from an URL or base64 data - look at the request schemas for details.',
|
||||
summary: 'Send an voice message',
|
||||
description:
|
||||
'Either from an URL or base64 data - look at the request schemas for details.',
|
||||
})
|
||||
sendVoice(@Body() request: MessageVoiceRequest) {
|
||||
const whatsapp = this.manager.getSession(request.session);
|
||||
@@ -115,21 +72,15 @@ export class ChattingController {
|
||||
|
||||
@Post('/sendVideo')
|
||||
@ApiOperation({
|
||||
summary:
|
||||
'Send a video. Either from an URL or base64 data - look at the request schemas for details.',
|
||||
summary: 'Send a video',
|
||||
description:
|
||||
'Either from an URL or base64 data - look at the request schemas for details.',
|
||||
})
|
||||
sendVideo(@Body() request: MessageVideoRequest) {
|
||||
const whatsapp = this.manager.getSession(request.session);
|
||||
return whatsapp.sendVideo(request);
|
||||
}
|
||||
|
||||
@Post('/reply')
|
||||
@ApiOperation({ summary: 'Reply to a text message' })
|
||||
reply(@Body() request: MessageReplyRequest) {
|
||||
const whatsapp = this.manager.getSession(request.session);
|
||||
return whatsapp.reply(request);
|
||||
}
|
||||
|
||||
@Post('/sendSeen')
|
||||
sendSeen(@Body() chat: SendSeenRequest) {
|
||||
const whatsapp = this.manager.getSession(chat.session);
|
||||
@@ -166,10 +117,72 @@ export class ChattingController {
|
||||
return;
|
||||
}
|
||||
|
||||
@Post('/sendPoll')
|
||||
@ApiOperation({
|
||||
summary: 'Send a poll with options',
|
||||
description: 'You can use it as buttons or list replacement',
|
||||
})
|
||||
sendPoll(@Body() request: MessagePollRequest) {
|
||||
const whatsapp = this.manager.getSession(request.session);
|
||||
return whatsapp.sendPoll(request);
|
||||
}
|
||||
|
||||
@Post('/sendLocation')
|
||||
sendLocation(@Body() request: MessageLocationRequest) {
|
||||
const whatsapp = this.manager.getSession(request.session);
|
||||
return whatsapp.sendLocation(request);
|
||||
}
|
||||
|
||||
@Post('/sendLinkPreview')
|
||||
sendLinkPreview(@Body() request: MessageLinkPreviewRequest) {
|
||||
const whatsapp = this.manager.getSession(request.session);
|
||||
return whatsapp.sendLinkPreview(request);
|
||||
}
|
||||
|
||||
@Get('/messages')
|
||||
@ApiOperation({ summary: 'Get messages in a chat' })
|
||||
getMessages(@Query() query: GetMessageQuery) {
|
||||
const whatsapp = this.manager.getSession(query.session);
|
||||
return whatsapp.getMessages(query);
|
||||
}
|
||||
|
||||
@Get('/sendText')
|
||||
@ApiOperation({ summary: 'Send a text message', deprecated: true })
|
||||
sendTextGet(@Query() query: MessageTextQuery) {
|
||||
const whatsapp = this.manager.getSession(query.session);
|
||||
const msg = new MessageTextRequest();
|
||||
msg.chatId = query.phone;
|
||||
msg.text = query.text;
|
||||
return whatsapp.sendText(msg);
|
||||
}
|
||||
|
||||
@Post('/sendContactVcard')
|
||||
sendContactVcard(@Body() request: MessageContactVcardRequest) {
|
||||
const whatsapp = this.manager.getSession(request.session);
|
||||
return whatsapp.sendContactVCard(request);
|
||||
}
|
||||
|
||||
@Get('/checkNumberStatus')
|
||||
@ApiOperation({
|
||||
summary: 'Check number status',
|
||||
description: 'DEPRECATED. Use "POST /contacts/check-exists" instead',
|
||||
deprecated: true,
|
||||
})
|
||||
async DEPRECATED_checkNumberStatus(
|
||||
@Query() request: CheckNumberStatusQuery,
|
||||
): Promise<WANumberExistResult> {
|
||||
const whatsapp = this.manager.getSession(request.session);
|
||||
return whatsapp.checkNumberStatus(request);
|
||||
}
|
||||
|
||||
@Post('/reply')
|
||||
@ApiOperation({
|
||||
summary:
|
||||
'DEPRECATED - you can set "reply_to" field when sending text, image, etc',
|
||||
deprecated: true,
|
||||
})
|
||||
reply(@Body() request: MessageReplyRequest) {
|
||||
const whatsapp = this.manager.getSession(request.session);
|
||||
return whatsapp.reply(request);
|
||||
}
|
||||
}
|
||||
@@ -11,14 +11,15 @@ import { ContactQuery, ContactRequest } from '../structures/contacts.dto';
|
||||
|
||||
@ApiSecurity('api_key')
|
||||
@Controller('api/contacts')
|
||||
@ApiTags('contacts')
|
||||
@ApiTags('👤 Contacts')
|
||||
export class ContactsController {
|
||||
constructor(private manager: SessionManager) {}
|
||||
|
||||
@Get('/')
|
||||
@ApiOperation({
|
||||
summary:
|
||||
'Get contact basic info. The method always return result, even if the phone number is not registered in WhatsApp. For that - use /check-exists endpoint below.',
|
||||
summary: 'Get contact basic info',
|
||||
description:
|
||||
'The method always return result, even if the phone number is not registered in WhatsApp. For that - use /contacts/check-exists endpoint below.',
|
||||
})
|
||||
get(@Query() query: ContactQuery) {
|
||||
const whatsapp = this.manager.getSession(query.session);
|
||||
@@ -43,8 +44,9 @@ export class ContactsController {
|
||||
|
||||
@Get('/about')
|
||||
@ApiOperation({
|
||||
summary:
|
||||
'Gets the Contact\'s current "about" info. Returns null if you don\'t have permission to read their status.',
|
||||
summary: 'Gets the Contact\'s "about" info',
|
||||
description:
|
||||
'Returns null if you do not have permission to read their status.',
|
||||
})
|
||||
getAbout(@Query() query: ContactQuery) {
|
||||
const whatsapp = this.manager.getSession(query.session);
|
||||
@@ -53,8 +55,9 @@ export class ContactsController {
|
||||
|
||||
@Get('/profile-picture')
|
||||
@ApiOperation({
|
||||
summary:
|
||||
"Returns the contact's profile picture URL, if privacy settings allow it.",
|
||||
summary: "Get contact's profile picture URL",
|
||||
description:
|
||||
'If privacy settings do not allow to get the picture, the method will return null.',
|
||||
})
|
||||
getProfilePicture(@Query() query: ContactQuery) {
|
||||
const whatsapp = this.manager.getSession(query.session);
|
||||
|
||||
@@ -8,6 +8,11 @@ import {
|
||||
Put,
|
||||
} from '@nestjs/common';
|
||||
import { ApiOperation, ApiSecurity, ApiTags } from '@nestjs/swagger';
|
||||
import { GroupIdApiParam } from '@waha/nestjs/params/ChatIdApiParam';
|
||||
import {
|
||||
SessionApiParam,
|
||||
SessionParam,
|
||||
} from '@waha/nestjs/params/SessionApiParam';
|
||||
|
||||
import { SessionManager } from '../core/abc/manager.abc';
|
||||
import { WhatsappSession } from '../core/abc/session.abc';
|
||||
@@ -18,11 +23,10 @@ import {
|
||||
SettingsSecurityChangeInfo,
|
||||
SubjectRequest,
|
||||
} from '../structures/groups.dto';
|
||||
import { SessionApiParam, SessionParam } from './helpers';
|
||||
|
||||
@ApiSecurity('api_key')
|
||||
@Controller('api/:session/groups')
|
||||
@ApiTags('groups')
|
||||
@ApiTags('👥 Groups')
|
||||
export class GroupsController {
|
||||
constructor(private manager: SessionManager) {}
|
||||
|
||||
@@ -44,6 +48,7 @@ export class GroupsController {
|
||||
}
|
||||
|
||||
@Get(':id')
|
||||
@GroupIdApiParam
|
||||
@SessionApiParam
|
||||
@ApiOperation({ summary: 'Get the group.' })
|
||||
getGroup(@SessionParam session: WhatsappSession, @Param('id') id: string) {
|
||||
@@ -52,9 +57,11 @@ export class GroupsController {
|
||||
|
||||
@Put(':id/settings/security/info-admin-only')
|
||||
@SessionApiParam
|
||||
@GroupIdApiParam
|
||||
@ApiOperation({
|
||||
summary:
|
||||
'Updates the group settings to only allow admins to edit group info (title, description, photo).',
|
||||
summary: 'Updates the group "info admin only" settings.',
|
||||
description:
|
||||
'You can allow only admins to edit group info (title, description, photo).',
|
||||
})
|
||||
setInfoAdminOnly(
|
||||
@SessionParam session: WhatsappSession,
|
||||
@@ -66,9 +73,11 @@ export class GroupsController {
|
||||
|
||||
@Get(':id/settings/security/info-admin-only')
|
||||
@SessionApiParam
|
||||
@GroupIdApiParam
|
||||
@ApiOperation({
|
||||
summary:
|
||||
'Gets the group settings to only allow admins to edit group info (title, description, photo).',
|
||||
summary: "Get the group's 'info admin only' settings.",
|
||||
description:
|
||||
'You can allow only admins to edit group info (title, description, photo).',
|
||||
})
|
||||
getInfoAdminOnly(
|
||||
@SessionParam session: WhatsappSession,
|
||||
@@ -79,8 +88,10 @@ export class GroupsController {
|
||||
|
||||
@Put(':id/settings/security/messages-admin-only')
|
||||
@SessionApiParam
|
||||
@GroupIdApiParam
|
||||
@ApiOperation({
|
||||
summary:
|
||||
summary: 'Update settings - who can send messages',
|
||||
description:
|
||||
'Updates the group settings to only allow admins to send messages.',
|
||||
})
|
||||
setMessagesAdminOnly(
|
||||
@@ -93,8 +104,10 @@ export class GroupsController {
|
||||
|
||||
@Get(':id/settings/security/messages-admin-only')
|
||||
@SessionApiParam
|
||||
@GroupIdApiParam
|
||||
@ApiOperation({
|
||||
summary: 'Gets the group settings to only allow admins to send messages.',
|
||||
summary: 'Get settings - who can send messages',
|
||||
description: 'The group settings to only allow admins to send messages.',
|
||||
})
|
||||
getMessagesAdminOnly(
|
||||
@SessionParam session: WhatsappSession,
|
||||
@@ -105,6 +118,7 @@ export class GroupsController {
|
||||
|
||||
@Delete(':id')
|
||||
@SessionApiParam
|
||||
@GroupIdApiParam
|
||||
@ApiOperation({ summary: 'Delete the group.' })
|
||||
deleteGroup(@SessionParam session: WhatsappSession, @Param('id') id: string) {
|
||||
return session.deleteGroup(id);
|
||||
@@ -112,6 +126,7 @@ export class GroupsController {
|
||||
|
||||
@Post(':id/leave')
|
||||
@SessionApiParam
|
||||
@GroupIdApiParam
|
||||
@ApiOperation({ summary: 'Leave the group.' })
|
||||
leaveGroup(@SessionParam session: WhatsappSession, @Param('id') id: string) {
|
||||
return session.leaveGroup(id);
|
||||
@@ -119,11 +134,12 @@ export class GroupsController {
|
||||
|
||||
@Put(':id/description')
|
||||
@ApiOperation({
|
||||
summary:
|
||||
'Updates the group description.\n' +
|
||||
'Returns true if the subject was properly updated. This can return false if the user does not have the necessary permissions.\n',
|
||||
summary: 'Updates the group description.',
|
||||
description:
|
||||
'Returns "true" if the subject was properly updated. This can return "false" if the user does not have the necessary permissions.',
|
||||
})
|
||||
@SessionApiParam
|
||||
@GroupIdApiParam
|
||||
setDescription(
|
||||
@SessionParam session: WhatsappSession,
|
||||
@Param('id') id: string,
|
||||
@@ -134,10 +150,11 @@ export class GroupsController {
|
||||
|
||||
@Put(':id/subject')
|
||||
@SessionApiParam
|
||||
@GroupIdApiParam
|
||||
@ApiOperation({
|
||||
summary:
|
||||
'Updates the group subject.\n' +
|
||||
'Returns true if the subject was properly updated. This can return false if the user does not have the necessary permissions.\n',
|
||||
summary: 'Updates the group subject',
|
||||
description:
|
||||
'Returns "true" if the subject was properly updated. This can return "false" if the user does not have the necessary permissions.',
|
||||
})
|
||||
setSubject(
|
||||
@SessionParam session: WhatsappSession,
|
||||
@@ -149,7 +166,8 @@ export class GroupsController {
|
||||
|
||||
@Get(':id/invite-code')
|
||||
@SessionApiParam
|
||||
@ApiOperation({ summary: 'Gets the invite code for a specific group.' })
|
||||
@GroupIdApiParam
|
||||
@ApiOperation({ summary: 'Gets the invite code for the group.' })
|
||||
getInviteCode(
|
||||
@SessionParam session: WhatsappSession,
|
||||
@Param('id') id: string,
|
||||
@@ -159,6 +177,7 @@ export class GroupsController {
|
||||
|
||||
@Post(':id/invite-code/revoke')
|
||||
@SessionApiParam
|
||||
@GroupIdApiParam
|
||||
@ApiOperation({
|
||||
summary:
|
||||
'Invalidates the current group invite code and generates a new one.',
|
||||
@@ -172,7 +191,8 @@ export class GroupsController {
|
||||
|
||||
@Get(':id/participants/')
|
||||
@SessionApiParam
|
||||
@ApiOperation({ summary: 'Get a list of participants by in the group.' })
|
||||
@GroupIdApiParam
|
||||
@ApiOperation({ summary: 'Get participants' })
|
||||
getParticipants(
|
||||
@SessionParam session: WhatsappSession,
|
||||
@Param('id') id: string,
|
||||
@@ -182,7 +202,8 @@ export class GroupsController {
|
||||
|
||||
@Post(':id/participants/add')
|
||||
@SessionApiParam
|
||||
@ApiOperation({ summary: 'Adds a list of participants by ID to the group.' })
|
||||
@GroupIdApiParam
|
||||
@ApiOperation({ summary: 'Add participants' })
|
||||
addParticipants(
|
||||
@SessionParam session: WhatsappSession,
|
||||
@Param('id') id: string,
|
||||
@@ -193,8 +214,9 @@ export class GroupsController {
|
||||
|
||||
@Post(':id/participants/remove')
|
||||
@SessionApiParam
|
||||
@GroupIdApiParam
|
||||
@ApiOperation({
|
||||
summary: 'Removes a list of participants by ID to the group.',
|
||||
summary: 'Remove participants',
|
||||
})
|
||||
removeParticipants(
|
||||
@SessionParam session: WhatsappSession,
|
||||
@@ -206,6 +228,7 @@ export class GroupsController {
|
||||
|
||||
@Post(':id/admin/promote')
|
||||
@SessionApiParam
|
||||
@GroupIdApiParam
|
||||
@ApiOperation({ summary: 'Promote participants to admin users.' })
|
||||
promoteToAdmin(
|
||||
@SessionParam session: WhatsappSession,
|
||||
@@ -217,7 +240,8 @@ export class GroupsController {
|
||||
|
||||
@Post(':id/admin/demote')
|
||||
@SessionApiParam
|
||||
@ApiOperation({ summary: 'Demotes participants by to regular users.' })
|
||||
@GroupIdApiParam
|
||||
@ApiOperation({ summary: 'Demotes participants to regular users.' })
|
||||
demoteToAdmin(
|
||||
@SessionParam session: WhatsappSession,
|
||||
@Param('id') id: string,
|
||||
|
||||
@@ -1,16 +1,22 @@
|
||||
import { Controller, Get } from '@nestjs/common';
|
||||
import { ApiTags } from '@nestjs/swagger';
|
||||
import { ApiOperation, ApiSecurity, ApiTags } from '@nestjs/swagger';
|
||||
import { HealthCheck } from '@nestjs/terminus';
|
||||
|
||||
import { WAHAHealthCheckService } from '../core/abc/WAHAHealthCheckService';
|
||||
|
||||
@ApiSecurity('api_key')
|
||||
@Controller('health')
|
||||
@ApiTags('other')
|
||||
@ApiTags('🔍 Observability')
|
||||
export class HealthController {
|
||||
constructor(private wahaHealth: WAHAHealthCheckService) {}
|
||||
|
||||
@Get()
|
||||
@HealthCheck()
|
||||
@ApiOperation({
|
||||
summary: 'Check the health of the server',
|
||||
description:
|
||||
"Perform all health checks and return the server's health status.",
|
||||
})
|
||||
async check() {
|
||||
return this.wahaHealth.check();
|
||||
}
|
||||
|
||||
@@ -1,96 +0,0 @@
|
||||
import {
|
||||
applyDecorators,
|
||||
Injectable,
|
||||
Param,
|
||||
PipeTransform,
|
||||
} from '@nestjs/common';
|
||||
import {
|
||||
ApiExtraModels,
|
||||
ApiParam,
|
||||
ApiResponse,
|
||||
getSchemaPath,
|
||||
} from '@nestjs/swagger';
|
||||
|
||||
import { SessionManager } from '../core/abc/manager.abc';
|
||||
import { WhatsappSession } from '../core/abc/session.abc';
|
||||
import { Base64File } from '../structures/files.dto';
|
||||
|
||||
/**
|
||||
* Get session name and return Whatsapp session back (if exists)
|
||||
* use it as
|
||||
@Param('session', SessionPipe) session: WhatsappSession,
|
||||
*/
|
||||
@Injectable()
|
||||
export class SessionPipe implements PipeTransform<WhatsappSession> {
|
||||
constructor(private manager: SessionManager) {}
|
||||
|
||||
async transform(value: any) {
|
||||
return this.manager.getSession(value);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Decorator for a method that uses SessionPipe above
|
||||
*/
|
||||
export const SessionApiParam = ApiParam({
|
||||
name: 'session',
|
||||
required: true,
|
||||
type: 'string',
|
||||
schema: {
|
||||
default: 'default',
|
||||
},
|
||||
description: 'WhatsApp session name',
|
||||
});
|
||||
|
||||
/**
|
||||
* Session param
|
||||
@SessionParam session: WhatsappSession,
|
||||
*/
|
||||
export const SessionParam = Param('session', SessionPipe);
|
||||
|
||||
export const ChatIdApiParam = ApiParam({
|
||||
name: 'chatId',
|
||||
required: true,
|
||||
type: 'string',
|
||||
description: 'Chat ID',
|
||||
example: '123456789@c.us',
|
||||
});
|
||||
|
||||
export const MessageIdApiParam = ApiParam({
|
||||
name: 'messageId',
|
||||
required: true,
|
||||
type: 'string',
|
||||
description: 'Message ID',
|
||||
example: 'true_123456789@c.us_BAE6A33293978B16',
|
||||
});
|
||||
|
||||
function getRefSchemaPaths(models) {
|
||||
return models.map((model) => {
|
||||
return { $ref: getSchemaPath(model) };
|
||||
});
|
||||
}
|
||||
|
||||
export function ApiFileAcceptHeader(...models) {
|
||||
models = models.length ? models : [Base64File];
|
||||
return applyDecorators(
|
||||
// Add extra models, otherwise it'll give a error
|
||||
// $ref not found
|
||||
ApiExtraModels(...models),
|
||||
ApiResponse({
|
||||
status: 200,
|
||||
content: {
|
||||
'image/png': {
|
||||
schema: {
|
||||
type: 'string',
|
||||
format: 'binary',
|
||||
},
|
||||
},
|
||||
'application/json': {
|
||||
schema: {
|
||||
oneOf: getRefSchemaPaths(models),
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
import { Body, Controller, Get, Param, Put } from '@nestjs/common';
|
||||
import { ApiOperation, ApiSecurity, ApiTags } from '@nestjs/swagger';
|
||||
import { WhatsappSession } from '@waha/core/abc/session.abc';
|
||||
import { ChatIdApiParam } from '@waha/nestjs/params/ChatIdApiParam';
|
||||
import {
|
||||
SessionApiParam,
|
||||
SessionParam,
|
||||
} from '@waha/nestjs/params/SessionApiParam';
|
||||
import { Label, SetLabelsRequest } from '@waha/structures/labels.dto';
|
||||
|
||||
import { SessionManager } from '../core/abc/manager.abc';
|
||||
|
||||
@ApiSecurity('api_key')
|
||||
@Controller('api/:session/labels')
|
||||
@ApiTags('🏷️ Labels')
|
||||
export class LabelsController {
|
||||
constructor(private manager: SessionManager) {}
|
||||
|
||||
@Get('/')
|
||||
@SessionApiParam
|
||||
@ApiOperation({ summary: 'Get all labels' })
|
||||
getAll(@SessionParam session: WhatsappSession): Promise<Label[]> {
|
||||
return session.getLabels();
|
||||
}
|
||||
|
||||
@Get('/chats/:chatId')
|
||||
@SessionApiParam
|
||||
@ChatIdApiParam
|
||||
@ApiOperation({ summary: 'Get labels for the chat' })
|
||||
getChatLabels(
|
||||
@SessionParam session: WhatsappSession,
|
||||
@Param('chatId') chatId: string,
|
||||
): Promise<Label[]> {
|
||||
return session.getChatLabels(chatId);
|
||||
}
|
||||
|
||||
@Put('/chats/:chatId')
|
||||
@SessionApiParam
|
||||
@ChatIdApiParam
|
||||
@ApiOperation({ summary: 'Save labels for the chat' })
|
||||
putChatLabels(
|
||||
@SessionParam session: WhatsappSession,
|
||||
@Param('chatId') chatId: string,
|
||||
@Body() request: SetLabelsRequest,
|
||||
) {
|
||||
return session.putLabelsToChat(chatId, request.labels);
|
||||
}
|
||||
|
||||
@Get('/:labelId/chats')
|
||||
@SessionApiParam
|
||||
@ApiOperation({ summary: 'Get chats by label' })
|
||||
getChatsByLabel(
|
||||
@SessionParam session: WhatsappSession,
|
||||
@Param('labelId') labelId: string,
|
||||
) {
|
||||
return session.getChatsByLabelId(labelId);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
import { Controller, Get } from '@nestjs/common';
|
||||
import { ApiOperation, ApiSecurity, ApiTags } from '@nestjs/swagger';
|
||||
import { PingResponse } from '@waha/structures/ping.dto';
|
||||
|
||||
@ApiSecurity('api_key')
|
||||
@Controller('ping')
|
||||
@ApiTags('🔍 Observability')
|
||||
export class PingController {
|
||||
@Get()
|
||||
@ApiOperation({
|
||||
summary: 'Ping the server',
|
||||
description: 'Check if the server is alive and responding to requests.',
|
||||
})
|
||||
ping(): PingResponse {
|
||||
return { message: 'pong' };
|
||||
}
|
||||
}
|
||||
@@ -7,6 +7,11 @@ import {
|
||||
Post,
|
||||
} from '@nestjs/common';
|
||||
import { ApiOperation, ApiSecurity, ApiTags } from '@nestjs/swagger';
|
||||
import { ChatIdApiParam } from '@waha/nestjs/params/ChatIdApiParam';
|
||||
import {
|
||||
SessionApiParam,
|
||||
SessionParam,
|
||||
} from '@waha/nestjs/params/SessionApiParam';
|
||||
|
||||
import { SessionManager } from '../core/abc/manager.abc';
|
||||
import { WhatsappSession } from '../core/abc/session.abc';
|
||||
@@ -15,11 +20,10 @@ import {
|
||||
WAHAChatPresences,
|
||||
WAHASessionPresence,
|
||||
} from '../structures/presence.dto';
|
||||
import { SessionApiParam, SessionParam } from './helpers';
|
||||
|
||||
@ApiSecurity('api_key')
|
||||
@Controller('api/:session/presence')
|
||||
@ApiTags('presence')
|
||||
@ApiTags('✅ Presence')
|
||||
export class PresenceController {
|
||||
constructor(private manager: SessionManager) {}
|
||||
|
||||
@@ -64,6 +68,7 @@ export class PresenceController {
|
||||
|
||||
@Get(':chatId')
|
||||
@SessionApiParam
|
||||
@ChatIdApiParam
|
||||
@ApiOperation({
|
||||
summary:
|
||||
"Get the presence for the chat id. If it hasn't been subscribed - it also subscribes to it.",
|
||||
@@ -77,6 +82,7 @@ export class PresenceController {
|
||||
|
||||
@Post(':chatId/subscribe')
|
||||
@SessionApiParam
|
||||
@ChatIdApiParam
|
||||
@ApiOperation({
|
||||
summary: 'Subscribe to presence events for the chat.',
|
||||
})
|
||||
|
||||
@@ -7,16 +7,16 @@ import {
|
||||
UseInterceptors,
|
||||
} from '@nestjs/common';
|
||||
import { ApiSecurity, ApiTags } from '@nestjs/swagger';
|
||||
import { ApiFileAcceptHeader } from '@waha/nestjs/ApiFileAcceptHeader';
|
||||
import { Response } from 'express';
|
||||
|
||||
import { SessionManager } from '../core/abc/manager.abc';
|
||||
import { BufferResponseInterceptor } from '../nestjs/BufferResponseInterceptor';
|
||||
import { SessionQuery } from '../structures/base.dto';
|
||||
import { BufferResponseInterceptor } from './BufferResponseInterceptor';
|
||||
import { ApiFileAcceptHeader } from './helpers';
|
||||
|
||||
@ApiSecurity('api_key')
|
||||
@Controller('api')
|
||||
@ApiTags('screenshot')
|
||||
@ApiTags('🖼️ Screenshot')
|
||||
export class ScreenshotController {
|
||||
constructor(private manager: SessionManager) {}
|
||||
|
||||
|
||||
@@ -0,0 +1,107 @@
|
||||
import * as process from 'node:process';
|
||||
|
||||
import {
|
||||
Body,
|
||||
Controller,
|
||||
Get,
|
||||
Logger,
|
||||
Post,
|
||||
Query,
|
||||
UsePipes,
|
||||
} from '@nestjs/common';
|
||||
import { ApiOperation, ApiSecurity, ApiTags } from '@nestjs/swagger';
|
||||
import { getApp } from '@waha/main';
|
||||
import { WAHAValidationPipe } from '@waha/nestjs/pipes/WAHAValidationPipe';
|
||||
import { WAHAEnvironment } from '@waha/structures/environment.dto';
|
||||
import {
|
||||
EnvironmentQuery,
|
||||
ServerStatusResponse,
|
||||
StopRequest,
|
||||
} from '@waha/structures/server.dto';
|
||||
import { VERSION } from '@waha/version';
|
||||
import * as lodash from 'lodash';
|
||||
|
||||
@ApiSecurity('api_key')
|
||||
@Controller('api/server')
|
||||
@ApiTags('🔍 Observability')
|
||||
export class ServerController {
|
||||
private logger: Logger;
|
||||
|
||||
constructor() {
|
||||
this.logger = new Logger('ServerController');
|
||||
}
|
||||
|
||||
@Get('version')
|
||||
@ApiOperation({ summary: 'Get the version of the server' })
|
||||
get(): WAHAEnvironment {
|
||||
return VERSION;
|
||||
}
|
||||
|
||||
@Get('environment')
|
||||
@ApiOperation({ summary: 'Get the server environment' })
|
||||
environment(
|
||||
@Query(new WAHAValidationPipe()) query: EnvironmentQuery,
|
||||
// eslint-disable-next-line @typescript-eslint/ban-types
|
||||
): object {
|
||||
let result = process.env;
|
||||
if (!query.all) {
|
||||
result = lodash.pickBy(result, (value, key) => {
|
||||
return (
|
||||
key.startsWith('WAHA_') ||
|
||||
key.startsWith('WHATSAPP_') ||
|
||||
key === 'DEBUG'
|
||||
);
|
||||
});
|
||||
}
|
||||
const map = new Map<string, string>();
|
||||
// sort and set
|
||||
Object.keys(result)
|
||||
.sort()
|
||||
.forEach((key) => {
|
||||
map.set(key, result[key]);
|
||||
});
|
||||
return Object.fromEntries(map);
|
||||
}
|
||||
|
||||
@Get('status')
|
||||
@ApiOperation({ summary: 'Get the server status' })
|
||||
async status(): Promise<ServerStatusResponse> {
|
||||
const now = Date.now();
|
||||
const uptime = Math.floor(process.uptime() * 1000);
|
||||
const startTimestamp = now - uptime;
|
||||
return {
|
||||
startTimestamp: startTimestamp,
|
||||
uptime: uptime,
|
||||
};
|
||||
}
|
||||
|
||||
@Post('stop')
|
||||
@ApiOperation({
|
||||
summary: 'Stop (and restart) the server',
|
||||
description:
|
||||
"If you're using docker, after calling this endpoint Docker will start a new container, " +
|
||||
'so you can use this endpoint to restart the server',
|
||||
})
|
||||
@UsePipes(new WAHAValidationPipe())
|
||||
async stop(@Body() request: StopRequest) {
|
||||
const timeout = 1_000;
|
||||
if (request.force) {
|
||||
this.logger.log(`Force stopping the server in ${timeout}ms`);
|
||||
setTimeout(() => {
|
||||
this.logger.log('Force stopping the server');
|
||||
process.kill(process.pid, 'SIGKILL');
|
||||
}, timeout);
|
||||
} else {
|
||||
this.logger.log(`Gracefully stopping the server in ${timeout}ms`);
|
||||
setTimeout(async () => {
|
||||
this.logger.log('Gracefully closing the application...');
|
||||
const app = getApp();
|
||||
if (app) {
|
||||
await app.close();
|
||||
}
|
||||
this.logger.log('Application closed');
|
||||
process.exit(0);
|
||||
}, timeout);
|
||||
}
|
||||
}
|
||||
}
|
||||
+253
-47
@@ -6,69 +6,66 @@ import {
|
||||
NotFoundException,
|
||||
Param,
|
||||
Post,
|
||||
Put,
|
||||
Query,
|
||||
UsePipes,
|
||||
} from '@nestjs/common';
|
||||
import { UnprocessableEntityException } from '@nestjs/common/exceptions/unprocessable-entity.exception';
|
||||
import { ApiOperation, ApiSecurity, ApiTags } from '@nestjs/swagger';
|
||||
import {
|
||||
ApiExtraModels,
|
||||
ApiOperation,
|
||||
ApiSecurity,
|
||||
ApiTags,
|
||||
} from '@nestjs/swagger';
|
||||
SessionApiParam,
|
||||
SessionParam,
|
||||
} from '@waha/nestjs/params/SessionApiParam';
|
||||
import { WAHAValidationPipe } from '@waha/nestjs/pipes/WAHAValidationPipe';
|
||||
import { WAHASessionStatus } from '@waha/structures/enums.dto';
|
||||
import {
|
||||
SessionLogoutDeprecatedRequest,
|
||||
SessionStartDeprecatedRequest,
|
||||
SessionStopDeprecatedRequest,
|
||||
} from '@waha/structures/sessions.deprecated.dto';
|
||||
import { generatePrefixedId } from '@waha/utils/ids';
|
||||
import { sleep } from '@waha/utils/promiseTimeout';
|
||||
|
||||
import { SessionManager } from '../core/abc/manager.abc';
|
||||
import { WhatsappSession } from '../core/abc/session.abc';
|
||||
import { parseBool } from '../helpers';
|
||||
import {
|
||||
ListSessionsQuery,
|
||||
MeInfo,
|
||||
SessionCreateRequest,
|
||||
SessionDTO,
|
||||
SessionInfo,
|
||||
SessionLogoutRequest,
|
||||
SessionStartRequest,
|
||||
SessionStopRequest,
|
||||
SessionUpdateRequest,
|
||||
} from '../structures/sessions.dto';
|
||||
import { SessionApiParam, SessionParam } from './helpers';
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
const AsyncLock = require('async-lock');
|
||||
|
||||
@ApiSecurity('api_key')
|
||||
@Controller('api/sessions')
|
||||
@ApiTags('sessions')
|
||||
@ApiTags('🖥️ Sessions')
|
||||
class SessionsController {
|
||||
constructor(private manager: SessionManager) {}
|
||||
private lock: any;
|
||||
|
||||
@Post('/start/')
|
||||
async start(@Body() request: SessionStartRequest): Promise<SessionDTO> {
|
||||
const result = await this.manager.start(request);
|
||||
await this.manager.sessionConfigRepository.save(
|
||||
request.name,
|
||||
request.config || null,
|
||||
);
|
||||
return result;
|
||||
constructor(private manager: SessionManager) {
|
||||
this.lock = new AsyncLock({ maxPending: Infinity });
|
||||
}
|
||||
|
||||
@Post('/stop/')
|
||||
@ApiOperation({ summary: 'Stop session' })
|
||||
async stop(@Body() request: SessionStopRequest): Promise<void> {
|
||||
if (request.logout) {
|
||||
await this.manager.logout(request);
|
||||
} else {
|
||||
await this.manager.stop(request);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
@Post('/logout/')
|
||||
@ApiOperation({ summary: 'Logout from session.' })
|
||||
clean(@Body() request: SessionLogoutRequest): Promise<void> {
|
||||
return this.manager.logout(request);
|
||||
private withLock(name: string, fn: () => any) {
|
||||
return this.lock.acquire(name, fn);
|
||||
}
|
||||
|
||||
@Get('/')
|
||||
list(@Query() query: ListSessionsQuery): Promise<SessionInfo[]> {
|
||||
const all = parseBool(query.all);
|
||||
return this.manager.getSessions(all);
|
||||
@ApiOperation({ summary: 'List all sessions' })
|
||||
list(
|
||||
@Query(new WAHAValidationPipe()) query: ListSessionsQuery,
|
||||
): Promise<SessionInfo[]> {
|
||||
return this.manager.getSessions(query.all);
|
||||
}
|
||||
|
||||
@Get('/:session')
|
||||
@ApiOperation({ summary: 'Get session information' })
|
||||
@SessionApiParam
|
||||
@UsePipes(new WAHAValidationPipe())
|
||||
async get(@Param('session') name: string): Promise<SessionInfo> {
|
||||
const session = this.manager.getSessionInfo(name);
|
||||
if (session === null) {
|
||||
@@ -76,20 +73,229 @@ class SessionsController {
|
||||
}
|
||||
return session;
|
||||
}
|
||||
}
|
||||
|
||||
@ApiSecurity('api_key')
|
||||
@Controller('api/sessions/:session')
|
||||
@ApiTags('sessions')
|
||||
class SessionController {
|
||||
constructor(private manager: SessionManager) {}
|
||||
|
||||
@Get('me')
|
||||
@Get(':session/me')
|
||||
@SessionApiParam
|
||||
@ApiOperation({ summary: 'Get information about the authenticated account' })
|
||||
getMe(@SessionParam session: WhatsappSession): MeInfo | null {
|
||||
return session.getSessionMeInfo();
|
||||
}
|
||||
|
||||
@Post('')
|
||||
@ApiOperation({
|
||||
summary: 'Create a session',
|
||||
description:
|
||||
'Create session a new session (and start it at the same time if required).',
|
||||
})
|
||||
@UsePipes(new WAHAValidationPipe())
|
||||
async create(@Body() request: SessionCreateRequest): Promise<SessionDTO> {
|
||||
const name = request.name || generatePrefixedId('session');
|
||||
await this.withLock(name, async () => {
|
||||
if (await this.manager.exists(name)) {
|
||||
const msg = `Session '${name}' already exists. Use PUT to update it.`;
|
||||
throw new UnprocessableEntityException(msg);
|
||||
}
|
||||
|
||||
const config = request.config;
|
||||
const start = request.start || false;
|
||||
await this.manager.upsert(name, config);
|
||||
if (start) {
|
||||
await this.manager.start(name);
|
||||
}
|
||||
});
|
||||
return await this.manager.getSessionInfo(name);
|
||||
}
|
||||
|
||||
@Put(':session')
|
||||
@ApiOperation({
|
||||
summary: 'Update a session',
|
||||
description: '',
|
||||
})
|
||||
@SessionApiParam
|
||||
@UsePipes(new WAHAValidationPipe())
|
||||
async update(
|
||||
@Param('session') name: string,
|
||||
@Body() request: SessionUpdateRequest,
|
||||
): Promise<SessionDTO> {
|
||||
await this.withLock(name, async () => {
|
||||
if (!(await this.manager.exists(name))) {
|
||||
throw new NotFoundException('Session not found');
|
||||
}
|
||||
const config = request.config;
|
||||
const isRunning = this.manager.isRunning(name);
|
||||
await this.manager.stop(name, true);
|
||||
await this.manager.upsert(name, config);
|
||||
if (isRunning) {
|
||||
await this.manager.start(name);
|
||||
}
|
||||
});
|
||||
return await this.manager.getSessionInfo(name);
|
||||
}
|
||||
|
||||
@Delete(':session')
|
||||
@SessionApiParam
|
||||
@ApiOperation({
|
||||
summary: 'Delete the session',
|
||||
description:
|
||||
'Delete the session with the given name. Stop and logout as well. Idempotent operation.',
|
||||
})
|
||||
@UsePipes(new WAHAValidationPipe())
|
||||
async delete(@Param('session') name: string): Promise<void> {
|
||||
await this.withLock(name, async () => {
|
||||
await this.manager.stop(name, true);
|
||||
await this.manager.logout(name);
|
||||
await this.manager.delete(name);
|
||||
});
|
||||
}
|
||||
|
||||
@Post(':session/start')
|
||||
@SessionApiParam
|
||||
@ApiOperation({
|
||||
summary: 'Start the session',
|
||||
description:
|
||||
'Start the session with the given name. The session must exist. Identity operation.',
|
||||
})
|
||||
@UsePipes(new WAHAValidationPipe())
|
||||
async start(@Param('session') name: string): Promise<SessionDTO> {
|
||||
await this.withLock(name, async () => {
|
||||
const exists = await this.manager.exists(name);
|
||||
if (!exists) {
|
||||
throw new NotFoundException('Session not found');
|
||||
}
|
||||
const isRunning = this.manager.isRunning(name);
|
||||
if (!isRunning) {
|
||||
await this.manager.start(name);
|
||||
}
|
||||
});
|
||||
return await this.manager.getSessionInfo(name);
|
||||
}
|
||||
|
||||
@Post(':session/stop')
|
||||
@SessionApiParam
|
||||
@ApiOperation({
|
||||
summary: 'Stop the session',
|
||||
description: 'Stop the session with the given name. Idempotent operation.',
|
||||
})
|
||||
@UsePipes(new WAHAValidationPipe())
|
||||
async stop(@Param('session') name: string): Promise<SessionDTO> {
|
||||
await this.withLock(name, async () => {
|
||||
const exists = await this.manager.exists(name);
|
||||
if (!exists) {
|
||||
throw new NotFoundException('Session not found');
|
||||
}
|
||||
await this.manager.stop(name, false);
|
||||
});
|
||||
return await this.manager.getSessionInfo(name);
|
||||
}
|
||||
|
||||
@Post(':session/logout')
|
||||
@SessionApiParam
|
||||
@ApiOperation({
|
||||
summary: 'Logout from the session',
|
||||
description: 'Logout the session, restart a session if it was not STOPPED',
|
||||
})
|
||||
@UsePipes(new WAHAValidationPipe())
|
||||
async logout(@Param('session') name: string): Promise<SessionDTO> {
|
||||
await this.withLock(name, async () => {
|
||||
const exists = await this.manager.exists(name);
|
||||
if (!exists) {
|
||||
throw new NotFoundException('Session not found');
|
||||
}
|
||||
const isRunning = this.manager.isRunning(name);
|
||||
await this.manager.stop(name, true);
|
||||
await this.manager.logout(name);
|
||||
if (isRunning) {
|
||||
await this.manager.start(name);
|
||||
}
|
||||
});
|
||||
return await this.manager.getSessionInfo(name);
|
||||
}
|
||||
|
||||
@Post(':session/restart')
|
||||
@SessionApiParam
|
||||
@ApiOperation({
|
||||
summary: 'Restart the session',
|
||||
description: 'Restart the session with the given name.',
|
||||
})
|
||||
@UsePipes(new WAHAValidationPipe())
|
||||
async restart(@Param('session') name: string): Promise<SessionDTO> {
|
||||
await this.withLock(name, async () => {
|
||||
const exists = await this.manager.exists(name);
|
||||
if (!exists) {
|
||||
throw new NotFoundException('Session not found');
|
||||
}
|
||||
await this.manager.stop(name, true);
|
||||
await this.manager.start(name);
|
||||
});
|
||||
|
||||
return await this.manager.getSessionInfo(name);
|
||||
}
|
||||
|
||||
@Post('/start/')
|
||||
@ApiOperation({
|
||||
summary: 'Upsert and Start session',
|
||||
description:
|
||||
'Create session (if not exists) or update a config (if exists) and start it.',
|
||||
deprecated: true,
|
||||
})
|
||||
async DEPRACATED_start(
|
||||
@Body() request: SessionStartDeprecatedRequest,
|
||||
): Promise<SessionDTO> {
|
||||
const name = request.name;
|
||||
if (this.manager.isRunning(name)) {
|
||||
const msg = `Session '${name}' is already started.`;
|
||||
throw new UnprocessableEntityException(msg);
|
||||
}
|
||||
|
||||
return await this.withLock(name, async () => {
|
||||
const config = request.config;
|
||||
await this.manager.upsert(name, config);
|
||||
return await this.manager.start(name);
|
||||
});
|
||||
}
|
||||
|
||||
@Post('/stop/')
|
||||
@ApiOperation({
|
||||
summary: 'Stop (and Logout if asked) session',
|
||||
description: 'Stop session and Logout by default.',
|
||||
deprecated: true,
|
||||
})
|
||||
async DEPRECATED_stop(
|
||||
@Body() request: SessionStopDeprecatedRequest,
|
||||
): Promise<void> {
|
||||
const name = request.name;
|
||||
if (request.logout) {
|
||||
// Old API did remove the session complete
|
||||
await this.withLock(name, async () => {
|
||||
await this.manager.stop(name, true);
|
||||
await this.manager.logout(name);
|
||||
await this.manager.delete(name);
|
||||
});
|
||||
} else {
|
||||
await this.withLock(name, async () => {
|
||||
await this.manager.stop(name, false);
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
@Post('/logout/')
|
||||
@ApiOperation({
|
||||
summary: 'Logout and Delete session.',
|
||||
description: 'Stop, Logout and Delete session.',
|
||||
deprecated: true,
|
||||
})
|
||||
async DEPRECATED_logout(
|
||||
@Body() request: SessionLogoutDeprecatedRequest,
|
||||
): Promise<void> {
|
||||
const name = request.name;
|
||||
await this.withLock(name, async () => {
|
||||
await this.manager.stop(name, true);
|
||||
await this.manager.logout(name);
|
||||
await this.manager.delete(name);
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
export { SessionController, SessionsController };
|
||||
export { SessionsController };
|
||||
@@ -1,13 +1,9 @@
|
||||
import {
|
||||
Body,
|
||||
Controller,
|
||||
Delete,
|
||||
Get,
|
||||
Param,
|
||||
Post,
|
||||
Query,
|
||||
} from '@nestjs/common';
|
||||
import { Body, Controller, Post } from '@nestjs/common';
|
||||
import { ApiOperation, ApiSecurity, ApiTags } from '@nestjs/swagger';
|
||||
import {
|
||||
SessionApiParam,
|
||||
SessionParam,
|
||||
} from '@waha/nestjs/params/SessionApiParam';
|
||||
|
||||
import { SessionManager } from '../core/abc/manager.abc';
|
||||
import { WhatsappSession } from '../core/abc/session.abc';
|
||||
@@ -18,17 +14,16 @@ import {
|
||||
VideoStatus,
|
||||
VoiceStatus,
|
||||
} from '../structures/status.dto';
|
||||
import { SessionApiParam, SessionParam } from './helpers';
|
||||
|
||||
@ApiSecurity('api_key')
|
||||
@Controller('api/:session/status')
|
||||
@ApiTags('status')
|
||||
@ApiTags('🟢 Status')
|
||||
class StatusController {
|
||||
constructor(private manager: SessionManager) {}
|
||||
|
||||
@Post('text')
|
||||
@SessionApiParam
|
||||
@ApiOperation({ summary: 'Send text status.' })
|
||||
@ApiOperation({ summary: 'Send text status' })
|
||||
sendTextStatus(
|
||||
@SessionParam session: WhatsappSession,
|
||||
@Body() status: TextStatus,
|
||||
@@ -38,7 +33,7 @@ class StatusController {
|
||||
|
||||
@Post('image')
|
||||
@SessionApiParam
|
||||
@ApiOperation({ summary: 'Send image status.' })
|
||||
@ApiOperation({ summary: 'Send image status' })
|
||||
sendImageStatus(
|
||||
@SessionParam session: WhatsappSession,
|
||||
@Body() status: ImageStatus,
|
||||
@@ -48,7 +43,7 @@ class StatusController {
|
||||
|
||||
@Post('voice')
|
||||
@SessionApiParam
|
||||
@ApiOperation({ summary: 'Send voice status.' })
|
||||
@ApiOperation({ summary: 'Send voice status' })
|
||||
sendVoiceStatus(
|
||||
@SessionParam session: WhatsappSession,
|
||||
@Body() status: VoiceStatus,
|
||||
@@ -58,7 +53,7 @@ class StatusController {
|
||||
|
||||
@Post('video')
|
||||
@SessionApiParam
|
||||
@ApiOperation({ summary: 'Send video status.' })
|
||||
@ApiOperation({ summary: 'Send video status' })
|
||||
sendVideoStatus(
|
||||
@SessionParam session: WhatsappSession,
|
||||
@Body() status: VideoStatus,
|
||||
@@ -68,7 +63,7 @@ class StatusController {
|
||||
|
||||
@Post('delete')
|
||||
@SessionApiParam
|
||||
@ApiOperation({ summary: 'DELETE sent status.' })
|
||||
@ApiOperation({ summary: 'DELETE sent status' })
|
||||
deleteStatus(
|
||||
@SessionParam session: WhatsappSession,
|
||||
@Body() status: DeleteStatusRequest,
|
||||
|
||||
@@ -1,14 +1,24 @@
|
||||
import { Controller, Get } from '@nestjs/common';
|
||||
import { ApiExtraModels, ApiSecurity, ApiTags } from '@nestjs/swagger';
|
||||
import {
|
||||
ApiExtraModels,
|
||||
ApiOperation,
|
||||
ApiSecurity,
|
||||
ApiTags,
|
||||
} from '@nestjs/swagger';
|
||||
|
||||
import { WAHAEnvironment } from '../structures/environment.dto';
|
||||
import { VERSION } from '../version';
|
||||
|
||||
@ApiSecurity('api_key')
|
||||
@Controller('api/version')
|
||||
@ApiTags('other')
|
||||
@ApiTags('🔍 Observability')
|
||||
export class VersionController {
|
||||
@Get('')
|
||||
@ApiOperation({
|
||||
summary: 'Get the server version ',
|
||||
deprecated: true,
|
||||
description: "Use 'GET /api/server/version' instead ",
|
||||
})
|
||||
get(): WAHAEnvironment {
|
||||
return VERSION;
|
||||
}
|
||||
|
||||
+17
-21
@@ -1,20 +1,16 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { WAHAEvents } from '@waha/structures/enums.dto';
|
||||
|
||||
import { parseBool } from './helpers';
|
||||
import { WAHAEngine } from './structures/enums.dto';
|
||||
import { WebhookConfig } from './structures/webhooks.config.dto';
|
||||
import { getEngineName } from './version';
|
||||
|
||||
@Injectable()
|
||||
export class WhatsappConfigService {
|
||||
public filesUri = '/api/files';
|
||||
public schema = 'http';
|
||||
|
||||
constructor(private configService: ConfigService) {}
|
||||
|
||||
get filesURL(): string {
|
||||
return `${this.schema}://${this.hostname}:${this.port}${this.filesUri}/`;
|
||||
get schema() {
|
||||
return this.configService.get('WHATSAPP_API_SCHEMA', 'http');
|
||||
}
|
||||
|
||||
get hostname(): string {
|
||||
@@ -25,17 +21,6 @@ export class WhatsappConfigService {
|
||||
return this.configService.get('WHATSAPP_API_PORT', '3000');
|
||||
}
|
||||
|
||||
get filesFolder(): string {
|
||||
return this.configService.get(
|
||||
'WHATSAPP_FILES_FOLDER',
|
||||
'/tmp/whatsapp-files',
|
||||
);
|
||||
}
|
||||
|
||||
get filesLifetime(): number {
|
||||
return this.configService.get<number>('WHATSAPP_FILES_LIFETIME', 180);
|
||||
}
|
||||
|
||||
get mimetypes(): string[] {
|
||||
if (!this.shouldDownloadMedia) {
|
||||
return ['mimetype/ignore-all-media'];
|
||||
@@ -66,8 +51,11 @@ export class WhatsappConfigService {
|
||||
}
|
||||
|
||||
get proxyServer(): string[] | string | undefined {
|
||||
const single = this.configService.get('WHATSAPP_PROXY_SERVER', undefined);
|
||||
const multipleValues = this.configService.get(
|
||||
const single = this.configService.get<string>(
|
||||
'WHATSAPP_PROXY_SERVER',
|
||||
undefined,
|
||||
);
|
||||
const multipleValues = this.configService.get<string>(
|
||||
'WHATSAPP_PROXY_SERVER_LIST',
|
||||
undefined,
|
||||
);
|
||||
@@ -103,7 +91,7 @@ export class WhatsappConfigService {
|
||||
return this.get('WHATSAPP_HOOK_URL');
|
||||
}
|
||||
|
||||
private getWebhookEvents(): string[] {
|
||||
private getWebhookEvents(): WAHAEvents[] {
|
||||
const value = this.get('WHATSAPP_HOOK_EVENTS', '');
|
||||
return value ? value.split(',') : [];
|
||||
}
|
||||
@@ -120,6 +108,14 @@ export class WhatsappConfigService {
|
||||
return this.configService.get('WHATSAPP_API_KEY', '');
|
||||
}
|
||||
|
||||
getExcludedPaths(): string[] {
|
||||
const value = this.configService.get('WHATSAPP_API_KEY_EXCLUDE_PATH', '');
|
||||
if (!value) {
|
||||
return [];
|
||||
}
|
||||
return value.split(',');
|
||||
}
|
||||
|
||||
getHealthMediaFilesThreshold(): number {
|
||||
return this.configService.get<number>(
|
||||
'WHATSAPP_HEALTH_MEDIA_FILES_THRESHOLD_MB',
|
||||
|
||||
@@ -29,8 +29,8 @@ export class SwaggerConfiguratorCore {
|
||||
'<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>' +
|
||||
'<li><a href="/dashboard">WAHA Dashboard</a></li>' +
|
||||
'<li><a href="https://waha.devlike.pro/docs/how-to/waha-dashboard">WAHA Dashboard Documentation</a></li>' +
|
||||
'</ul>' +
|
||||
'<p>Support the project and get WAHA Plus version!</p>' +
|
||||
'<ul>' +
|
||||
@@ -56,25 +56,30 @@ export class SwaggerConfiguratorCore {
|
||||
.setDescription(this.description)
|
||||
.setExternalDoc(this.title, this.externalDocUrl)
|
||||
.setVersion(VERSION.version)
|
||||
.addTag('sessions', 'Control WhatsApp sessions')
|
||||
.addTag('auth', 'Authentication')
|
||||
.addTag('screenshot', 'Get screenshot of WhatsApp and show QR code')
|
||||
.addTag('chatting', 'Chatting methods')
|
||||
.addTag('channels', 'Channels (newsletters) methods')
|
||||
.addTag('🖥️ Sessions', 'Control WhatsApp sessions (accounts)')
|
||||
.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',
|
||||
'Status (aka stories) methods. <b>NOWEB</b> engine only!',
|
||||
)
|
||||
.addTag('chats', `Chats methods`)
|
||||
.addTag('💬 Chats', `Chats methods`)
|
||||
.addTag(
|
||||
'contacts',
|
||||
'👤 Contacts',
|
||||
`Contacts methods.<br>
|
||||
Use phone number (without +) or phone number and \`@c.us\` at the end as \`contactId\`.<br>
|
||||
'E.g: \`12312312310\` OR \`12312312310@c.us\`<br>`,
|
||||
)
|
||||
.addTag('groups', `Groups methods.<br>`)
|
||||
.addTag('presence', `Presence information`)
|
||||
.addTag('other', 'Other methods')
|
||||
.addTag('👥 Groups', `Groups methods.<br>`)
|
||||
.addTag('✅ Presence', `Presence information`)
|
||||
.addTag(
|
||||
'🏷️ Labels',
|
||||
'Labels - available only for WhatsApp Business accounts',
|
||||
)
|
||||
.addTag('🔍 Observability', 'Other methods')
|
||||
.addTag('🗄️ Storage', 'Storage methods')
|
||||
.addApiKey({
|
||||
type: 'apiKey',
|
||||
description: 'Your secret api key',
|
||||
|
||||
@@ -1 +1,3 @@
|
||||
export abstract class DataStore {}
|
||||
export abstract class DataStore {
|
||||
abstract init(sessionName?: string): Promise<void>;
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import { ConsoleLogger, Injectable } from '@nestjs/common';
|
||||
import { DiskHealthIndicator, HealthCheckService } from '@nestjs/terminus';
|
||||
import { Injectable, Logger, LoggerService } from '@nestjs/common';
|
||||
import { HealthCheckService } from '@nestjs/terminus';
|
||||
import type { HealthCheckResult } from '@nestjs/terminus/dist/health-check/health-check-result.interface';
|
||||
|
||||
import { WhatsappConfigService } from '../../config.service';
|
||||
@@ -7,12 +7,14 @@ import { SessionManager } from './manager.abc';
|
||||
|
||||
@Injectable()
|
||||
export abstract class WAHAHealthCheckService {
|
||||
protected logger: LoggerService;
|
||||
constructor(
|
||||
protected sessionManager: SessionManager,
|
||||
protected health: HealthCheckService,
|
||||
protected log: ConsoleLogger,
|
||||
protected config: WhatsappConfigService,
|
||||
) {}
|
||||
) {
|
||||
this.logger = new Logger('WAHAHealthCheckService');
|
||||
}
|
||||
|
||||
abstract check(): Promise<HealthCheckResult>;
|
||||
}
|
||||
+17
-11
@@ -1,19 +1,13 @@
|
||||
import {
|
||||
BeforeApplicationShutdown,
|
||||
OnApplicationShutdown,
|
||||
} from '@nestjs/common';
|
||||
import { BeforeApplicationShutdown } 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,
|
||||
SessionConfig,
|
||||
SessionDTO,
|
||||
SessionInfo,
|
||||
SessionLogoutRequest,
|
||||
SessionStartRequest,
|
||||
SessionStopRequest,
|
||||
} from '../../structures/sessions.dto';
|
||||
import { ISessionAuthRepository } from '../storage/ISessionAuthRepository';
|
||||
import { ISessionConfigRepository } from '../storage/ISessionConfigRepository';
|
||||
@@ -37,11 +31,22 @@ export abstract class SessionManager implements BeforeApplicationShutdown {
|
||||
//
|
||||
// API Methods
|
||||
//
|
||||
abstract start(request: SessionStartRequest): Promise<SessionDTO>;
|
||||
/**
|
||||
* Either create or update
|
||||
*/
|
||||
abstract exists(name: string): Promise<boolean>;
|
||||
|
||||
abstract stop(request: SessionStopRequest): Promise<void>;
|
||||
abstract isRunning(name: string): boolean;
|
||||
|
||||
abstract logout(request: SessionLogoutRequest): Promise<void>;
|
||||
abstract upsert(name: string, config?: SessionConfig): Promise<void>;
|
||||
|
||||
abstract delete(name: string): Promise<void>;
|
||||
|
||||
abstract start(name: string): Promise<SessionDTO>;
|
||||
|
||||
abstract stop(name: string, silent: boolean): Promise<void>;
|
||||
|
||||
abstract logout(name: string): Promise<void>;
|
||||
|
||||
abstract getSession(name: string): WhatsappSession;
|
||||
|
||||
@@ -55,6 +60,7 @@ export abstract class SessionManager implements BeforeApplicationShutdown {
|
||||
const data: WAHAWebhook = {
|
||||
event: event,
|
||||
session: session.name,
|
||||
metadata: session.sessionConfig?.metadata,
|
||||
me: me,
|
||||
payload: payload,
|
||||
engine: session.engine,
|
||||
|
||||
@@ -1,28 +0,0 @@
|
||||
abstract class MediaStorage {
|
||||
abstract save(
|
||||
messageId: string,
|
||||
mimetype: string,
|
||||
buffer: Buffer,
|
||||
): Promise<string>;
|
||||
}
|
||||
|
||||
interface IEngineMediaProcessor<Message> {
|
||||
hasMedia(message: Message): boolean;
|
||||
|
||||
getFilename(message: Message): string | null;
|
||||
|
||||
getMimetype(message: Message): string;
|
||||
|
||||
getMessageId(message: Message): string;
|
||||
|
||||
getMediaBuffer(message: Message): Promise<Buffer | null>;
|
||||
}
|
||||
|
||||
interface MediaManager {
|
||||
processMedia<Message>(
|
||||
processor: IEngineMediaProcessor<Message>,
|
||||
message: Message,
|
||||
): Promise<Message>;
|
||||
}
|
||||
|
||||
export { IEngineMediaProcessor, MediaManager, MediaStorage };
|
||||
+62
-19
@@ -4,6 +4,7 @@ import {
|
||||
ListChannelsQuery,
|
||||
} from '@waha/structures/channels.dto';
|
||||
import { GetChatsQuery } from '@waha/structures/chats.dto';
|
||||
import { Label, LabelID } from '@waha/structures/labels.dto';
|
||||
import { LoggerBuilder } from '@waha/utils/logging';
|
||||
import { EventEmitter } from 'events';
|
||||
import * as fs from 'fs';
|
||||
@@ -55,9 +56,9 @@ import {
|
||||
} from '../../structures/status.dto';
|
||||
import { WASessionStatusBody } from '../../structures/webhooks.dto';
|
||||
import { NotImplementedByEngineError } from '../exceptions';
|
||||
import { IMediaManager } from '../media/IMediaManager';
|
||||
import { QR } from '../QR';
|
||||
import { DataStore } from './DataStore';
|
||||
import { MediaManager } from './media.abc';
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
const qrcode = require('qrcode-terminal');
|
||||
@@ -87,7 +88,7 @@ export enum WAHAInternalEvent {
|
||||
export interface SessionParams {
|
||||
name: string;
|
||||
printQR: boolean;
|
||||
mediaManager: MediaManager;
|
||||
mediaManager: IMediaManager;
|
||||
loggerBuilder: LoggerBuilder;
|
||||
sessionStore: DataStore;
|
||||
proxyConfig?: ProxyConfig;
|
||||
@@ -100,7 +101,7 @@ export abstract class WhatsappSession {
|
||||
public engine: WAHAEngine;
|
||||
|
||||
public name: string;
|
||||
protected mediaManager: MediaManager;
|
||||
protected mediaManager: IMediaManager;
|
||||
public loggerBuilder: LoggerBuilder;
|
||||
protected logger: Logger;
|
||||
protected sessionStore: DataStore;
|
||||
@@ -151,29 +152,43 @@ export abstract class WhatsappSession {
|
||||
// Run optimized version of Chrome
|
||||
// References:
|
||||
// https://github.com/pedroslopez/whatsapp-web.js/issues/1420
|
||||
// https://github.com/wppconnect-team/wppconnect/issues/1326
|
||||
// https://www.bannerbear.com/blog/ways-to-speed-up-puppeteer-screenshots/
|
||||
return [
|
||||
'--no-sandbox',
|
||||
'--aggressive-cache-discard',
|
||||
'--disable-accelerated-2d-canvas',
|
||||
'--disable-application-cache',
|
||||
'--disable-background-networking',
|
||||
'--disable-cache',
|
||||
'--disable-client-side-phishing-detection',
|
||||
'--disable-setuid-sandbox',
|
||||
'--disable-component-update',
|
||||
'--disable-default-apps',
|
||||
'--disable-popup-blocking',
|
||||
'--disable-offer-store-unmasked-wallet-cards',
|
||||
'--disable-speech-api',
|
||||
'--hide-scrollbars',
|
||||
'--mute-audio',
|
||||
'--disable-extensions',
|
||||
'--disable-dev-shm-usage',
|
||||
'--disable-accelerated-2d-canvas',
|
||||
'--no-first-run',
|
||||
'--no-default-browser-check',
|
||||
'--no-pings',
|
||||
'--password-store=basic',
|
||||
'--use-mock-keychain',
|
||||
'--no-zygote',
|
||||
'--single-process',
|
||||
'--disable-extensions',
|
||||
'--disable-gpu',
|
||||
'--disable-offer-store-unmasked-wallet-cards',
|
||||
'--disable-offline-load-stale-cache',
|
||||
'--disable-popup-blocking',
|
||||
'--disable-setuid-sandbox',
|
||||
'--disable-speech-api',
|
||||
'--disable-sync',
|
||||
'--disable-translate',
|
||||
'--disable-web-security',
|
||||
'--disk-cache-size=0',
|
||||
'--hide-scrollbars',
|
||||
'--ignore-certificate-errors',
|
||||
'--ignore-ssl-errors',
|
||||
'--metrics-recording-only',
|
||||
'--mute-audio',
|
||||
'--no-default-browser-check',
|
||||
'--no-first-run',
|
||||
'--no-pings',
|
||||
'--no-sandbox',
|
||||
'--no-zygote',
|
||||
'--password-store=basic',
|
||||
'--safebrowsing-disable-auto-update',
|
||||
'--single-process',
|
||||
'--use-mock-keychain',
|
||||
];
|
||||
}
|
||||
|
||||
@@ -324,6 +339,34 @@ export abstract class WhatsappSession {
|
||||
throw new NotImplementedByEngineError();
|
||||
}
|
||||
|
||||
public chatsArchiveChat(chatId: string): Promise<any> {
|
||||
throw new NotImplementedByEngineError();
|
||||
}
|
||||
|
||||
public chatsUnarchiveChat(chatId: string): Promise<any> {
|
||||
throw new NotImplementedByEngineError();
|
||||
}
|
||||
|
||||
/**
|
||||
* Labels methods
|
||||
*/
|
||||
|
||||
public getLabels(): Promise<Label[]> {
|
||||
throw new NotImplementedByEngineError();
|
||||
}
|
||||
|
||||
public getChatsByLabelId(labelId: string) {
|
||||
throw new NotImplementedByEngineError();
|
||||
}
|
||||
|
||||
public getChatLabels(chatId: string): Promise<Label[]> {
|
||||
throw new NotImplementedByEngineError();
|
||||
}
|
||||
|
||||
public putLabelsToChat(chatId: string, labels: LabelID[]) {
|
||||
throw new NotImplementedByEngineError();
|
||||
}
|
||||
|
||||
/**
|
||||
* Contacts methods
|
||||
*/
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import {
|
||||
BeforeApplicationShutdown,
|
||||
ConsoleLogger,
|
||||
Logger,
|
||||
LoggerService,
|
||||
} from '@nestjs/common';
|
||||
@@ -13,9 +12,10 @@ import {
|
||||
WebSocketServer,
|
||||
} from '@nestjs/websockets';
|
||||
import { SessionManager } from '@waha/core/abc/manager.abc';
|
||||
import { WebsocketHeartbeatJob } from '@waha/nestjs/ws/WebsocketHeartbeatJob';
|
||||
import { WebSocket } from '@waha/nestjs/ws/ws';
|
||||
import { WAHAEvents } from '@waha/structures/enums.dto';
|
||||
import { HeartbeatJob } from '@waha/utils/HeartbeatJob';
|
||||
import { WebSocket } from '@waha/utils/ws';
|
||||
import { generatePrefixedId } from '@waha/utils/ids';
|
||||
import { IncomingMessage } from 'http';
|
||||
import * as lodash from 'lodash';
|
||||
import * as url from 'url';
|
||||
@@ -41,16 +41,19 @@ export class WebsocketGatewayCore
|
||||
new Map();
|
||||
|
||||
private readonly logger: LoggerService;
|
||||
private heartbeat: HeartbeatJob;
|
||||
private heartbeat: WebsocketHeartbeatJob;
|
||||
|
||||
constructor(private manager: SessionManager) {
|
||||
this.logger = new Logger('WebsocketGateway');
|
||||
this.heartbeat = new HeartbeatJob(this.logger, this.HEARTBEAT_INTERVAL);
|
||||
this.heartbeat = new WebsocketHeartbeatJob(
|
||||
this.logger,
|
||||
this.HEARTBEAT_INTERVAL,
|
||||
);
|
||||
}
|
||||
|
||||
handleConnection(socket: WebSocket, request: IncomingMessage, ...args): any {
|
||||
const random = Math.random().toString(36).substring(7);
|
||||
const id = `wsc_${random}`;
|
||||
// wsc - websocket client
|
||||
const id = generatePrefixedId('wsc');
|
||||
socket.id = id;
|
||||
this.logger.debug(`New client connected: ${request.url}`);
|
||||
const params = this.getParams(id, request, socket);
|
||||
@@ -102,7 +105,9 @@ export class WebsocketGatewayCore
|
||||
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) => {
|
||||
this.server?.clients.forEach((options, client) => {
|
||||
// @ts-ignore
|
||||
this.logger.debug(`Closing client connection - ${client.id}...`);
|
||||
client.close(1001, 'Server is shutting down');
|
||||
});
|
||||
// Do not turn off heartbeat service here,
|
||||
|
||||
+41
-29
@@ -1,13 +1,20 @@
|
||||
import { ConsoleLogger, INestApplication, Module } from '@nestjs/common';
|
||||
import { 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 { ServerController } from '@waha/api/server.controller';
|
||||
import { WebsocketGatewayCore } from '@waha/core/api/websocket.gateway.core';
|
||||
import { getPinoLogLevel, getPinoTransport } from '@waha/utils/logging';
|
||||
import { MediaLocalStorageConfig } from '@waha/core/media/local/MediaLocalStorageConfig';
|
||||
import { BufferJsonReplacerInterceptor } from '@waha/nestjs/BufferJsonReplacerInterceptor';
|
||||
import {
|
||||
getPinoHttpUseLevel,
|
||||
getPinoLogLevel,
|
||||
getPinoTransport,
|
||||
} from '@waha/utils/logging';
|
||||
import * as Joi from 'joi';
|
||||
import { LoggerModule } from 'nestjs-pino';
|
||||
import { join } from 'path';
|
||||
import { Logger } from 'pino';
|
||||
@@ -18,12 +25,11 @@ import { ChattingController } from '../api/chatting.controller';
|
||||
import { ContactsController } from '../api/contacts.controller';
|
||||
import { GroupsController } from '../api/groups.controller';
|
||||
import { HealthController } from '../api/health.controller';
|
||||
import { LabelsController } from '../api/labels.controller';
|
||||
import { PingController } from '../api/ping.controller';
|
||||
import { PresenceController } from '../api/presence.controller';
|
||||
import { ScreenshotController } from '../api/screenshot.controller';
|
||||
import {
|
||||
SessionController,
|
||||
SessionsController,
|
||||
} from '../api/sessions.controller';
|
||||
import { SessionsController } from '../api/sessions.controller';
|
||||
import { StatusController } from '../api/status.controller';
|
||||
import { VersionController } from '../api/version.controller';
|
||||
import { WhatsappConfigService } from '../config.service';
|
||||
@@ -41,10 +47,15 @@ export const IMPORTS = [
|
||||
pinoHttp: {
|
||||
quietReqLogger: true,
|
||||
level: getPinoLogLevel(),
|
||||
useLevel: getPinoHttpUseLevel(),
|
||||
transport: getPinoTransport(),
|
||||
autoLogging: {
|
||||
ignore: (req) => {
|
||||
return req.url.startsWith('/dashboard/');
|
||||
return (
|
||||
req.url.startsWith('/dashboard/') ||
|
||||
req.url.startsWith('/api/files/') ||
|
||||
req.url.startsWith('/api/s3/')
|
||||
);
|
||||
},
|
||||
},
|
||||
serializers: {
|
||||
@@ -63,29 +74,24 @@ export const IMPORTS = [
|
||||
}),
|
||||
ConfigModule.forRoot({
|
||||
isGlobal: true,
|
||||
validationSchema: Joi.object({
|
||||
WHATSAPP_API_SCHEMA: Joi.string().valid('http', 'https').default('http'),
|
||||
}),
|
||||
}),
|
||||
ServeStaticModule.forRootAsync({
|
||||
imports: [],
|
||||
extraProviders: [WhatsappConfigService, DashboardConfigServiceCore],
|
||||
inject: [WhatsappConfigService, DashboardConfigServiceCore],
|
||||
useFactory: (
|
||||
config: WhatsappConfigService,
|
||||
dashboardConfig: DashboardConfigServiceCore,
|
||||
) => {
|
||||
const options = [
|
||||
// Serve files (media)
|
||||
extraProviders: [DashboardConfigServiceCore],
|
||||
inject: [DashboardConfigServiceCore],
|
||||
useFactory: (dashboardConfig: DashboardConfigServiceCore) => {
|
||||
if (!dashboardConfig.enabled) {
|
||||
return [];
|
||||
}
|
||||
return [
|
||||
{
|
||||
rootPath: config.filesFolder,
|
||||
serveRoot: config.filesUri,
|
||||
},
|
||||
];
|
||||
if (dashboardConfig.enabled) {
|
||||
options.push({
|
||||
rootPath: join(__dirname, '..', 'dashboard'),
|
||||
serveRoot: dashboardConfig.dashboardUri,
|
||||
});
|
||||
}
|
||||
return options;
|
||||
},
|
||||
];
|
||||
},
|
||||
}),
|
||||
PassportModule,
|
||||
@@ -94,17 +100,19 @@ export const IMPORTS = [
|
||||
export const CONTROLLERS = [
|
||||
AuthController,
|
||||
SessionsController,
|
||||
SessionController,
|
||||
ChattingController,
|
||||
ChatsController,
|
||||
ChannelsController,
|
||||
StatusController,
|
||||
LabelsController,
|
||||
ContactsController,
|
||||
GroupsController,
|
||||
PresenceController,
|
||||
ScreenshotController,
|
||||
VersionController,
|
||||
PingController,
|
||||
HealthController,
|
||||
ServerController,
|
||||
VersionController,
|
||||
];
|
||||
const PROVIDERS = [
|
||||
{
|
||||
@@ -123,8 +131,8 @@ const PROVIDERS = [
|
||||
SwaggerConfigServiceCore,
|
||||
WhatsappConfigService,
|
||||
EngineConfigService,
|
||||
ConsoleLogger,
|
||||
WebsocketGatewayCore,
|
||||
MediaLocalStorageConfig,
|
||||
];
|
||||
|
||||
@Module({
|
||||
@@ -133,7 +141,11 @@ const PROVIDERS = [
|
||||
providers: PROVIDERS,
|
||||
})
|
||||
export class AppModuleCore {
|
||||
constructor(protected config: WhatsappConfigService) {}
|
||||
public startTimestamp: number;
|
||||
|
||||
constructor(protected config: WhatsappConfigService) {
|
||||
this.startTimestamp = Date.now();
|
||||
}
|
||||
|
||||
static getHttpsOptions(logger: Logger) {
|
||||
return undefined;
|
||||
|
||||
@@ -4,6 +4,7 @@ import makeWASocket, {
|
||||
DisconnectReason,
|
||||
extractMessageContent,
|
||||
getAggregateVotesInPollMessage,
|
||||
getContentType,
|
||||
getKeyAuthor,
|
||||
getUrlFromDirectPath,
|
||||
isJidGroup,
|
||||
@@ -18,12 +19,17 @@ import makeWASocket, {
|
||||
WAMessageContent,
|
||||
WAMessageKey,
|
||||
} from '@adiwajshing/baileys';
|
||||
import { WACallEvent } from '@adiwajshing/baileys/lib/Types/Call';
|
||||
import { Label as NOWEBLabel } from '@adiwajshing/baileys/lib/Types/Label';
|
||||
import { LabelAssociationType } from '@adiwajshing/baileys/lib/Types/LabelAssociation';
|
||||
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 { IMediaEngineProcessor } from '@waha/core/media/IMediaEngineProcessor';
|
||||
import { flipObject, parseBool, splitAt } from '@waha/helpers';
|
||||
import { PairingCodeResponse } from '@waha/structures/auth.dto';
|
||||
import { CallData } from '@waha/structures/calls.dto';
|
||||
import {
|
||||
Channel,
|
||||
ChannelRole,
|
||||
@@ -32,11 +38,20 @@ import {
|
||||
} from '@waha/structures/channels.dto';
|
||||
import { GetChatsQuery } from '@waha/structures/chats.dto';
|
||||
import { ContactQuery, ContactRequest } from '@waha/structures/contacts.dto';
|
||||
import {
|
||||
Label,
|
||||
LabelChatAssociation,
|
||||
LabelID,
|
||||
} from '@waha/structures/labels.dto';
|
||||
import { ReplyToMessage } from '@waha/structures/message.dto';
|
||||
import {
|
||||
PollVote,
|
||||
PollVotePayload,
|
||||
WAMessageAckBody,
|
||||
} from '@waha/structures/webhooks.dto';
|
||||
import { waitUntil } from '@waha/utils/promiseTimeout';
|
||||
import { SingleDelayedJobRunner } from '@waha/utils/SingleDelayedJobRunner';
|
||||
import { SinglePeriodicJobRunner } from '@waha/utils/SinglePeriodicJobRunner';
|
||||
import * as Buffer from 'buffer';
|
||||
import { Agent } from 'https';
|
||||
import * as lodash from 'lodash';
|
||||
@@ -90,7 +105,6 @@ import {
|
||||
DeleteStatusRequest,
|
||||
TextStatus,
|
||||
} from '../../../structures/status.dto';
|
||||
import { IEngineMediaProcessor } from '../../abc/media.abc';
|
||||
import {
|
||||
ensureSuffix,
|
||||
getChannelInviteLink,
|
||||
@@ -135,13 +149,13 @@ const ToEnginePresenceStatus = flipObject(PresenceStatuses);
|
||||
|
||||
export class WhatsappSessionNoWebCore extends WhatsappSession {
|
||||
private START_ATTEMPT_DELAY_SECONDS = 2;
|
||||
private AUTO_RESTART_AFTER_SECONDS = 30 * 60;
|
||||
private AUTO_RESTART_AFTER_SECONDS = 28 * 60;
|
||||
|
||||
engine = WAHAEngine.NOWEB;
|
||||
authFactory = new NowebAuthFactoryCore();
|
||||
storageFactory = new NowebStorageFactoryCore();
|
||||
private startTimeoutId: null | ReturnType<typeof setTimeout> = null;
|
||||
private autoRestartTimeoutId: null | ReturnType<typeof setTimeout> = null;
|
||||
private startDelayedJob: SingleDelayedJobRunner;
|
||||
private autoRestartJob: SinglePeriodicJobRunner;
|
||||
private msgRetryCounterCache: NodeCache;
|
||||
protected engineLogger: BaileysLogger;
|
||||
|
||||
@@ -166,11 +180,27 @@ export class WhatsappSessionNoWebCore extends WhatsappSession {
|
||||
this.engineLogger = this.loggerBuilder.child({
|
||||
name: 'NOWEBEngine',
|
||||
}) as unknown as BaileysLogger;
|
||||
|
||||
// Restart job if session failed
|
||||
this.startDelayedJob = new SingleDelayedJobRunner(
|
||||
'start-engine',
|
||||
this.START_ATTEMPT_DELAY_SECONDS * SECOND,
|
||||
this.logger,
|
||||
);
|
||||
|
||||
// Enable auto-restart
|
||||
const shiftSeconds = Math.floor(Math.random() * 30);
|
||||
const delay = this.AUTO_RESTART_AFTER_SECONDS + shiftSeconds;
|
||||
this.autoRestartJob = new SinglePeriodicJobRunner(
|
||||
'auto-restart',
|
||||
delay * SECOND,
|
||||
this.logger,
|
||||
);
|
||||
}
|
||||
|
||||
start() {
|
||||
async start() {
|
||||
this.status = WAHASessionStatus.STARTING;
|
||||
this.buildClient();
|
||||
await this.buildClient();
|
||||
}
|
||||
|
||||
getSocketConfig(agent, state): any {
|
||||
@@ -268,20 +298,15 @@ export class WhatsappSessionNoWebCore extends WhatsappSession {
|
||||
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;
|
||||
private enableAutoRestart() {
|
||||
this.autoRestartJob.start(async () => {
|
||||
this.logger.info('Auto-restarting the client connection...');
|
||||
this.sock?.end(new Error('auto-restart'));
|
||||
}, delay * SECOND);
|
||||
if (this.sock?.ws?.isConnecting) {
|
||||
this.logger.warn('Auto-restart skipped, the client is connecting...');
|
||||
return;
|
||||
}
|
||||
this.sock?.end(undefined);
|
||||
});
|
||||
}
|
||||
|
||||
protected async getMessage(
|
||||
@@ -301,19 +326,9 @@ export class WhatsappSessionNoWebCore extends WhatsappSession {
|
||||
}
|
||||
|
||||
private restartClient() {
|
||||
if (this.startTimeoutId) {
|
||||
this.logger.info(
|
||||
'Request to restart is already in progress, ignoring this request',
|
||||
);
|
||||
return;
|
||||
}
|
||||
this.logger.info(
|
||||
`Setting up client start in ${this.START_ATTEMPT_DELAY_SECONDS} seconds...`,
|
||||
);
|
||||
this.startTimeoutId = setTimeout(() => {
|
||||
this.startTimeoutId = undefined;
|
||||
this.buildClient();
|
||||
}, this.START_ATTEMPT_DELAY_SECONDS * SECOND);
|
||||
this.startDelayedJob.schedule(async () => {
|
||||
await this.buildClient();
|
||||
});
|
||||
}
|
||||
|
||||
protected listenConnectionEvents() {
|
||||
@@ -335,9 +350,11 @@ export class WhatsappSessionNoWebCore extends WhatsappSession {
|
||||
this.qr.save('');
|
||||
// reconnect if not logged out
|
||||
if (shouldReconnect) {
|
||||
this.logger.info(
|
||||
`Connection closed due to '${lastDisconnect.error}', reconnecting...`,
|
||||
);
|
||||
if (lastDisconnect.error) {
|
||||
this.logger.info(
|
||||
`Connection closed due to '${lastDisconnect.error}', reconnecting...`,
|
||||
);
|
||||
}
|
||||
this.restartClient();
|
||||
} else {
|
||||
this.logger.error(
|
||||
@@ -424,13 +441,16 @@ export class WhatsappSessionNoWebCore extends WhatsappSession {
|
||||
}
|
||||
|
||||
private async end() {
|
||||
clearTimeout(this.autoRestartTimeoutId);
|
||||
clearTimeout(this.startTimeoutId);
|
||||
this.autoRestartJob.stop();
|
||||
this.startDelayedJob.cancel();
|
||||
// @ts-ignore
|
||||
this.sock?.ev?.removeAllListeners();
|
||||
this.sock?.ws?.removeAllListeners();
|
||||
await this.store?.close();
|
||||
// wait until connection is not connecting to avoid error:
|
||||
// "WebSocket was closed before the connection was established"
|
||||
await waitUntil(async () => !this.sock?.ws?.isConnecting, 1_000, 10_000);
|
||||
this.sock?.end(undefined);
|
||||
this.store?.close();
|
||||
}
|
||||
|
||||
getSessionMeInfo(): MeInfo | null {
|
||||
@@ -518,26 +538,20 @@ export class WhatsappSessionNoWebCore extends WhatsappSession {
|
||||
};
|
||||
}
|
||||
|
||||
sendText(request: MessageTextRequest) {
|
||||
async sendText(request: MessageTextRequest) {
|
||||
const chatId = toJID(this.ensureSuffix(request.chatId));
|
||||
const message = {
|
||||
text: request.text,
|
||||
mentions: request.mentions?.map(toJID),
|
||||
};
|
||||
return this.sock.sendMessage(chatId, message);
|
||||
const options = await this.getMessageOptions(request);
|
||||
return this.sock.sendMessage(chatId, message, options);
|
||||
}
|
||||
|
||||
public deleteMessage(chatId: string, messageId: string) {
|
||||
const jid = toJID(this.ensureSuffix(chatId));
|
||||
const key = parseMessageId(messageId);
|
||||
const options = {
|
||||
statusJidList: [
|
||||
'79521562380@s.whatsapp.net',
|
||||
'79069571990@s.whatsapp.net',
|
||||
'79069471990@s.whatsapp.net',
|
||||
],
|
||||
};
|
||||
return this.sock.sendMessage(jid, { delete: key }, options);
|
||||
const key = parseMessageIdSerialized(messageId);
|
||||
return this.sock.sendMessage(jid, { delete: key });
|
||||
}
|
||||
|
||||
public editMessage(
|
||||
@@ -546,7 +560,7 @@ export class WhatsappSessionNoWebCore extends WhatsappSession {
|
||||
request: EditMessageRequest,
|
||||
) {
|
||||
const jid = toJID(this.ensureSuffix(chatId));
|
||||
const key = parseMessageId(messageId);
|
||||
const key = parseMessageIdSerialized(messageId);
|
||||
const message = {
|
||||
text: request.text,
|
||||
mentions: request.mentions?.map(toJID),
|
||||
@@ -576,23 +590,18 @@ export class WhatsappSessionNoWebCore extends WhatsappSession {
|
||||
};
|
||||
const message = { poll: poll };
|
||||
const remoteJid = toJID(request.chatId);
|
||||
const result = await this.sock.sendMessage(remoteJid, message);
|
||||
const options = await this.getMessageOptions(request);
|
||||
const result = await this.sock.sendMessage(remoteJid, message, options);
|
||||
return this.toWAMessage(result);
|
||||
}
|
||||
|
||||
async reply(request: MessageReplyRequest) {
|
||||
const { id } = parseMessageId(request.reply_to);
|
||||
const quotedMessage = await this.store.loadMessage(
|
||||
toJID(request.chatId),
|
||||
id,
|
||||
);
|
||||
const options = await this.getMessageOptions(request);
|
||||
const message = {
|
||||
text: request.text,
|
||||
mentions: request.mentions?.map(toJID),
|
||||
};
|
||||
return await this.sock.sendMessage(request.chatId, message, {
|
||||
quoted: quotedMessage,
|
||||
});
|
||||
return await this.sock.sendMessage(request.chatId, message, options);
|
||||
}
|
||||
|
||||
sendImage(request: MessageImageRequest) {
|
||||
@@ -623,11 +632,14 @@ export class WhatsappSessionNoWebCore extends WhatsappSession {
|
||||
}
|
||||
|
||||
async sendSeen(request: SendSeenRequest) {
|
||||
const key = parseMessageId(request.messageId);
|
||||
const key = parseMessageIdSerialized(request.messageId);
|
||||
const participant = request.participant
|
||||
? toJID(this.ensureSuffix(request.participant))
|
||||
: undefined;
|
||||
const data = {
|
||||
remoteJid: key.remoteJid,
|
||||
id: key.id,
|
||||
participant: request.participant,
|
||||
participant: participant,
|
||||
};
|
||||
return this.sock.readMessages([data]);
|
||||
}
|
||||
@@ -654,19 +666,18 @@ export class WhatsappSessionNoWebCore extends WhatsappSession {
|
||||
toJID(chatId),
|
||||
toNumber(limit),
|
||||
);
|
||||
const result = [];
|
||||
|
||||
const promises = [];
|
||||
for (const msg of messages) {
|
||||
const wamsg = await this.processIncomingMessage(msg, downloadMedia);
|
||||
if (!wamsg) {
|
||||
continue;
|
||||
}
|
||||
result.push(wamsg);
|
||||
promises.push(this.processIncomingMessage(msg, downloadMedia));
|
||||
}
|
||||
let result = await Promise.all(promises);
|
||||
result = result.filter(Boolean);
|
||||
return result;
|
||||
}
|
||||
|
||||
async setReaction(request: MessageReactionRequest) {
|
||||
const key = parseMessageId(request.messageId);
|
||||
const key = parseMessageIdSerialized(request.messageId);
|
||||
const reactionMessage = {
|
||||
react: {
|
||||
text: request.reaction,
|
||||
@@ -677,7 +688,7 @@ export class WhatsappSessionNoWebCore extends WhatsappSession {
|
||||
}
|
||||
|
||||
async setStar(request: MessageStarRequest) {
|
||||
const key = parseMessageId(request.messageId);
|
||||
const key = parseMessageIdSerialized(request.messageId);
|
||||
await this.sock.chatModify(
|
||||
{
|
||||
star: {
|
||||
@@ -700,6 +711,73 @@ export class WhatsappSessionNoWebCore extends WhatsappSession {
|
||||
return chats;
|
||||
}
|
||||
|
||||
protected async chatsPutArchive(
|
||||
chatId: string,
|
||||
archive: boolean,
|
||||
): Promise<any> {
|
||||
const jid = toJID(chatId);
|
||||
const messages = await this.store.getMessagesByJid(jid, 1);
|
||||
return await this.sock.chatModify(
|
||||
{ archive: archive, lastMessages: messages },
|
||||
jid,
|
||||
);
|
||||
}
|
||||
|
||||
public chatsArchiveChat(chatId: string): Promise<any> {
|
||||
return this.chatsPutArchive(chatId, true);
|
||||
}
|
||||
|
||||
public chatsUnarchiveChat(chatId: string): Promise<any> {
|
||||
return this.chatsPutArchive(chatId, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Labels methods
|
||||
*/
|
||||
|
||||
public async getLabels(): Promise<Label[]> {
|
||||
const labels = await this.store.getLabels();
|
||||
return labels.map(this.toLabel);
|
||||
}
|
||||
|
||||
public async getChatsByLabelId(labelId: string) {
|
||||
const chats = await this.store.getChatsByLabelId(labelId);
|
||||
// Remove unreadCount, it's not ready yet
|
||||
chats.forEach((chat) => delete chat.unreadCount);
|
||||
return chats;
|
||||
}
|
||||
|
||||
public async getChatLabels(chatId: string): Promise<Label[]> {
|
||||
const jid = toJID(chatId);
|
||||
const labels = await this.store.getChatLabels(jid);
|
||||
return labels.map(this.toLabel);
|
||||
}
|
||||
|
||||
public async putLabelsToChat(chatId: string, labels: LabelID[]) {
|
||||
const jid = toJID(chatId);
|
||||
const labelsIds = labels.map((label) => label.id);
|
||||
const currentLabels = await this.store.getChatLabels(jid);
|
||||
const currentLabelsIds = currentLabels.map((label) => label.id);
|
||||
const addLabelsIds = lodash.difference(labelsIds, currentLabelsIds);
|
||||
const removeLabelsIds = lodash.difference(currentLabelsIds, labelsIds);
|
||||
for (const labelId of addLabelsIds) {
|
||||
await this.sock.addChatLabel(jid, labelId);
|
||||
}
|
||||
for (const labelId of removeLabelsIds) {
|
||||
await this.sock.removeChatLabel(jid, labelId);
|
||||
}
|
||||
}
|
||||
|
||||
protected toLabel(label: NOWEBLabel): Label {
|
||||
const color = label.color;
|
||||
return {
|
||||
id: label.id,
|
||||
name: label.name,
|
||||
color: color,
|
||||
colorHex: Label.toHex(color),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Contacts methods
|
||||
*/
|
||||
@@ -849,13 +927,7 @@ export class WhatsappSessionNoWebCore extends WhatsappSession {
|
||||
|
||||
public deleteStatus(request: DeleteStatusRequest) {
|
||||
const messageId = request.id;
|
||||
let key: any;
|
||||
if (messageId.includes('_')) {
|
||||
key = parseMessageId(messageId);
|
||||
} else {
|
||||
key = { id: messageId };
|
||||
}
|
||||
|
||||
const key = parseMessageIdSerialized(messageId, true);
|
||||
key.fromMe = true;
|
||||
key.remoteJid = BROADCAST_ID;
|
||||
const JIDs = request.contacts.map(toJID);
|
||||
@@ -935,19 +1007,19 @@ export class WhatsappSessionNoWebCore extends WhatsappSession {
|
||||
}
|
||||
|
||||
public async channelsFollowChannel(id: string): Promise<void> {
|
||||
return await this.sock.newsletterFollow(id);
|
||||
return await this.sock.newsletterAction(id, 'follow');
|
||||
}
|
||||
|
||||
public async channelsUnfollowChannel(id: string): Promise<void> {
|
||||
return await this.sock.newsletterUnfollow(id);
|
||||
return await this.sock.newsletterAction(id, 'unfollow');
|
||||
}
|
||||
|
||||
public async channelsMuteChannel(id: string): Promise<void> {
|
||||
return await this.sock.newsletterMute(id);
|
||||
return await this.sock.newsletterAction(id, 'mute');
|
||||
}
|
||||
|
||||
public async channelsUnmuteChannel(id: string): Promise<void> {
|
||||
return await this.sock.newsletterUnmute(id);
|
||||
return await this.sock.newsletterAction(id, 'unmute');
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1013,6 +1085,99 @@ export class WhatsappSessionNoWebCore extends WhatsappSession {
|
||||
);
|
||||
});
|
||||
return true;
|
||||
case WAHAEvents.CALL_RECEIVED:
|
||||
this.sock.ev.on('call', (calls: WACallEvent[]) => {
|
||||
calls = lodash.filter(calls, { status: 'offer' });
|
||||
for (const call of calls) {
|
||||
const body = this.toCallData(call);
|
||||
handler(body);
|
||||
}
|
||||
});
|
||||
return true;
|
||||
case WAHAEvents.CALL_ACCEPTED:
|
||||
this.sock.ev.on('call', (calls: WACallEvent[]) => {
|
||||
calls = lodash.filter(calls, { status: 'accept' });
|
||||
for (const call of calls) {
|
||||
const body = this.toCallData(call);
|
||||
handler(body);
|
||||
}
|
||||
});
|
||||
return true;
|
||||
case WAHAEvents.CALL_REJECTED:
|
||||
this.sock.ev.on('call', (calls: WACallEvent[]) => {
|
||||
const acceptCalls = lodash.filter(calls, { status: 'accept' });
|
||||
if (acceptCalls.length > 0) {
|
||||
// We got two events when accepting calls - reject and accept
|
||||
// Like for each device
|
||||
// So if we see accepted call - ignore rejected
|
||||
return;
|
||||
}
|
||||
|
||||
calls = lodash.filter(calls, { status: 'reject' });
|
||||
for (const call of calls) {
|
||||
const body = this.toCallData(call);
|
||||
if (body.isGroup == null) {
|
||||
// We get two "reject" events, one with null property, ignore it
|
||||
return;
|
||||
}
|
||||
handler(body);
|
||||
}
|
||||
});
|
||||
return true;
|
||||
case WAHAEvents.LABEL_UPSERT:
|
||||
this.sock.ev.on('labels.edit', (data: NOWEBLabel) => {
|
||||
if (data.deleted) {
|
||||
return;
|
||||
}
|
||||
const body = this.toLabel(data);
|
||||
handler(body);
|
||||
});
|
||||
return true;
|
||||
case WAHAEvents.LABEL_DELETED:
|
||||
this.sock.ev.on('labels.edit', (data: NOWEBLabel) => {
|
||||
if (!data.deleted) {
|
||||
return;
|
||||
}
|
||||
const body = this.toLabel(data);
|
||||
handler(body);
|
||||
});
|
||||
return true;
|
||||
case WAHAEvents.LABEL_CHAT_ADDED:
|
||||
this.sock.ev.on('labels.association', async ({ association, type }) => {
|
||||
if (type !== 'add') {
|
||||
return;
|
||||
}
|
||||
if (association.type !== LabelAssociationType.Chat) {
|
||||
return;
|
||||
}
|
||||
const labelData = await this.store.getLabelById(association.labelId);
|
||||
const label = labelData ? this.toLabel(labelData) : null;
|
||||
const body: LabelChatAssociation = {
|
||||
labelId: association.labelId,
|
||||
chatId: toCusFormat(association.chatId),
|
||||
label: label,
|
||||
};
|
||||
handler(body);
|
||||
});
|
||||
return true;
|
||||
case WAHAEvents.LABEL_CHAT_DELETED:
|
||||
this.sock.ev.on('labels.association', async ({ association, type }) => {
|
||||
if (type !== 'remove') {
|
||||
return;
|
||||
}
|
||||
if (association.type !== LabelAssociationType.Chat) {
|
||||
return;
|
||||
}
|
||||
const labelData = await this.store.getLabelById(association.labelId);
|
||||
const label = labelData ? this.toLabel(labelData) : null;
|
||||
const body: LabelChatAssociation = {
|
||||
labelId: association.labelId,
|
||||
chatId: toCusFormat(association.chatId),
|
||||
label: label,
|
||||
};
|
||||
handler(body);
|
||||
});
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
@@ -1092,18 +1257,8 @@ export class WhatsappSessionNoWebCore extends WhatsappSession {
|
||||
protected toWAMessage(message): Promise<WAMessage> {
|
||||
const fromToParticipant = getFromToParticipant(message);
|
||||
const id = buildMessageId(message.key);
|
||||
let body = message.message.conversation;
|
||||
if (!body) {
|
||||
// Some of the messages have no conversation, but instead have text in extendedTextMessage
|
||||
// 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;
|
||||
}
|
||||
const body = this.extractBody(message.message);
|
||||
const replyTo = this.extractReplyTo(message.message);
|
||||
const ack = message.ack || message.status - 1;
|
||||
return Promise.resolve({
|
||||
id: id,
|
||||
@@ -1123,10 +1278,45 @@ export class WhatsappSessionNoWebCore extends WhatsappSession {
|
||||
ackName: WAMessageAck[ack] || ACK_UNKNOWN,
|
||||
location: message.location,
|
||||
vCards: message.vCards,
|
||||
replyTo: replyTo,
|
||||
_data: message,
|
||||
});
|
||||
}
|
||||
|
||||
protected extractBody(message): string | null {
|
||||
if (!message) {
|
||||
return null;
|
||||
}
|
||||
let body = message.conversation;
|
||||
if (!body) {
|
||||
// Some of the messages have no conversation, but instead have text in extendedTextMessage
|
||||
// https://github.com/devlikeapro/waha/issues/90
|
||||
body = message.extendedTextMessage?.text;
|
||||
}
|
||||
if (!body) {
|
||||
// Populate from caption
|
||||
const mediaContent = extractMediaContent(message);
|
||||
// @ts-ignore - AudioMessage doesn't have caption field
|
||||
body = mediaContent?.caption;
|
||||
}
|
||||
return body;
|
||||
}
|
||||
|
||||
protected extractReplyTo(message): ReplyToMessage | null {
|
||||
const msgType = getContentType(message);
|
||||
const contextInfo = message[msgType]?.contextInfo;
|
||||
if (!contextInfo) {
|
||||
return null;
|
||||
}
|
||||
const quotedMessage = contextInfo.quotedMessage;
|
||||
const body = this.extractBody(quotedMessage);
|
||||
return {
|
||||
id: contextInfo.stanzaId,
|
||||
participant: toCusFormat(contextInfo.participant),
|
||||
body: body,
|
||||
};
|
||||
}
|
||||
|
||||
protected toWAContact(contact: Contact) {
|
||||
contact.id = toCusFormat(contact.id);
|
||||
// @ts-ignore
|
||||
@@ -1250,6 +1440,20 @@ export class WhatsappSessionNoWebCore extends WhatsappSession {
|
||||
handler(payload);
|
||||
}
|
||||
|
||||
private toCallData(call: WACallEvent): CallData {
|
||||
// call.date can be either string 2024-07-18T09:45:55.000Z or Date
|
||||
const date = new Date(call.date);
|
||||
// convert to timestamp in seconds
|
||||
const timestamp: number = date.getTime() / 1000;
|
||||
return {
|
||||
id: call.id,
|
||||
from: toCusFormat(call.from),
|
||||
timestamp: timestamp,
|
||||
isVideo: call.isVideo,
|
||||
isGroup: call.isGroup,
|
||||
};
|
||||
}
|
||||
|
||||
private toWahaPresences(
|
||||
remoteJid: string,
|
||||
storedPresences: { [participant: string]: PresenceData },
|
||||
@@ -1276,11 +1480,23 @@ export class WhatsappSessionNoWebCore extends WhatsappSession {
|
||||
|
||||
protected downloadMedia(message) {
|
||||
const processor = new EngineMediaProcessor(this);
|
||||
return this.mediaManager.processMedia(processor, message);
|
||||
return this.mediaManager.processMedia(processor, message, this.name);
|
||||
}
|
||||
|
||||
protected async getMessageOptions(request: any): Promise<any> {
|
||||
let quoted;
|
||||
if (request.reply_to) {
|
||||
const key = parseMessageIdSerialized(request.reply_to, true);
|
||||
quoted = await this.store.loadMessage(toJID(request.chatId), key.id);
|
||||
}
|
||||
|
||||
return {
|
||||
quoted: quoted,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export class EngineMediaProcessor implements IEngineMediaProcessor<any> {
|
||||
export class EngineMediaProcessor implements IMediaEngineProcessor<any> {
|
||||
constructor(public session: WhatsappSessionNoWebCore) {}
|
||||
|
||||
hasMedia(message: any): boolean {
|
||||
@@ -1367,7 +1583,11 @@ function buildMessageId({ id, remoteJid, fromMe }) {
|
||||
* false_11111111111@c.us_AAA
|
||||
* {id: "AAA", remoteJid: "11111111111@s.whatsapp.net", "fromMe": false}
|
||||
*/
|
||||
function parseMessageId(messageId) {
|
||||
function parseMessageIdSerialized(messageId: string, soft: boolean = false) {
|
||||
if (!messageId.includes('_') && soft) {
|
||||
return { id: messageId };
|
||||
}
|
||||
|
||||
const parts = messageId.split('_');
|
||||
if (parts.length != 3) {
|
||||
throw new Error(
|
||||
|
||||
@@ -3,6 +3,8 @@ import { Chat } from '@adiwajshing/baileys';
|
||||
export interface IChatRepository {
|
||||
getAll(): Promise<Chat[]>;
|
||||
|
||||
getAllByIds(ids: string[]): Promise<Chat[]>;
|
||||
|
||||
getAllWithMessages(limit?: number, offset?: number): Promise<Chat[]>;
|
||||
|
||||
getById(id: string): Promise<Chat | null>;
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
import {
|
||||
LabelAssociation,
|
||||
LabelAssociationType,
|
||||
} from '@adiwajshing/baileys/lib/Types/LabelAssociation';
|
||||
|
||||
export interface ILabelAssociationRepository {
|
||||
deleteOne(association: LabelAssociation): Promise<void>;
|
||||
|
||||
save(association: LabelAssociation): Promise<void>;
|
||||
|
||||
deleteByLabelId(labelId: string): Promise<void>;
|
||||
|
||||
getAssociationsByLabelId(
|
||||
labelId: string,
|
||||
type: LabelAssociationType,
|
||||
): Promise<LabelAssociation[]>;
|
||||
|
||||
getAssociationsByChatId(chatId: string): Promise<LabelAssociation[]>;
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
import { Contact } from '@adiwajshing/baileys';
|
||||
import { Label } from '@adiwajshing/baileys/lib/Types/Label';
|
||||
|
||||
export interface ILabelsRepository {
|
||||
getById(id: string): Promise<Label | null>;
|
||||
|
||||
getAll(): Promise<Label[]>;
|
||||
|
||||
getAllByIds(ids: string[]): Promise<Label[]>;
|
||||
|
||||
deleteById(id: string): Promise<void>;
|
||||
|
||||
save(label: Label): Promise<void>;
|
||||
}
|
||||
@@ -1,15 +1,40 @@
|
||||
import { WAMessage } from '@adiwajshing/baileys';
|
||||
import { LabelAssociation } from '@adiwajshing/baileys/lib/Types/LabelAssociation';
|
||||
import { ILabelAssociationRepository } from '@waha/core/engines/noweb/store/ILabelAssociationsRepository';
|
||||
import { ILabelsRepository } from '@waha/core/engines/noweb/store/ILabelsRepository';
|
||||
|
||||
import { IChatRepository } from './IChatRepository';
|
||||
import { IContactRepository } from './IContactRepository';
|
||||
import { IMessagesRepository } from './IMessagesRepository';
|
||||
|
||||
export interface INowebStorage {
|
||||
init(): Promise<void>;
|
||||
export abstract class INowebStorage {
|
||||
abstract init(): Promise<void>;
|
||||
|
||||
close(): Promise<void>;
|
||||
abstract close(): Promise<void>;
|
||||
|
||||
getContactsRepository(): IContactRepository;
|
||||
abstract getContactsRepository(): IContactRepository;
|
||||
|
||||
getChatRepository(): IChatRepository;
|
||||
abstract getChatRepository(): IChatRepository;
|
||||
|
||||
getMessagesRepository(): IMessagesRepository;
|
||||
abstract getMessagesRepository(): IMessagesRepository;
|
||||
|
||||
abstract getLabelsRepository(): ILabelsRepository;
|
||||
|
||||
abstract getLabelAssociationRepository(): ILabelAssociationRepository;
|
||||
|
||||
protected getMessagesMetadata(): Map<string, any> {
|
||||
return new Map()
|
||||
.set('jid', (msg: WAMessage) => msg.key.remoteJid)
|
||||
.set('id', (msg: WAMessage) => msg.key.id)
|
||||
.set('messageTimestamp', (msg: WAMessage) => msg.messageTimestamp);
|
||||
}
|
||||
|
||||
protected getLabelAssociationMetadata() {
|
||||
return new Map().set(
|
||||
'id',
|
||||
(a: LabelAssociation) =>
|
||||
// @ts-ignore
|
||||
`${a.type}_${a.labelId}_${a.chatId}_${a.messageId}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,7 @@ import {
|
||||
Contact,
|
||||
proto,
|
||||
} from '@adiwajshing/baileys';
|
||||
import { Label } from '@adiwajshing/baileys/lib/Types/Label';
|
||||
|
||||
export interface INowebStore {
|
||||
presences: any;
|
||||
@@ -23,4 +24,12 @@ export interface INowebStore {
|
||||
getContacts(): Promise<Contact[]>;
|
||||
|
||||
getContactById(jid: string): Promise<Contact>;
|
||||
|
||||
getLabels(): Promise<Label[]>;
|
||||
|
||||
getLabelById(labelId: string): Promise<Label | null>;
|
||||
|
||||
getChatsByLabelId(labelId: string): Promise<Chat[]>;
|
||||
|
||||
getChatLabels(chatId: string): Promise<Label[]>;
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Chat, Contact, makeInMemoryStore, proto } from '@adiwajshing/baileys';
|
||||
import { BadRequestException, ConsoleLogger } from '@nestjs/common';
|
||||
import { Label } from '@adiwajshing/baileys/lib/Types/Label';
|
||||
import { BadRequestException } from '@nestjs/common';
|
||||
|
||||
import { INowebStore } from './INowebStore';
|
||||
|
||||
@@ -51,4 +52,20 @@ export class NowebInMemoryStore implements INowebStore {
|
||||
getContactById(jid: string): Promise<Contact> {
|
||||
throw new BadRequestException(this.errorMessage);
|
||||
}
|
||||
|
||||
getLabels(): Promise<Label[]> {
|
||||
throw new BadRequestException(this.errorMessage);
|
||||
}
|
||||
|
||||
getLabelById(labelId: string): Promise<Label | null> {
|
||||
throw new BadRequestException(this.errorMessage);
|
||||
}
|
||||
|
||||
getChatsByLabelId(labelId: string): Promise<Chat[]> {
|
||||
throw new BadRequestException(this.errorMessage);
|
||||
}
|
||||
|
||||
getChatLabels(chatId: string): Promise<Label[]> {
|
||||
throw new BadRequestException(this.errorMessage);
|
||||
}
|
||||
}
|
||||
@@ -9,7 +9,13 @@ import {
|
||||
updateMessageWithReaction,
|
||||
updateMessageWithReceipt,
|
||||
} from '@adiwajshing/baileys';
|
||||
import { ConsoleLogger } from '@nestjs/common';
|
||||
import { Label } from '@adiwajshing/baileys/lib/Types/Label';
|
||||
import {
|
||||
LabelAssociation,
|
||||
LabelAssociationType,
|
||||
} from '@adiwajshing/baileys/lib/Types/LabelAssociation';
|
||||
import { ILabelAssociationRepository } from '@waha/core/engines/noweb/store/ILabelAssociationsRepository';
|
||||
import { ILabelsRepository } from '@waha/core/engines/noweb/store/ILabelsRepository';
|
||||
import { toNumber } from 'lodash';
|
||||
import { Logger } from 'pino';
|
||||
|
||||
@@ -28,6 +34,8 @@ export class NowebPersistentStore implements INowebStore {
|
||||
private chatRepo: IChatRepository;
|
||||
private contactRepo: IContactRepository;
|
||||
private messagesRepo: IMessagesRepository;
|
||||
private labelsRepo: ILabelsRepository;
|
||||
private labelAssociationsRepo: ILabelAssociationRepository;
|
||||
public presences: any;
|
||||
private lock: any;
|
||||
|
||||
@@ -39,6 +47,8 @@ export class NowebPersistentStore implements INowebStore {
|
||||
this.chatRepo = storage.getChatRepository();
|
||||
this.contactRepo = storage.getContactsRepository();
|
||||
this.messagesRepo = storage.getMessagesRepository();
|
||||
this.labelsRepo = storage.getLabelsRepository();
|
||||
this.labelAssociationsRepo = storage.getLabelAssociationRepository();
|
||||
this.presences = {};
|
||||
this.lock = new AsyncLock({ maxPending: Infinity });
|
||||
}
|
||||
@@ -83,6 +93,10 @@ export class NowebPersistentStore implements INowebStore {
|
||||
ev.on('contacts.update', (data) =>
|
||||
this.withLock('contacts', () => this.onContactUpdate(data)),
|
||||
);
|
||||
ev.on('labels.edit', (data) => this.onLabelsEdit(data));
|
||||
ev.on('labels.association', ({ association, type }) =>
|
||||
this.onLabelsAssociation(association, type),
|
||||
);
|
||||
// Presence
|
||||
ev.on('presence.update', (data) => this.onPresenceUpdate(data));
|
||||
this.socket = socket;
|
||||
@@ -275,6 +289,26 @@ export class NowebPersistentStore implements INowebStore {
|
||||
}
|
||||
}
|
||||
|
||||
private async onLabelsEdit(label: Label) {
|
||||
if (label.deleted) {
|
||||
await this.labelsRepo.deleteById(label.id);
|
||||
await this.labelAssociationsRepo.deleteByLabelId(label.id);
|
||||
} else {
|
||||
await this.labelsRepo.save(label);
|
||||
}
|
||||
}
|
||||
|
||||
private async onLabelsAssociation(
|
||||
association: LabelAssociation,
|
||||
type: 'add' | 'remove',
|
||||
) {
|
||||
if (type === 'remove') {
|
||||
await this.labelAssociationsRepo.deleteOne(association);
|
||||
} else {
|
||||
await this.labelAssociationsRepo.save(association);
|
||||
}
|
||||
}
|
||||
|
||||
private async onPresenceUpdate({ id, presences: update }) {
|
||||
this.presences[id] = this.presences[id] || {};
|
||||
Object.assign(this.presences[id], update);
|
||||
@@ -303,4 +337,29 @@ export class NowebPersistentStore implements INowebStore {
|
||||
getContacts() {
|
||||
return this.contactRepo.getAll();
|
||||
}
|
||||
|
||||
getLabels(): Promise<Label[]> {
|
||||
return this.labelsRepo.getAll();
|
||||
}
|
||||
|
||||
getLabelById(labelId: string): Promise<Label | null> {
|
||||
return this.labelsRepo.getById(labelId);
|
||||
}
|
||||
|
||||
async getChatsByLabelId(labelId: string): Promise<Chat[]> {
|
||||
const associations =
|
||||
await this.labelAssociationsRepo.getAssociationsByLabelId(
|
||||
labelId,
|
||||
LabelAssociationType.Chat,
|
||||
);
|
||||
const ids = associations.map((association) => association.chatId);
|
||||
return await this.chatRepo.getAllByIds(ids);
|
||||
}
|
||||
|
||||
async getChatLabels(chatId: string): Promise<Label[]> {
|
||||
const associations =
|
||||
await this.labelAssociationsRepo.getAssociationsByChatId(chatId);
|
||||
const ids = associations.map((association) => association.labelId);
|
||||
return await this.labelsRepo.getAllByIds(ids);
|
||||
}
|
||||
}
|
||||
@@ -53,4 +53,26 @@ export const NOWEB_STORE_SCHEMA = [
|
||||
new Index('timestamp_index', ['messageTimestamp']),
|
||||
],
|
||||
),
|
||||
new Schema(
|
||||
'labels',
|
||||
[new Field('id', 'TEXT'), new Field('data', 'TEXT')],
|
||||
[new Index('labels_id_index', ['id'])],
|
||||
),
|
||||
new Schema(
|
||||
'labelAssociations',
|
||||
[
|
||||
new Field('id', 'TEXT'),
|
||||
new Field('type', 'TEXT'),
|
||||
new Field('labelId', 'TEXT'),
|
||||
new Field('chatId', 'TEXT'),
|
||||
new Field('messageId', 'TEXT'),
|
||||
new Field('data', 'TEXT'),
|
||||
],
|
||||
[
|
||||
new Index('label_assoc_id_index', ['id']),
|
||||
new Index('label_assoc_type_label_index', ['type', 'labelId']),
|
||||
new Index('label_assoc_type_chat_index', ['type', 'chatId']),
|
||||
new Index('label_assoc_type_message_index', ['type', 'messageId']),
|
||||
],
|
||||
),
|
||||
];
|
||||
@@ -38,6 +38,15 @@ export class Sqlite3KVRepository<Entity> {
|
||||
return this.all(this.select());
|
||||
}
|
||||
|
||||
getAllByIds(ids: string[]) {
|
||||
return this.all(this.select().whereIn('id', ids));
|
||||
}
|
||||
|
||||
protected getAllBy(filters: any) {
|
||||
const query = this.select().where(filters);
|
||||
return this.all(query);
|
||||
}
|
||||
|
||||
protected async getBy(filters: any) {
|
||||
const query = this.select().where(filters).limit(1);
|
||||
return this.get(query);
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
import {
|
||||
LabelAssociation,
|
||||
LabelAssociationType,
|
||||
} from '@adiwajshing/baileys/lib/Types/LabelAssociation';
|
||||
import { ILabelAssociationRepository } from '@waha/core/engines/noweb/store/ILabelAssociationsRepository';
|
||||
|
||||
import { Sqlite3KVRepository } from './Sqlite3KVRepository';
|
||||
|
||||
export class Sqlite3LabelAssociationsRepository
|
||||
extends Sqlite3KVRepository<LabelAssociation>
|
||||
implements ILabelAssociationRepository
|
||||
{
|
||||
async deleteOne(association: LabelAssociation): Promise<void> {
|
||||
await this.deleteBy({
|
||||
type: association.type,
|
||||
chatId: association.chatId,
|
||||
labelId: association.labelId,
|
||||
// @ts-ignore: messageId doesn't existing in ChatLabelAssociation
|
||||
messageId: association.messageId || null,
|
||||
});
|
||||
}
|
||||
|
||||
async deleteByLabelId(labelId: string): Promise<void> {
|
||||
await this.deleteBy({ labelId: labelId });
|
||||
}
|
||||
|
||||
getAssociationsByLabelId(
|
||||
labelId: string,
|
||||
type: LabelAssociationType,
|
||||
): Promise<LabelAssociation[]> {
|
||||
return this.getAllBy({
|
||||
type: type,
|
||||
labelId: labelId,
|
||||
});
|
||||
}
|
||||
|
||||
getAssociationsByChatId(chatId: string): Promise<LabelAssociation[]> {
|
||||
return this.getAllBy({ chatId: chatId, type: LabelAssociationType.Chat });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
import { Label } from '@adiwajshing/baileys/lib/Types/Label';
|
||||
import { ILabelsRepository } from '@waha/core/engines/noweb/store/ILabelsRepository';
|
||||
|
||||
import { Sqlite3KVRepository } from './Sqlite3KVRepository';
|
||||
|
||||
export class Sqlite3LabelsRepository
|
||||
extends Sqlite3KVRepository<Label>
|
||||
implements ILabelsRepository {}
|
||||
@@ -1,4 +1,9 @@
|
||||
import { WAMessage } from '@adiwajshing/baileys';
|
||||
import { LabelAssociation } from '@adiwajshing/baileys/lib/Types/LabelAssociation';
|
||||
import { ILabelAssociationRepository } from '@waha/core/engines/noweb/store/ILabelAssociationsRepository';
|
||||
import { ILabelsRepository } from '@waha/core/engines/noweb/store/ILabelsRepository';
|
||||
import { Sqlite3LabelAssociationsRepository } from '@waha/core/engines/noweb/store/sqlite3/Sqlite3LabelAssociationsRepository';
|
||||
import { Sqlite3LabelsRepository } from '@waha/core/engines/noweb/store/sqlite3/Sqlite3LabelsRepository';
|
||||
|
||||
import { INowebStorage } from '../INowebStorage';
|
||||
import { Field, Index, NOWEB_STORE_SCHEMA, Schema } from '../Schema';
|
||||
@@ -10,11 +15,12 @@ import { Sqlite3SchemaValidation } from './Sqlite3SchemaValidation';
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
const Database = require('better-sqlite3');
|
||||
|
||||
export class Sqlite3Storage implements INowebStorage {
|
||||
export class Sqlite3Storage extends INowebStorage {
|
||||
private readonly db: any;
|
||||
private readonly tables: Schema[];
|
||||
|
||||
constructor(filePath: string) {
|
||||
super();
|
||||
this.db = new Database(filePath);
|
||||
this.tables = NOWEB_STORE_SCHEMA;
|
||||
}
|
||||
@@ -71,6 +77,33 @@ export class Sqlite3Storage implements INowebStorage {
|
||||
this.db.exec(
|
||||
'CREATE INDEX IF NOT EXISTS timestamp_index ON messages (messageTimestamp)',
|
||||
);
|
||||
|
||||
//
|
||||
// Labels
|
||||
//
|
||||
this.db.exec(
|
||||
'CREATE TABLE IF NOT EXISTS labels (id TEXT PRIMARY KEY, data TEXT)',
|
||||
);
|
||||
this.db.exec(
|
||||
'CREATE UNIQUE INDEX IF NOT EXISTS labels_id_index ON labels (id)',
|
||||
);
|
||||
|
||||
// Label associations
|
||||
this.db.exec(
|
||||
'CREATE TABLE IF NOT EXISTS labelAssociations (id TEXT PRIMARY KEY, type TEXT, labelId TEXT, chatId TEXT, messageId TEXT, data TEXT)',
|
||||
);
|
||||
this.db.exec(
|
||||
'CREATE UNIQUE INDEX IF NOT EXISTS label_assoc_id_index ON labelAssociations (id)',
|
||||
);
|
||||
this.db.exec(
|
||||
'CREATE INDEX IF NOT EXISTS label_assoc_type_label_index ON labelAssociations (type, labelId)',
|
||||
);
|
||||
this.db.exec(
|
||||
'CREATE INDEX IF NOT EXISTS label_assoc_type_chat_index ON labelAssociations (type, chatId)',
|
||||
);
|
||||
this.db.exec(
|
||||
'CREATE INDEX IF NOT EXISTS label_assoc_type_message_index ON labelAssociations (type, messageId)',
|
||||
);
|
||||
}
|
||||
|
||||
async close() {
|
||||
@@ -85,11 +118,21 @@ export class Sqlite3Storage implements INowebStorage {
|
||||
return new Sqlite3ChatRepository(this.db, this.getSchema('chats'));
|
||||
}
|
||||
|
||||
getLabelsRepository(): ILabelsRepository {
|
||||
return new Sqlite3LabelsRepository(this.db, this.getSchema('labels'));
|
||||
}
|
||||
|
||||
getLabelAssociationRepository(): ILabelAssociationRepository {
|
||||
const metadata = this.getLabelAssociationMetadata();
|
||||
return new Sqlite3LabelAssociationsRepository(
|
||||
this.db,
|
||||
this.getSchema('labelAssociations'),
|
||||
metadata,
|
||||
);
|
||||
}
|
||||
|
||||
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);
|
||||
const metadata = this.getMessagesMetadata();
|
||||
return new Sqlite3MessagesRepository(
|
||||
this.db,
|
||||
this.getSchema('messages'),
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { UnprocessableEntityException } from '@nestjs/common/exceptions/unprocessable-entity.exception';
|
||||
import { IMediaEngineProcessor } from '@waha/core/media/IMediaEngineProcessor';
|
||||
import * as Buffer from 'buffer';
|
||||
import { create, CreateConfig, Message, Whatsapp } from 'venom-bot';
|
||||
|
||||
@@ -21,7 +22,6 @@ import {
|
||||
WAHASessionStatus,
|
||||
} from '../../../structures/enums.dto';
|
||||
import { WAMessage } from '../../../structures/responses.dto';
|
||||
import { IEngineMediaProcessor } from '../../abc/media.abc';
|
||||
import { WAHAInternalEvent, WhatsappSession } from '../../abc/session.abc';
|
||||
import { NotImplementedByEngineError } from '../../exceptions';
|
||||
import { QR } from '../../QR';
|
||||
@@ -248,7 +248,7 @@ export class WhatsappSessionVenomCore extends WhatsappSession {
|
||||
|
||||
protected downloadMedia(message: Message) {
|
||||
const processor = new EngineMediaProcessor(this);
|
||||
return this.mediaManager.processMedia(processor, message);
|
||||
return this.mediaManager.processMedia(processor, message, this.name);
|
||||
}
|
||||
|
||||
private processIncomingMessage(message: Message) {
|
||||
@@ -280,7 +280,7 @@ export class WhatsappSessionVenomCore extends WhatsappSession {
|
||||
}
|
||||
}
|
||||
|
||||
export class EngineMediaProcessor implements IEngineMediaProcessor<Message> {
|
||||
export class EngineMediaProcessor implements IMediaEngineProcessor<Message> {
|
||||
constructor(public session: WhatsappSessionVenomCore) {}
|
||||
|
||||
hasMedia(message: any): boolean {
|
||||
|
||||
@@ -1,535 +0,0 @@
|
||||
<!doctype html>
|
||||
<html class="no-js" dir="ltr" loc="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1" />
|
||||
<title>WhatsApp Web</title>
|
||||
<meta name="viewport" content="width=device-width" />
|
||||
<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.facebook.com/images/whatsapp/www/whatsapp-promo.png"
|
||||
/>
|
||||
<link
|
||||
id="favicon"
|
||||
rel="shortcut icon"
|
||||
href="/img/favicon/1x/favicon.png"
|
||||
type="image/png"
|
||||
/>
|
||||
<link
|
||||
rel="apple-touch-icon"
|
||||
sizes="192x192"
|
||||
href="/apple-touch-icon.png"
|
||||
type="image/png"
|
||||
/>
|
||||
<meta
|
||||
name="theme-color"
|
||||
content="#111b21"
|
||||
media="(prefers-color-scheme: dark)"
|
||||
/>
|
||||
<meta name="theme-color" content="#f0f2f5" />
|
||||
<link
|
||||
id="whatsapp-pwa-manifest-link"
|
||||
rel="manifest"
|
||||
href="/manifest.json"
|
||||
crossorigin="use-credentials"
|
||||
/>
|
||||
<style>
|
||||
#initial_startup {
|
||||
--startup-background: #f0f2f5;
|
||||
--startup-background-rgb: 240, 242, 245;
|
||||
--startup-icon: #bbc5cb;
|
||||
--secondary-lighter: #8696a0;
|
||||
--primary-title: #41525d;
|
||||
--progress-primary: #00c298;
|
||||
--progress-background: #e9edef;
|
||||
}
|
||||
.dark #initial_startup {
|
||||
--startup-background: #111b21;
|
||||
--startup-background-rgb: 17, 27, 33;
|
||||
--startup-icon: #676f73;
|
||||
--primary-title: rgba(233, 237, 239, 0.88);
|
||||
--secondary-lighter: #667781;
|
||||
--progress-primary: #0b846d;
|
||||
--progress-background: #233138;
|
||||
}
|
||||
#app,
|
||||
body,
|
||||
html {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
#app {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
}
|
||||
#initial_startup {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
user-select: none;
|
||||
background-color: var(--startup-background);
|
||||
}
|
||||
#initial_startup .graphic {
|
||||
margin-top: -40px;
|
||||
color: var(--startup-icon);
|
||||
}
|
||||
#initial_startup .graphic .resume-logo {
|
||||
transform: translateX(calc(50% - 52px / 2));
|
||||
}
|
||||
#initial_startup .graphic::after {
|
||||
position: relative;
|
||||
top: -100%;
|
||||
left: calc(50% - 72px * 2 - 72px / 2);
|
||||
display: block;
|
||||
width: calc(72px * 3);
|
||||
height: 100%;
|
||||
content: '';
|
||||
background: linear-gradient(
|
||||
to right,
|
||||
rgba(var(--startup-background-rgb), 0.5) 0,
|
||||
rgba(var(--startup-background-rgb), 0.5) 33.33%,
|
||||
rgba(var(--startup-background-rgb), 0) 44.1%,
|
||||
rgba(var(--startup-background-rgb), 0) 55.8%,
|
||||
rgba(var(--startup-background-rgb), 0.5) 66.66%,
|
||||
rgba(var(--startup-background-rgb), 0.5) 100%
|
||||
);
|
||||
opacity: 1;
|
||||
animation: shimmer 1.5s linear 0.6s infinite;
|
||||
}
|
||||
html[dir='rtl'] #initial_startup .graphic::after {
|
||||
animation-direction: reverse;
|
||||
}
|
||||
@keyframes shimmer {
|
||||
from {
|
||||
left: calc(50% - 72px * 2 - 72px / 2);
|
||||
}
|
||||
to {
|
||||
left: calc(50% - 72px / 2);
|
||||
}
|
||||
}
|
||||
#initial_startup .progress {
|
||||
position: relative;
|
||||
width: 420px;
|
||||
height: 3px;
|
||||
margin-top: 40px;
|
||||
}
|
||||
#initial_startup .progress progress {
|
||||
vertical-align: top;
|
||||
}
|
||||
#initial_startup .main {
|
||||
margin-top: 40px;
|
||||
font-size: 17px;
|
||||
color: var(--primary-title);
|
||||
}
|
||||
#initial_startup .secondary {
|
||||
margin-top: 12px;
|
||||
font-size: 14px;
|
||||
color: var(--secondary-lighter);
|
||||
}
|
||||
#initial_startup .secondary span {
|
||||
display: inline-block;
|
||||
margin-bottom: 2px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
progress {
|
||||
-webkit-appearance: none;
|
||||
appearance: none;
|
||||
width: 100%;
|
||||
height: 3px;
|
||||
margin: 0;
|
||||
color: var(--progress-primary);
|
||||
background-color: var(--progress-background);
|
||||
border: none;
|
||||
}
|
||||
progress[value]::-webkit-progress-bar {
|
||||
background-color: var(--progress-background);
|
||||
}
|
||||
progress[value]::-moz-progress-bar,
|
||||
progress[value]::-webkit-progress-value {
|
||||
background-color: var(--progress-primary);
|
||||
transition: width 0.45s ease;
|
||||
}
|
||||
</style>
|
||||
<link
|
||||
href="/stylex-f0601c57714fe972a8c4d2c2562f8082.css"
|
||||
rel="stylesheet"
|
||||
/>
|
||||
<link href="/app-6113f1562d101247269b.css" rel="stylesheet" />
|
||||
</head>
|
||||
<body class="web">
|
||||
<script
|
||||
data-binary-transparency-hash-key="inline-js-d11c72a06542fc3b43af600028cf0f996e4b9b178dfa047e18730efaf74aab0b"
|
||||
>
|
||||
try {
|
||||
var systemThemeDark,
|
||||
theme = window.localStorage.getItem('theme'),
|
||||
systemThemeMode = window.localStorage.getItem('system-theme-mode');
|
||||
if (('true' === systemThemeMode || !theme) && window.matchMedia) {
|
||||
var systemTheme = window.matchMedia('(prefers-color-scheme: dark)');
|
||||
systemThemeDark = systemTheme && systemTheme.matches;
|
||||
}
|
||||
var darkTheme = '"dark"' === theme || Boolean(systemThemeDark);
|
||||
darkTheme && document.body.classList.add('dark');
|
||||
} catch (e) {}
|
||||
</script>
|
||||
<div id="app"></div>
|
||||
<div id="hard_expire_time" data-time="1719543128.405"></div>
|
||||
<div id="initial_startup">
|
||||
<div class="graphic">
|
||||
<span
|
||||
><svg width="250" height="52" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
class="resume-logo"
|
||||
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="progress">
|
||||
<progress value="0" max="100" dir="ltr"></progress>
|
||||
</div>
|
||||
<div class="main">WhatsApp</div>
|
||||
<div class="secondary">
|
||||
<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
|
||||
> End-to-end encrypted
|
||||
</div>
|
||||
</div>
|
||||
<link
|
||||
rel="preload"
|
||||
crossorigin="anonymous"
|
||||
as="fetch"
|
||||
href="/binary-transparency-manifest-2.2412.54.json"
|
||||
id="binary-transparency-manifest-preload"
|
||||
/>
|
||||
<script src="/libsignal-protocol-ee5b8ba.min.js"></script>
|
||||
<script defer="defer" src="/runtime.295f0230b6dbd8983513.js"></script>
|
||||
<script
|
||||
data-binary-transparency-hash-key="inline-js-cdf06293c99d00ee0c1bc0fcfdbd76a7ae9aef6290926dbff0cba3a7994b7fca"
|
||||
>
|
||||
/*! Copyright (c) 2024 WhatsApp Inc. All Rights Reserved. */
|
||||
(self.webpackChunkwhatsapp_web_client =
|
||||
self.webpackChunkwhatsapp_web_client || []).push([
|
||||
[5617],
|
||||
{
|
||||
307914: (e) => {
|
||||
e.exports = function (e) {
|
||||
return e && e.__esModule ? e : { default: e };
|
||||
};
|
||||
},
|
||||
595318: (e) => {
|
||||
(e.exports = function (e) {
|
||||
return e && e.__esModule ? e : { default: e };
|
||||
}),
|
||||
(e.exports.default = e.exports),
|
||||
(e.exports.__esModule = !0);
|
||||
},
|
||||
415227: (e, t) => {
|
||||
'use strict';
|
||||
Object.defineProperty(t, '__esModule', { value: !0 }),
|
||||
(t.default = function (e) {
|
||||
const t = new Error(e);
|
||||
if (void 0 === t.stack)
|
||||
try {
|
||||
throw t;
|
||||
} catch (e) {}
|
||||
return t;
|
||||
});
|
||||
},
|
||||
670983: (e, t, r) => {
|
||||
'use strict';
|
||||
var n = r(307914);
|
||||
Object.defineProperty(t, '__esModule', { value: !0 }),
|
||||
(t.default = function (e) {
|
||||
let t =
|
||||
arguments.length > 1 && void 0 !== arguments[1]
|
||||
? arguments[1]
|
||||
: '?';
|
||||
if (null == e)
|
||||
throw (0, o.default)('Unexpected null or undefined: ' + t);
|
||||
return e;
|
||||
});
|
||||
var o = n(r(415227));
|
||||
},
|
||||
250473: (e, t, r) => {
|
||||
'use strict';
|
||||
Object.defineProperty(t, '__esModule', { value: !0 }),
|
||||
(t.UNIQUE_UPLOADS_ALLOWED_BEFORE_THROTTLE =
|
||||
t.TICKET_URL =
|
||||
t.CLB_URL =
|
||||
t.CLB_TOKEN =
|
||||
t.CLB_CHECK_URL =
|
||||
void 0);
|
||||
var n = r(508247);
|
||||
(t.CLB_CHECK_URL =
|
||||
'https://crashlogs.whatsapp.net/wa_fls_upload_check'),
|
||||
(t.CLB_TOKEN =
|
||||
'1063127757113399|745146ffa34413f9dbb5469f5370b7af'),
|
||||
(t.CLB_URL = 'https://crashlogs.whatsapp.net/wa_clb_data');
|
||||
const o = n.DYN_ORIGIN + 'web-contact-us';
|
||||
(t.TICKET_URL = o), (t.UNIQUE_UPLOADS_ALLOWED_BEFORE_THROTTLE = 0);
|
||||
},
|
||||
207024: (e, t, r) => {
|
||||
'use strict';
|
||||
var n = r(595318);
|
||||
Object.defineProperty(t, '__esModule', { value: !0 }),
|
||||
(t.getDistribution = function () {
|
||||
let e = 'unknown';
|
||||
return (e = 'prod'), 'web_prod';
|
||||
}),
|
||||
(t.getLogUserAgent = function (e) {
|
||||
let t,
|
||||
{ appVersion: r, browser: n, device: o } = e;
|
||||
return (t = 'Web/' + n), `WhatsApp/${r} ${t} Device/${o}`;
|
||||
}),
|
||||
n(r(472514)),
|
||||
n(r(556869));
|
||||
},
|
||||
794858: (e, t, r) => {
|
||||
'use strict';
|
||||
var n = r(595318),
|
||||
o = n(r(670983)),
|
||||
a = r(508247),
|
||||
s = r(250473),
|
||||
c = r(207024),
|
||||
_ = n(r(174285)),
|
||||
u = r(425017);
|
||||
function i(e) {
|
||||
!(function (e, t) {
|
||||
const r = window.navigator.userAgent;
|
||||
if (r === p && e.includes('getElementsByTagName')) return;
|
||||
const n = new FormData(),
|
||||
i = new Blob([e], { type: 'text/plain' });
|
||||
n.append(
|
||||
'from_jid',
|
||||
(function () {
|
||||
if (f) return f;
|
||||
try {
|
||||
(f = JSON.parse(
|
||||
(0, o.default)(_.default, 'localStorage').getItem(
|
||||
d.UNKNOWN_ID,
|
||||
),
|
||||
)),
|
||||
f && (f = f.replace('-', ''));
|
||||
} catch (e) {}
|
||||
if (!f) {
|
||||
f = 'unknown' + Math.floor(1e10 * Math.random());
|
||||
const e = f;
|
||||
try {
|
||||
(0, o.default)(_.default, 'localStorage').setItem(
|
||||
d.UNKNOWN_ID,
|
||||
JSON.stringify(e),
|
||||
);
|
||||
} catch (e) {}
|
||||
}
|
||||
return (0, o.default)(f, 'id');
|
||||
})(),
|
||||
),
|
||||
n.append(
|
||||
'agent',
|
||||
(0, c.getLogUserAgent)(
|
||||
(0, u.parseUASimple)(r, a.VERSION_BASE),
|
||||
),
|
||||
),
|
||||
n.append('file', i, 'logs.txt'),
|
||||
n.append('tags', 'load');
|
||||
const l = new XMLHttpRequest(),
|
||||
I =
|
||||
s.CLB_URL +
|
||||
'?access_token=' +
|
||||
encodeURIComponent(s.CLB_TOKEN);
|
||||
l.open('POST', I, !0), l.send(n);
|
||||
})(e);
|
||||
}
|
||||
function l(e) {
|
||||
let { error: t, reason: r, stack: n } = e;
|
||||
const o = new Date().toISOString();
|
||||
return `${o}: error: ${t}\n${o}: reason for logs: ${r}\n${o}: userAgent: ${window.navigator.userAgent}\n${n}`;
|
||||
}
|
||||
null == window.onerror &&
|
||||
(window.onerror = function (e, t, r) {
|
||||
const n = t.split('?')[0];
|
||||
return "Uncaught SyntaxError: Unexpected token '<'" === e
|
||||
? ((function (e) {
|
||||
i(
|
||||
l({
|
||||
error: 'failed to load a js or css bundle',
|
||||
reason: `failed to load [${e
|
||||
.split('/')[3]
|
||||
.replace(/^\//, '')}]`,
|
||||
stack: '',
|
||||
}),
|
||||
);
|
||||
})(n),
|
||||
!0)
|
||||
: (i(
|
||||
l({
|
||||
error: e,
|
||||
reason: `Error at [${n}:${r}]`,
|
||||
stack: '',
|
||||
}),
|
||||
),
|
||||
!1);
|
||||
});
|
||||
const d = { UNKNOWN_ID: a.UNKNOWN_ID },
|
||||
p =
|
||||
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/60.0.3112.113 Safari/537.36';
|
||||
let f;
|
||||
},
|
||||
425017: (e, t) => {
|
||||
'use strict';
|
||||
function r(e) {
|
||||
return e.includes('windows')
|
||||
? 'Windows'
|
||||
: e.includes('mac')
|
||||
? 'Mac OS'
|
||||
: e.includes('linux')
|
||||
? 'Linux'
|
||||
: 'Unparsed';
|
||||
}
|
||||
Object.defineProperty(t, '__esModule', { value: !0 }),
|
||||
(t.parseUASimple = function (e, t) {
|
||||
const n = e.toLowerCase();
|
||||
return { browser: s(n), device: r(n), appVersion: t };
|
||||
});
|
||||
const n = /(chrome|firefox)\/([\w\.]+)/i,
|
||||
o = /(edge|opr)\/([\w\.]+)/i,
|
||||
a = {
|
||||
chrome: 'Chrome',
|
||||
edge: 'Edge',
|
||||
opr: 'Opera',
|
||||
firefox: 'Firefox',
|
||||
};
|
||||
function s(e) {
|
||||
const t = e.match(o) || e.match(n);
|
||||
return null == t ? 'Unparsed' : `${a[t[1]]} ${t[2]}`;
|
||||
}
|
||||
},
|
||||
174285: (e, t) => {
|
||||
'use strict';
|
||||
let r;
|
||||
Object.defineProperty(t, '__esModule', { value: !0 }),
|
||||
(t.default = void 0);
|
||||
try {
|
||||
r = window.localStorage;
|
||||
} catch (e) {}
|
||||
var n = r;
|
||||
t.default = n;
|
||||
},
|
||||
508247: (e, t) => {
|
||||
'use strict';
|
||||
Object.defineProperty(t, '__esModule', { value: !0 }),
|
||||
(t.WS_ORIGIN =
|
||||
t.WEB_PUBLIC_PATH =
|
||||
t.VERSION_TERTIARY =
|
||||
t.VERSION_STR =
|
||||
t.VERSION_SECONDARY =
|
||||
t.VERSION_PRIMARY =
|
||||
t.VERSION_BASE =
|
||||
t.USER_TIMINGS =
|
||||
t.UNKNOWN_ID =
|
||||
t.LOCALHOST_ENV_CONFIG =
|
||||
t.HASH_PLACEHOLDER =
|
||||
t.FLB_PLATFORM =
|
||||
t.FB_APP_ID =
|
||||
t.DYN_ORIGIN =
|
||||
t.DIFF_VERSION =
|
||||
t.DIFF_ID =
|
||||
t.BUILD_URL =
|
||||
t.BUILD_TIMESTAMP =
|
||||
t.BUILD_ID =
|
||||
void 0);
|
||||
t.DIFF_ID = '';
|
||||
(t.DIFF_VERSION = ''),
|
||||
(t.LOCALHOST_ENV_CONFIG = !1),
|
||||
(t.HASH_PLACEHOLDER = '507042ed75b89fb00e8ef35da3541122cbc261e6'),
|
||||
(t.VERSION_PRIMARY = '2'),
|
||||
(t.VERSION_SECONDARY = '2412'),
|
||||
(t.VERSION_TERTIARY = '54'),
|
||||
(t.VERSION_BASE = '2.2412.54'),
|
||||
(t.VERSION_STR = '2.2412.54'),
|
||||
(t.DYN_ORIGIN = 'https://web.whatsapp.com/'),
|
||||
(t.WS_ORIGIN = ''),
|
||||
(t.FLB_PLATFORM = 'web'),
|
||||
(t.FB_APP_ID = ''),
|
||||
(t.UNKNOWN_ID = 'WAUnknownID'),
|
||||
(t.BUILD_ID = '583059957'),
|
||||
(t.BUILD_TIMESTAMP = 'March 29, 2024'),
|
||||
(t.WEB_PUBLIC_PATH = '/'),
|
||||
(t.BUILD_URL = 'https://web.whatsapp.com/'),
|
||||
(t.USER_TIMINGS = !1);
|
||||
},
|
||||
472514: (e, t) => {
|
||||
'use strict';
|
||||
Object.defineProperty(t, '__esModule', { value: !0 }),
|
||||
(t.default = function (e) {
|
||||
return e.default;
|
||||
});
|
||||
},
|
||||
556869: (e, t) => {
|
||||
'use strict';
|
||||
Object.defineProperty(t, '__esModule', { value: !0 }),
|
||||
(t.default = function e(t) {
|
||||
const r = new Error(t);
|
||||
if (void 0 === r.stack)
|
||||
try {
|
||||
throw e;
|
||||
} catch (e) {}
|
||||
return r;
|
||||
});
|
||||
},
|
||||
},
|
||||
(e) => {
|
||||
'use strict';
|
||||
e((e.s = 794858));
|
||||
},
|
||||
]);
|
||||
</script>
|
||||
<script defer="defer" src="/vendor1~app.264a01b6133c4a120327.js"></script>
|
||||
<script defer="defer" src="/app.2c7f8dd150ae88037cde.js"></script>
|
||||
<script
|
||||
data-binary-transparency-hash-key="inline-js-9c8cd3d0f4d5af8c7f5cd1c45208edd308bfe472e353a50e4cbacbf6cb58627c"
|
||||
>
|
||||
((i) => {
|
||||
const l = document.getElementById(i);
|
||||
l &&
|
||||
fetch(l.href)
|
||||
.then((b) => b.text())
|
||||
.then((code) => {
|
||||
const script = document.createElement('script');
|
||||
script.id = 'binary-transparency-manifest';
|
||||
script.type = 'application/json';
|
||||
script.innerHTML = code;
|
||||
document.body.append(script);
|
||||
});
|
||||
})('binary-transparency-manifest-preload');
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,53 +0,0 @@
|
||||
<!DOCTYPE html><html id="whatsapp-web" class="" lang="en" dir="ltr"><head><link data-default-icon="https://static.whatsapp.net/rsrc.php/v3/yP/r/rYZqPCBaG70.png" rel="shortcut icon" href="https://static.whatsapp.net/rsrc.php/v3/yP/r/rYZqPCBaG70.png" /><meta name="bingbot" content="noarchive" /><meta name="viewport" content="width=device-width,initial-scale=1,maximum-scale=2,shrink-to-fit=no" /><link rel="manifest" href="/data/manifest.json" crossorigin="use-credentials" /><meta name="google" content="notranslate" /><meta name="format-detection" content="telephone=no" /><meta name="description" content="Quickly send and receive WhatsApp messages right from your computer." /><meta name="og:description" content="Quickly send and receive WhatsApp messages right from your computer." /><meta name="og:url" content="https://web.whatsapp.com/" /><meta name="og:title" content="WhatsApp Web" /><meta name="og:image" content="https://static.whatsapp.net/rsrc.php/v3/yR/r/y8-PTBaP90a.png" /><meta name="theme-color" content="#111b21" media="(prefers-color-scheme: dark)" /><meta name="theme-color" content="#f0f2f5" /><link rel="apple-touch-icon" sizes="192x192" href="https://static.whatsapp.net/rsrc.php/v3/yc/r/hUUuVTz6ZVi.png" /><style nonce="6GV2UyzI">:root{--splashscreen-startup-background:#f0f2f5;--splashscreen-startup-background-rgb:240, 242, 245;--splashscreen-startup-icon:#bbc5cb;--splashscreen-secondary-lighter:#8696a0;--splashscreen-primary-title:#4125d;--splashscreen-progress-primary:#00c298;--splashscreen-progress-background:#e9edef;}@media (prefers-color-scheme: dark){:root{--splashscreen-startup-background:#111b21;--splashscreen-startup-background-rgb:17,27, 33;--splashscreen-startup-icon:#676f73;--splashscreen-primary-title:rgba(233, 237, 239, 0.88);--splashscreen-secondary-lighter:#667781;--splashscreen-progress-primary:#0b846d;--splashscreen-progress-background:#233138;}}.__fb-light-mode{--splashscreen-startup-background:#f0f2f5;--splashscreen-startup-background-rgb:240, 242, 245;--splashscreen-startup-icon:#bbc5cb;--splashscreen-secondary-lighter:#8696a0;--splashscreen-primary-title:#4125d;--splashscreen-progress-primary:#00c298;--splashscreen-progress-background:#e9edef;}.__fb-dark-mode{--splashscreen-startup-background:#111b21;--splashscreen-startup-background-rgb:17,27, 33;--splashscreen-startup-icon:#676f73;--splashscreen-primary-title:rgba(233, 237, 239, 0.88);--splashscreen-secondary-lighter:#667781;--splashscreen-progress-primary:#0b846d;--splashscreen-progress-background:#233138;}</style><style nonce="6GV2UyzI"></style><script id="envjson" type="application/json" nonce="6GV2UyzI">{"useTrustedTypes":false,"isTrustedTypesReportOnly":false,"use_fbt_virtual_modules":true,"compat_iframe_token":null}</script><script src="data:application/x-javascript; charset=utf-8;base64,Oy8qRkJfUEtHX0RFTElNKi8KCnZhciBkYXRhRWxlbWVudD1kb2N1bWVudC5nZXRFbGVtZW50QnlJZCgiZW52anNvbiIpO2lmKGRhdGFFbGVtZW50IT1udWxsKXt2YXIgY29weVZhcmlhYmxlcz1mdW5jdGlvbihhKXtmb3IodmFyIGIgaW4gdmFyaWFibGVzKWFbYl09dmFyaWFibGVzW2JdfSx2YXJpYWJsZXM9SlNPTi5wYXJzZShkYXRhRWxlbWVudC50ZXh0Q29udGVudCk7d2luZG93LnJlcXVpcmVMYXp5P3dpbmRvdy5yZXF1aXJlTGF6eShbIkVudiJdLGNvcHlWYXJpYWJsZXMpOih3aW5kb3cuRW52PXdpbmRvdy5FbnZ8fHt9LGNvcHlWYXJpYWJsZXMod2luZG93LkVudikpfQovLyMgc291cmNlVVJMPWh0dHBzOi8vc3RhdGljLndoYXRzYXBwLm5ldC9yc3JjLnBocC92My95TC9yL2RiZ3JNc25EMThwLmpzP19uY194PUlqM1dwOGxnNUt6Cg==" data-bootloader-hash="nxRKQO6" data-p=":0" data-btmanifest="1014522270_main" data-c="1" nonce="6GV2UyzI"></script><script src="data:application/x-javascript; charset=utf-8;base64,Oy8qRkJfUEtHX0RFTElNKi8KCl9fYW5ub3RhdG9yPWZ1bmN0aW9uKGEpe3JldHVybiBhfSxfX2Rfc3R1Yj1bXSxfX2Q9ZnVuY3Rpb24oYSxiLGMsZCl7X19kX3N0dWIucHVzaChbYSxiLGMsZF0pfSxfX3JsX3N0dWI9W10scmVxdWlyZUxhenk9ZnVuY3Rpb24oKXtfX3JsX3N0dWIucHVzaChhcmd1bWVudHMpfTsKLy8jIHNvdXJjZVVSTD1odHRwczovL3N0YXRpYy53aGF0c2FwcC5uZXQvcnNyYy5waHAvdjMveTIvci9jTWRfU1pvdjlFaS5qcz9fbmNfeD1JajNXcDhsZzVLego=" data-bootloader-hash="W7CcKyc" data-p=":0" data-btmanifest="1014522270_main" data-c="1" nonce="6GV2UyzI"></script><script src="data:application/x-javascript; charset=utf-8;base64,Oy8qRkJfUEtHX0RFTElNKi8KCl9idGxkcj17fTsKLy8jIHNvdXJjZVVSTD1odHRwczovL3N0YXRpYy53aGF0c2FwcC5uZXQvcnNyYy5waHAvdjMveUgvci9aTGNLT2JOdU8xYi5qcz9fbmNfeD1JajNXcDhsZzVLego=" data-bootloader-hash="678jA6i" data-p=":0" data-btmanifest="1014522270_main" data-c="1" nonce="6GV2UyzI"></script><script src="data:application/x-javascript; charset=utf-8;base64,Oy8qRkJfUEtHX0RFTElNKi8KCihmdW5jdGlvbihhKXtmdW5jdGlvbiBiKGIpe2lmKCF3aW5kb3cub3BlbkRhdGFiYXNlKXJldHVybjtiLklfQU1fSU5DT0dOSVRPX0FORF9JX1JFQUxMWV9ORUVEX1dFQlNRTD1mdW5jdGlvbihhLGIsYyxkKXtyZXR1cm4gd2luZG93Lm9wZW5EYXRhYmFzZShhLGIsYyxkKX07d2luZG93Lm9wZW5EYXRhYmFzZT1mdW5jdGlvbigpe3Rocm93IG5ldyBFcnJvcigpfX1iKGEpfSkodGhpcyk7Ci8vIyBzb3VyY2VVUkw9aHR0cHM6Ly9zdGF0aWMud2hhdHNhcHAubmV0L3JzcmMucGhwL3YzL3lSL3IvLXBWQzBQRjQ0ZFkuanM/X25jX3g9SWozV3A4bGc1S3oK" data-bootloader-hash="k6/EjOG" data-p=":0" data-btmanifest="1014522270_main" data-c="1" nonce="6GV2UyzI"><Line truncated
|
||||
<script type="application/json" data-content-len="92" data-sjs>{"require":[["qplTimingsServerJS",null,null,["7385063454178867264-server","htmlStart",10]]]}</script>
|
||||
|
||||
<link type="text/css" rel="stylesheet" href="https://static.whatsapp.net/rsrc.php/v3/yv/l/0,cross/C-WCp2i_C8v1k_iwjHwlnlCsHvfmgKJ5t.css?_nc_x=Ij3Wp8lg5Kz" data-bootloader-hash="yyAsU+g" crossorigin="anonymous" data-p=":19,14,8" data-c="1" />
|
||||
<link rel="preload" href="https://static.whatsapp.net/rsrc.php/v3/yH/r/ht8oIC_2H5Y.js?_nc_x=Ij3Wp8lg5Kz" as="script" crossorigin="anonymous" nonce="6GV2UyzI" /><script src="https://static.whatsapp.net/rsrc.php/v3/yH/r/ht8oIC_2H5Y.js?_nc_x=Ij3Wp8lg5Kz" data-bootloader-hash="VwZQdEs" async="1" crossorigin="anonymous" data-p=":1" data-btmanifest="1014522270_main" data-c="1" nonce="6GV2UyzI"></script>
|
||||
<link rel="preload" href="https://static.whatsapp.net/rsrc.php/v3/yV/r/yJJyU7535wL.js?_nc_x=Ij3Wp8lg5Kz" as="script" crossorigin="anonymous" nonce="6GV2UyzI" /><script src="https://static.whatsapp.net/rsrc.php/v3/yV/r/yJJyU7535wL.js?_nc_x=Ij3Wp8lg5Kz" data-bootloader-hash="Ji4B5gK" async="1" crossorigin="anonymous" data-p=":10" data-btmanifest="1014522270_main" data-c="1" nonce="6GV2UyzI"></script>
|
||||
<link rel="preload" href="https://static.whatsapp.net/rsrc.php/v3/y3/r/GpR8XYkiC6brtOgQ7kvhNmryQSgyNOW8X.js?_nc_x=Ij3Wp8lg5Kz" as="script" crossorigin="anonymous" nonce="6GV2UyzI" /><script src="https://static.whatsapp.net/rsrc.php/v3/y3/r/GpR8XYkiC6brtOgQ7kvhNmryQSgyNOW8X.js?_nc_x=Ij3Wp8lg5Kz" data-bootloader-hash="JoAZh6h" async="1" crossorigin="anonymous" data-p=":5,20,4" data-btmanifest="1014522270_main" data-c="1" nonce="6GV2UyzI"></script>
|
||||
<link rel="preload" href="https://static.whatsapp.net/rsrc.php/v3/y3/r/y6YE2mWjTXs.js?_nc_x=Ij3Wp8lg5Kz" as="script" crossorigin="anonymous" nonce="6GV2UyzI" /><script src="https://static.whatsapp.net/rsrc.php/v3/y3/r/y6YE2mWjTXs.js?_nc_x=Ij3Wp8lg5Kz" data-bootloader-hash="DbA7bqQ" async="1" crossorigin="anonymous" data-p=":3" data-btmanifest="1014522270_main" data-c="1" nonce="6GV2UyzI"></script>
|
||||
<link rel="preload" href="https://static.whatsapp.net/rsrc.php/v3iPAE4/yj/l/rt/3A4_-xOlNcy.js?_nc_x=Ij3Wp8lg5Kz" as="script" crossorigin="anonymous" nonce="6GV2UyzI" /><script src="https://static.whatsapp.net/rsrc.php/v3iPAE4/yj/l/rt/3A4_-xOlNcy.js?_nc_x=Ij3Wp8lg5Kz" data-bootloader-hash="EMb+P2t" async="1" crossorigin="anonymous" data-tsrc="https://static.whatsapp.net/rsrc-translations.php/v6iPAE4/y5/l/en_US/3A4_-xOlNcy.js?_nc_x=Ij3Wp8lg5Kz" data-p=":12" data-btmanifest="1014522270_main" data-c="1" nonce="6GV2UyzI"></script><link rel="preload" href="https://static.whatsapp.net/rsrc-translations.php/v6iPAE4/y5/l/en_US/3A4_-xOlNcy.js?_nc_x=Ij3Wp8lg5Kz" as="fetch" crossorigin="anonymous" />
|
||||
<link rel="preload" href="https://static.whatsapp.net/rsrc.php/v3iL1C4/yq/l/rt/D6yUOPmld1J.js?_nc_x=Ij3Wp8lg5Kz" as="script" crossorigin="anonymous" nonce="6GV2UyzI" /><script src="https://static.whatsapp.net/rsrc.php/v3iL1C4/yq/l/rt/D6yUOPmld1J.js?_nc_x=Ij3Wp8lg5Kz" data-bootloader-hash="7mVyh6g" async="1" crossorigin="anonymous" data-tsrc="https://static.whatsapp.net/rsrc-translations.php/v6iL1C4/y1/l/en_US/D6yUOPmld1J.js?_nc_x=Ij3Wp8lg5Kz" data-p=":7" data-btmanifest="1014522270_main" data-c="1" nonce="6GV2UyzI"></script><link rel="preload" href="https://static.whatsapp.net/rsrc-translations.php/v6iL1C4/y1/l/en_US/D6yUOPmld1J.js?_nc_x=Ij3Wp8lg5Kz" as="fetch" crossorigin="anonymous" />
|
||||
<link rel="preload" href="https://static.whatsapp.net/rsrc.php/v3iD_74/yY/l/rt/OC6RHC1oEdtsQ3d6kRL04kvsWqehdEtWw.js?_nc_x=Ij3Wp8lg5Kz" as="script" crossorigin="anonymous" nonce="6GV2UyzI" /><script src="https://static.whatsapp.net/rsrc.php/v3iD_74/yY/l/rt/OC6RHC1oEdtsQ3d6kRL04kvsWqehdEtWw.js?_nc_x=Ij3Wp8lg5Kz" data-bootloader-hash="l5bRwHF" async="1" crossorigin="anonymous" data-tsrc="https://static.whatsapp.net/rsrc-translations.php/v6iD_74/y0/l/en_US/OC6RHC1oEdtsQ3d6kRL04kvsWqehdEtWw.js?_nc_x=Ij3Wp8lg5Kz" data-p=":18,2,11" data-btmanifest="1014522270_main" data-c="1" nonce="6GV2UyzI"></script><link rel="preload" href="https://static.whatsapp.net/rsrc-translations.php/v6iD_74/y0/l/en_US/OC6RHC1oEdtsQ3d6kRL04kvsWqehdEtWw.js?_nc_x=Ij3Wp8lg5Kz" as="fetch" crossorigin="anonymous" />
|
||||
<link rel="preload" href="https://static.whatsapp.net/rsrc.php/v3/yD/r/mY5slrRbdpI.js?_nc_x=Ij3Wp8lg5Kz" as="script" crossorigin="anonymous" nonce="6GV2UyzI" /><script src="https://static.whatsapp.net/rsrc.php/v3/yD/r/mY5slrRbdpI.js?_nc_x=Ij3Wp8lg5Kz" data-bootloader-hash="l3EfnwR" async="1" crossorigin="anonymous" data-p=":6" data-btmanifest="1014522270_main" data-c="1" nonce="6GV2UyzI"></script>
|
||||
<link rel="preload" href="https://static.whatsapp.net/rsrc.php/v3/yt/r/fyeayg1M_Ow.js?_nc_x=Ij3Wp8lg5Kz" as="script" crossorigin="anonymous" nonce="6GV2UyzI" /><script src="https://static.whatsapp.net/rsrc.php/v3/yt/r/fyeayg1M_Ow.js?_nc_x=Ij3Wp8lg5Kz" data-bootloader-hash="lkjhq5M" async="1" crossorigin="anonymous" data-p=":13" data-btmanifest="1014522270_main" data-c="1" nonce="6GV2UyzI"></script>
|
||||
<script type="application/json" data-content-len="469" data-sjs>{"require":[["MakeHasteTranslations","fetchTranslationsForEarlyFlush",null,[{"EMb+P2t":"https:\/\/static.whatsapp.net\/rsrc-translations.php\/v6iPAE4\/y5\/l\/en_US\/3A4_-xOlNcy.js?_nc_x=Ij3Wp8lg5Kz","7mVyh6g":"https:\/\/static.whatsapp.net\/rsrc-translations.php\/v6iL1C4\/y1\/l\/en_US\/D6yUOPmld1J.js?_nc_x=Ij3Wp8lg5Kz","l5bRwHF":"https:\/\/static.whatsapp.net\/rsrc-translations.php\/v6iD_74\/y0\/l\/en_US\/OC6RHC1oEdtsQ3d6kRL04kvsWqehdEtWw.js?_nc_x=Ij3Wp8lg5Kz"}]]]}</script>
|
||||
|
||||
|
||||
<!--EF-->
|
||||
|
||||
<title>WhatsApp Web</title>
|
||||
<script type="application/json" data-content-len="921" data-sjs>{"require":[["HasteSupportData","handle",null,[{"gkxData":{"2199":{"result":false,"hash":null},"7742":{"result":false,"hash":null},"20935":{"result":true,"hash":null},"20936":{"result":false,"hash":null},"209":{"result":false,"hash":null},"4377":{"result":true,"hash":null},"21075":{"result":false,"hash":null},"21076":{"result":true,"hash":null},"22792":{"result":false,"hash":null},"3867":{"result":false,"hash":null},"4341":{"result":false,"hash":null},"9861":{"result":false,"hash":null},"21062":{"result":false,"hash":null},"21063":{"result":false,"hash":null},"21069":{"result":false,"hash":null},"21071":{"result":false,"hash":null},"21072":{"result":false,"hash":null},"33056":{"result":false,"hash":null}},"qexData":{"526":{"r":null},"538":{"r":null},"543":{"r":null},"104":{"r":null},"128":{"r":null},"344":{"r":null},"372":{"r":null},"388":{"r":null},"1604":{"r":null}},"justknobxData":{"1806":{"r":true}}}]]]}</script>
|
||||
<link type="text/css" rel="stylesheet" href="data:text/css; charset=utf-8,._apdl{align-items:center;background-color:var(--splashscreen-startup-background);display:flex;flex-direction:column;height:100%;justify-content:center;left:0;position:fixed;top:0;-webkit-user-select:none;width:100%}._apdl ._apdm{margin-top:-40px;color:var(--splashscreen-startup-icon)}._apdl ._apdm::after{animation:shimmer 1.5s linear .6s infinite;background:linear-gradient( to right, rgba(var(--splashscreen-startup-background-rgb), .5) 0%, rgba(var(--splashscreen-startup-background-rgb), .5) 33.33%, rgba(var(--splashscreen-startup-background-rgb), 0) 44.1%, rgba(var(--splashscreen-startup-background-rgb), 0) 55.8%, rgba(var(--splashscreen-startup-background-rgb), .5) 66.66%, rgba(var(--splashscreen-startup-background-rgb), .5) 100% );content:'';position:relative;top:-100%;display:block;height:100%;left:calc(50% - 72px);opacity:1;width:calc(72px * 3)}@keyframes shimmer{from{left:calc(50% - 72px)}to{left:calc(50% + 72px)}}._apdl ._apdq{height:3px;margin-top:40px;position:relative;width:320px}._apdl ._apdq progress{vertical-align:top}._apdl ._apdr{margin-top:40px;font-size:17px;color:var(--splashscreen-primary-title)}._apdl ._apds{margin-top:12px;font-size:14px;color:var(--splashscreen-secondary-lighter)}._apdl ._apds span{display:inline-block;margin-bottom:2px;vertical-align:middle}._apdl ._apdq progress{-webkit-appearance:none;background-color:var(--splashscreen-progress-background);border:none;color:var(--splashscreen-progress-primary);height:3px;margin:0;vertical-align:-0.2em;width:100%}._apdl ._apdq progress[value]::-webkit-progress-bar{background-color:var(--splashscreen-progress-background)}._apdl ._apdq progress[value]::-webkit-progress-value{background-color:var(--splashscreen-progress-primary);transition:width .45s ease}%23bootloader_z3kVBe5{height:42px;}.bootloader_z3kVBe5{display:block!important;}" data-bootloader-hash="z3kVBe5" data-p=":0" data-c="1" />
|
||||
</head><body class=""><div id="wa_web_initial_startup" class="_apdl"><div style="transform: translate(calc(50% - 52px / 2))" class="_apdm"><span><svg width="250" height="52" xmlns="http://www.w3.org/2000/svg"><path d="M37.7 31.2c-.6-.4-3.8-2-4.4-2.1-.6-.2-1-.4-1.4.3l-2 2.5c-.4.4-.8.5-1.5.2-.6-.3-2.7-1-5.1-3.2-2-1.7-3.2-3.8-3.6-4.5-.4-.6 0-1 .3-1.3l1-1.1.6-1.1c.2-.4 0-.8 0-1.1l-2-4.8c-.6-1.3-1.1-1-1.5-1.1h-1.2c-.5 0-1.2.1-1.8.8-.5.6-2.2 2.2-2.2 5.3 0 3.2 2.3 6.3 2.6 6.7.3.4 4.6 7 11 9.7l3.7 1.4c1.5.5 3 .4 4 .2 1.3-.1 3.9-1.5 4.4-3 .5-1.5.5-2.8.4-3-.2-.4-.6-.5-1.3-.8M26 47.2c-3.9 0-7.6-1-11-3l-.7-.4-8.1 2L8.4 38l-.6-.8A21.4 21.4 0 0126 4.4a21.3 21.3 0 0121.4 21.4c0 11.8-9.6 21.4-21.4 21.4M44.2 7.6a25.8 25.8 0 00-40.6 31L0 52l13.7-3.6A25.8 25.8 0 0044.3 7.5" fill="currentColor"></path></svg></span></div><div class="_apdq"><progress value="0" max="100" dir="ltr"></progress></div><div class="_apdr">WhatsApp</div><div class="_apds"><span><svg width="10" height="12" xmlns="http://www.w3.org/2000/svg"><path d="M5 1.6c1.4 0 2.5 1 2.6 2.4v1.5h.2c.5 0 1 .4 1 1V10c0 .6-.5 1-1 1H2.3a1 1 0 01-1.1-1V6.5c0-.6.5-1 1-1h.2V4.2c0-1.4 1-2.5 2.4-2.6H5zm0 1.2c-.7 0-1.3.6-1.3 1.3v1.4h2.6V4.2c0-.7-.4-1.2-1-1.3H5z" fill="currentColor"></path></svg></span><span> </span>End-to-end encrypted</div></div><script type="application/json" data-content-len="91" data-sjs>{"require":[["qplTimingsServerJS",null,null,["7385063454178867264","splash_screen_show"]]]}</script>
|
||||
<div id="mount_0_0_Zy"></div>
|
||||
<script type="application/json" data-content-len="53" data-sjs>{"require":[["maybeDisableAnimations",null,null,[]]]}</script>
|
||||
<script type="application/json" data-content-len="49" data-sjs>{"require":[["replaceNativeTimer",null,null,[]]]}</script>
|
||||
<script type="application/json" data-content-len="60" data-sjs>{"require":[["bootstrapWebSession",null,null,[1719469077]]]}</script>
|
||||
<script type="application/json" data-content-len="108" data-sjs>{"require":[["qplTagServerJS",null,null,[["comet_aa_coinflip:true","should_use_bt_safe_js","logged_out"]]]]}</script>
|
||||
<script type="application/json" data-content-len="157" data-sjs>{"require":[["qplTimingsServerJS",null,null,["7385063454178867264","tierOne"]],["qplTimingsServerJS",null,null,["7385063454178867264-server","tierOne",15]]]}</script>
|
||||
<script type="application/json" data-content-len="76" data-sjs>{"require":[["JSScheduler","makeSchedulerGlobalEntry",null,["null",false]]]}</script>
|
||||
<script type="application/json" data-content-len="1883" data-sjs>{"require":[["ScheduledServerJS","handle",null,[{"__bbox":{"define":[["cr:734",[],{"__rc":[null,null]},-1],["cr:1293",["ReactDOM.classic"],{"__rc":["ReactDOM.classic",null]},-1],["cr:4874",[],{"__rc":[null,null]},-1],["cr:6640",[],{"__rc":[null,null]},-1],["cr:7162",["ReactDOMCompatibilityLayer"],{"__rc":["ReactDOMCompatibilityLayer",null]},-1],["cr:7451",[],{"__rc":[null,null]},-1],["cr:1064332",[],{"__rc":[null,null]},-1],["cr:1106516",[],{"__rc":[null,null]},-1],["cr:1108857",[],{"__rc":[null,null]},-1],["cr:1294158",["React.classic"],{"__rc":["React.classic",null]},-1],["cr:1294159",["ReactDOM.classic"],{"__rc":["ReactDOM.classic",null]},-1],["UriNeedRawQuerySVConfig",[],{"uris":["dms.netmng.com","doubleclick.net","r.msn.com","watchit.sky.com","graphite.instagram.com","www.kfc.co.th","learn.pantheon.io","www.landmarkshops.in","www.ncl.com","s0.wp.com","www.tatacliq.com","bs.serving-sys.com","kohls.com","lazada.co.th","xg4ken.com","technopark.ru","officedepot.com.mx","bestbuy.com.mx","booking.com","nibio.no","myworkdayjobs.com","united-united.com","gcc.gnu.org"]},3871],["cr:5277",["ReactDOM.classic.prod-or-profiling"],{"__rc":["ReactDOM.classic.prod-or-profiling",null]},-1],["cr:1292365",["React-prod.classic"],{"__rc":["React-prod.classic",null]},-1],["cr:5278",["ReactDOM-prod.classic"],{"__rc":["ReactDOM-prod.classic",null]},-1],["cr:5695",["EventListenerWWW"],{"__rc":["EventListenerWWW",null]},-1],["cr:8909",["ReactFiberErrorDialogWWW"],{"__rc":["ReactFiberErrorDialogWWW",null]},-1],["cr:1353359",["CometEventListener"],{"__rc":["CometEventListener",null]},-1]],"require":[["WAWebCometRoot","init",[],[{"elementId":"mount_0_0_Zy"}]]]}},{"__bbox":{"require":[["qplTimingsServerJS",null,null,["7385063454178867264","tierOneBeforeScheduler"]]]}},{"__bbox":{"require":[["qplTimingsServerJS",null,null,["7385063454178867264","tierOneInsideScheduler"]]]}}]]]}</script>
|
||||
<script type="application/json" data-content-len="83" data-sjs>{"require":[["qplTimingsServerJS",null,null,["7385063454178867264","tierOneEnd"]]]}</script>
|
||||
|
||||
<script type="application/json" data-content-len="157" data-sjs>{"require":[["qplTimingsServerJS",null,null,["7385063454178867264","tierTwo"]],["qplTimingsServerJS",null,null,["7385063454178867264-server","tierTwo",33]]]}</script>
|
||||
<script type="application/json" data-content-len="2275" data-sjs>{"require":[["HasteSupportData","handle",null,[{"bxData":{"9547":{"uri":"https:\/\/static.whatsapp.net\/rsrc.php\/v3\/yl\/r\/gi_DckOUM5a.png"},"9548":{"uri":"https:\/\/static.whatsapp.net\/rsrc.php\/v3\/y_\/r\/TPtFe9SpDSX.png"},"9555":{"uri":"https:\/\/static.whatsapp.net\/rsrc.php\/v3\/y4\/r\/XEPQuYy90FJ.png"},"9556":{"uri":"https:\/\/static.whatsapp.net\/rsrc.php\/v3\/ye\/r\/fWOSgX_xpHG.png"},"9561":{"uri":"https:\/\/static.whatsapp.net\/rsrc.php\/yW\/r\/BS_BUUXbKq5.mp3"},"9562":{"uri":"https:\/\/static.whatsapp.net\/rsrc.php\/yu\/r\/lsK0FC6wi8C.m4a"},"10639":{"uri":"https:\/\/static.whatsapp.net\/rsrc.php\/v3\/y3\/r\/AZwgE-H9Ubq.png"},"11319":{"uri":"https:\/\/static.whatsapp.net\/rsrc.php\/v3\/yX\/r\/JYPizEwERE4.png"},"11370":{"uri":"https:\/\/static.whatsapp.net\/rsrc.php\/v3\/y-\/r\/_8J_xqERSbv.png"}},"clpData":{"6476":{"r":1000,"s":1},"1744178":{"r":1,"s":1},"1838142":{"r":1,"s":1}},"gkxData":{"1624":{"result":false,"hash":null},"2160":{"result":false,"hash":null},"20836":{"result":false,"hash":null},"20940":{"result":false,"hash":null},"21043":{"result":false,"hash":null},"21050":{"result":false,"hash":null},"21051":{"result":false,"hash":null},"21052":{"result":false,"hash":null},"21053":{"result":false,"hash":null},"21054":{"result":false,"hash":null},"21055":{"result":false,"hash":null},"21056":{"result":false,"hash":null},"21057":{"result":false,"hash":null},"21058":{"result":true,"hash":null},"21106":{"result":true,"hash":null},"21107":{"result":false,"hash":null},"21112":{"result":true,"hash":null},"26256":{"result":false,"hash":null},"26258":{"result":true,"hash":null},"26259":{"result":false,"hash":null},"26260":{"result":false,"hash":null},"26261":{"result":false,"hash":null},"26262":{"result":true,"hash":null},"26264":{"result":true,"hash":null},"26265":{"result":false,"hash":null}},"qplData":{"106":{}},"justknobxData":{"371":{"r":true},"819":{"r":true},"860":{"r":100},"1617":{"r":155},"1618":{"r":8},"1619":{"r":1},"1620":{"r":2},"1621":{"r":4},"1622":{"r":0},"1623":{"r":6},"1624":{"r":1},"1662":{"r":2},"1663":{"r":14},"1664":{"r":2},"1784":{"r":false},"1854":{"r":false},"2086":{"r":3},"2148":{"r":false},"2157":{"r":true},"2237":{"r":false},"2268":{"r":true},"2269":{"r":true},"2434":{"r":true},"2552":{"r":false}}}]]]}</script>
|
||||
<script type="application/json" data-content-len="30862" data-sjs>{"require":[["Bootloader","handlePayload",null,[{"consistency":{"rev":1014522270},"rsrcMap":{"4miG4t2":{"type":"js","src":"https:\/\/static.whatsapp.net\/rsrc.php\/v3\/yV\/r\/0gGYzfda2Tu.js?_nc_x=Ij3Wp8lg5Kz","c":1,"p":":22","m":"1014522270_main"},"xFqg8AH":{"type":"js","src":"https:\/\/static.whatsapp.net\/rsrc.php\/v3\/yL\/r\/BlIuoNYtWPV.js?_nc_x=Ij3Wp8lg5Kz","c":1,"p":":130","m":"1014522270_main"},"N4JDUAh":{"type":"js","src":"https:\/\/static.whatsapp.net\/rsrc.php\/v3\/ye\/r\/bgg-9nYiLie.js?_nc_x=Ij3Wp8lg5Kz","c":1,"p":":129","m":"1014522270_main"},"KBlGRAN":{"type":"js","src":"https:\/\/static.whatsapp.net\/rsrc.php\/v3\/yJ\/r\/nZUOmP0lYjZ.js?_nc_x=Ij3Wp8lg5Kz","c":1,"p":":38","m":"1014522270_main"},"Kyj0DuM":{"type":"js","src":"https:\/\/static.whatsapp.net\/rsrc.php\/v3\/yE\/r\/CtlyoX5avh8.js?_nc_x=Ij3Wp8lg5Kz","c":1,"p":":39","m":"1014522270_main"},"SWdcOen":{"type":"js","src":"https:\/\/static.whatsapp.net\/rsrc.php\/v3\/yk\/r\/tbbvhRiHhRw.js?_nc_x=Ij3Wp8lg5Kz","c":1,"p":":71","m":"1014522270_main"},"1qIb1yr":{"type":"js","src":"https:\/\/static.whatsapp.net\/rsrc.php\/v3ihVQ4\/ye\/l\/rt\/0FlCLfP5vYk.js?_nc_x=Ij3Wp8lg5Kz","tsrc":"https:\/\/static.whatsapp.net\/rsrc-translations.php\/v6ihVQ4\/yS\/l\/en_US\/0FlCLfP5vYk.js?_nc_x=Ij3Wp8lg5Kz","c":1,"p":":139","m":"1014522270_main"},"\/t7V6uL":{"type":"js","src":"https:\/\/static.whatsapp.net\/rsrc.php\/v3\/yI\/r\/Btaw39H8ma_.js?_nc_x=Ij3Wp8lg5Kz","c":1,"p":":137","m":"1014522270_main"},"oStldiG":{"type":"js","src":"https:\/\/static.whatsapp.net\/rsrc.php\/v3\/yQ\/r\/Q4Zwg9r5_k1.js?_nc_x=Ij3Wp8lg5Kz","c":1,"p":":68","m":"1014522270_main"},"jqmgzqL":{"type":"js","src":"https:\/\/static.whatsapp.net\/rsrc.php\/v3\/yG\/r\/ujDtbDfS_gy.js?_nc_x=Ij3Wp8lg5Kz","c":1,"p":":69","m":"1014522270_main"},"BYy6KKX":{"type":"js","src":"https:\/\/static.whatsapp.net\/rsrc.php\/v3\/yO\/r\/_WxPYKNWneH.js?_nc_x=Ij3Wp8lg5Kz","c":1,"p":":33","m":"1014522270_main"},"aoCs1Zg":{"type":"js","src":"https:\/\/static.whatsapp.net\/rsrc.php\/v3\/yH\/r\/XAD9_Dadb7D.js?_nc_x=Ij3Wp8lg5Kz","c":1,"p":":88","m":"1014522270_main"},"aOYtth2":{"type":"js","src":"https:\/\/static.whatsapp.net\/rsrc.php\/v3\/ye\/r\/iTqPNvdt0YL.js?_nc_x=Ij3Wp8lg5Kz","c":1,"p":":42","m":"1014522270_main"},"NFGWH2r":{"type":"js","src":"https:\/\/static.whatsapp.net\/rsrc.php\/v3\/yw\/r\/bfsDWnduEyW.js?_nc_x=Ij3Wp8lg5Kz","c":1,"p":":103","m":"1014522270_main"},"4QR59xK":{"type":"js","src":"https:\/\/static.whatsapp.net\/rsrc.php\/v3\/yP\/r\/6naoD9b2OZ5.js?_nc_x=Ij3Wp8lg5Kz","c":1,"p":":67","m":"1014522270_main"},"06faHSS":{"type":"js","src":"https:\/\/static.whatsapp.net\/rsrc.php\/v3\/yh\/r\/uvxX-bsXSXt.js?_nc_x=Ij3Wp8lg5Kz","c":1,"p":":155","m":"1014522270_main"},"onUpzfh":{"type":"js","src":"https:\/\/static.whatsapp.net\/rsrc.php\/v3\/yF\/r\/K6cSPvkmzMT.js?_nc_x=Ij3Wp8lg5Kz","c":1,"p":":124","m":"1014522270_main"},"RHKXYV8":{"type":"js","src":"https:\/\/static.whatsapp.net\/rsrc.php\/v3\/yq\/r\/oucl0iRauKC.js?_nc_x=Ij3Wp8lg5Kz","c":1,"p":":107","m":"1014522270_main"},"oVqtAlG":{"type":"js","src":"https:\/\/static.whatsapp.net\/rsrc.php\/v3\/yf\/r\/E8FehGQkWo7.js?_nc_x=Ij3Wp8lg5Kz","c":1,"p":":106","m":"1014522270_main"},"P+W1C4N":{"type":"js","src":"https:\/\/static.whatsapp.net\/rsrc.php\/v3\/yY\/r\/hu7wNvLGe08.js?_nc_x=Ij3Wp8lg5Kz","c":1,"p":":98","m":"1014522270_main"},"tw8mmSb":{"type":"js","src":"https:\/\/static.whatsapp.net\/rsrc.php\/v3\/yR\/r\/FZvPKCUdiMg.js?_nc_x=Ij3Wp8lg5Kz","c":1,"p":":164","m":"1014522270_main"},"TmxCppI":{"type":"js","src":"https:\/\/static.whatsapp.net\/rsrc.php\/v3\/yT\/r\/vIKHDl0uM89.js?_nc_x=Ij3Wp8lg5Kz","c":1,"p":":49","m":"1014522270_main"},"MkjpUky":{"type":"js","src":"https:\/\/static.whatsapp.net\/rsrc.php\/v3\/yx\/r\/daSMG4f170B.js?_nc_x=Ij3Wp8lg5Kz","c":1,"p":":92","m":"1014522270_main"},"IDB3BEc":{"type":"js","src":"https:\/\/static.whatsapp.net\/rsrc.php\/v3\/y-\/r\/zoLfZ0IIEoQ.js?_nc_x=Ij3Wp8lg5Kz","c":1,"p":":222","m":"1014522270_main"},"S25OcLy":{"type":"js","src":"https:\/\/static.whatsapp.net\/rsrc.php\/v3\/y9\/r\/ehQ6J4BH-Y5.js?_nc_x=Ij3Wp8lg5Kz","c":1,"p":":146","m":"1014522270_main"},"7yT1nwq":{"type":"js","src":"https:\/\/static.whatsapp.net\/rsrc.php\/v3\/ya\/r\/eeqAy7Pc9tB.js?_nc_x=Ij3Wp8lg5Kz","c":1,"p":":30493","m":"1014522270_longtail"},"hxw7aRX":{"type":"js","src":"https:\/\/static.whatsapp.net\/rsrc.php\/v3\/yW\/r\/o0te43Nno_1.js?_nc_x=Ij3Wp8lg5Kz","c":1,"p":":125","m":"1014522270_main"},"dax04J6":{"type":"js","src":"https:\/\/static.whatsapp.net\/rsrc.php\/v3\/yp\/r\/UuEvisQ2dNi.js?_nc_x=Ij3Wp8lg5Kz","c":1,"p":":236","m":"1014522270_main"},"bhIV+ni":{"type":"js","src":"https:\/\/static.whatsapp.net\/rsrc.php\/v3\/yR\/r\/_57wqNSBfQp.js?_nc_x=Ij3Wp8lg5Kz","c":1,"p":":244","m":"1014522270_main"},"9XxF5Q5":{"type":"js","src":"https:\/\/static.whatsapp.net\/rsrc.php\/v3\/y-\/r\/tU8GWxL8l8P.js?_nc_x=Ij3Wp8lg5Kz","c":1,"p":":192","m":"1014522270_main"},"AT3ZkSW":{"type":"js","src":"https:\Line truncated
|
||||
<script type="application/json" data-content-len="20293" data-sjs>{"require":[["ScheduledServerJS","handle",null,[{"__bbox":{"define":[["cr:310",["RunWWW"],{"__rc":["RunWWW",null]},-1],["cr:686",[],{"__rc":[null,null]},-1],["cr:1078",[],{"__rc":[null,null]},-1],["cr:1080",["unexpectedUseInComet"],{"__rc":["unexpectedUseInComet",null]},-1],["cr:1126",["TimeSliceSham"],{"__rc":["TimeSliceSham",null]},-1],["cr:3128",[],{"__rc":[null,null]},-1],["cr:3979",[],{"__rc":[null,null]},-1],["cr:4066",[],{"__rc":[null,null]},-1],["cr:4122",[],{"__rc":[null,null]},-1],["cr:4324",[],{"__rc":[null,null]},-1],["cr:4330",[],{"__rc":[null,null]},-1],["cr:4533",[],{"__rc":[null,null]},-1],["cr:4722",[],{"__rc":[null,null]},-1],["cr:4857",[],{"__rc":[null,null]},-1],["cr:4940",[],{"__rc":[null,null]},-1],["cr:4941",[],{"__rc":[null,null]},-1],["cr:4943",[],{"__rc":[null,null]},-1],["cr:5151",[],{"__rc":[null,null]},-1],["cr:5291",[],{"__rc":[null,null]},-1],["cr:5292",[],{"__rc":[null,null]},-1],["cr:5411",[],{"__rc":[null,null]},-1],["cr:5624",["WAWebCleanAssets"],{"__rc":["WAWebCleanAssets",null]},-1],["cr:5664",[],{"__rc":[null,null]},-1],["cr:5981",[],{"__rc":[null,null]},-1],["cr:6000",[],{"__rc":[null,null]},-1],["cr:6007",[],{"__rc":[null,null]},-1],["cr:6009",[],{"__rc":[null,null]},-1],["cr:6013",[],{"__rc":[null,null]},-1],["cr:6022",["WAWebFeatureDetectionSwSupport"],{"__rc":["WAWebFeatureDetectionSwSupport",null]},-1],["cr:7385",["clearIntervalWWW"],{"__rc":["clearIntervalWWW",null]},-1],["cr:7386",["clearTimeoutWWW"],{"__rc":["clearTimeoutWWW",null]},-1],["cr:7389",["setIntervalAcrossTransitionsWWW"],{"__rc":["setIntervalAcrossTransitionsWWW",null]},-1],["cr:7390",["setTimeoutWWW"],{"__rc":["setTimeoutWWW",null]},-1],["cr:7391",["setTimeoutAcrossTransitionsWWW"],{"__rc":["setTimeoutAcrossTransitionsWWW",null]},-1],["cr:7730",["getFbtResult"],{"__rc":["getFbtResult",null]},-1],["cr:7907",[],{"__rc":[null,null]},-1],["cr:8958",["FBJSON"],{"__rc":["FBJSON",null]},-1],["cr:8959",["DTSG"],{"__rc":["DTSG",null]},-1],["cr:8960",["DTSG_ASYNC"],{"__rc":["DTSG_ASYNC",null]},-1],["cr:9713",[],{"__rc":[null,null]},-1],["cr:696703",["JSScheduler"],{"__rc":["JSScheduler",null]},-1],["cr:951783",[],{"__rc":[null,null]},-1],["cr:1984081",[],{"__rc":[null,null]},-1],["IntlCurrentLocale",[],{"code":"en_US"},5954],["WAMediaWasmWorkerResource",[],{"url":"https:\/\/static.whatsapp.net\/rsrc.php\/v3\/yz\/r\/iqQa0jgcX_h.js?_nc_x=Ij3Wp8lg5Kz","name":"WAMediaWasmWorkerBundle","dynamicModules":{"cr:6640":{"__bbox":{"__rc":[null,null]}}},"hsdp":{"bxData":{"236":{"uri":"https:\/\/static.whatsapp.net\/rsrc.php\/y_\/r\/IbbOXFaNJTy.wasm"},"6945":{"uri":"https:\/\/static.whatsapp.net\/rsrc.php\/yO\/r\/tL59TT3QfUA.wasm"},"6946":{"uri":"https:\/\/static.whatsapp.net\/rsrc.php\/yf\/r\/rogm88TRRiw.wasm"}},"qplData":{"106":{}}}},7438],["CookiePrivacySandboxConfig",[],{"is_affected_by_samesite_lax":false},7723],["WAWebMediaWorkerResource",[],{"url":"https:\/\/static.whatsapp.net\/rsrc.php\/v3\/yK\/r\/PzZ2siM8S-2.js?_nc_x=Ij3Wp8lg5Kz","name":"WAMediaWorkerBundle","useInitScript":true},7034],["CometPersistQueryParams",[],{"relative":{},"domain":{}},6231],["CookieDomain",[],{"domain":"whatsapp.com"},6421],["IntlVariationHoldout",[],{"disable_variation":false},6533],["IntlNumberTypeProps",["IntlCLDRNumberType05"],{"module":{"__m":"IntlCLDRNumberType05"}},7027],["WAWebRgbaToWebpWorkerResource",[],{"url":"https:\/\/static.whatsapp.net\/rsrc.php\/v3\/y-\/r\/numw29EV6UR.js?_nc_x=Ij3Wp8lg5Kz","name":"WAWebRgbaToWebpWorkerBundle","dynamicModules":{"cr:6640":{"__bbox":{"__rc":[null,null]}}},"hsdp":{}},7480],["WAWebFtsWorkerResource",[],{"url":"https:\/\/static.whatsapp.net\/rsrc.php\/v3\/yc\/r\/lRajA4A_zrm.js?_nc_x=Ij3Wp8lg5Kz","name":"WAWebFtsWorkerBundle","dynamicModules":{"cr:6640":{"__bbox":{"__rc":[null,null]}}},"hsdp":{"gkxData":{"26258":{"result":true,"hash":null},"26259":{"result":false,"hash":null}},"justknobxData":{"1617":{"r":155},"1618":{"r":8},"1619":{"r":1},"1620":{"r":2},"1621":{"r":4},"1622":{"r":0},"1623":{"r":6},"1624":{"r":1},"1662":{"r":2},"1663":{"r":14},"1664":{"r":2},"1854":{"r":false}}}},7301],["WAWebOpusRecorderWorkerResource",[],{"url":"https:\/\/static.whatsapp.net\/rsrc.php\/v3\/yw\/r\/_OjcNvgJb7R.js?_nc_x=Ij3Wp8lg5Kz","name":"WAWebOpusRecorderWorkerBundle","hsdp":{}},7347],["WAWebWebpToRgbaWorkerResource",[],{"url":"https:\/\/static.whatsapp.net\/rsrc.php\/v3\/yi\/r\/e0-X3rIGNEC.js?_nc_x=Ij3Wp8lg5Kz","name":"WAWebWebpToRgbaWorkerBundle","dynamicModules":{"cr:6640":{"__bbox":{"__rc":[null,null]}}},"hsdp":{}},7481],["GetAsyncParamsExtraData",[],{"extra_data":{}},7511],["BootloaderConfig",[],{"deferBootloads":false,"jsRetries":[200,500],"jsRetryAbortNum":2,"jsRetryAbortTime":5,"silentDups":true,"timeout":60000,"tieredLoadingFromTier":100,"hypStep4":true,"phdOn":false,"btCutoffIndex":259,"fastPathForAlreadyRequired":true,"earlyRequireLazy":false,"enableTimeoutLoggingForNonComet":false,"deferLongTailManifest":true,"lazySoT":false,"transLine truncated
|
||||
<script type="application/json" data-content-len="83" data-sjs>{"require":[["qplTimingsServerJS",null,null,["7385063454178867264","tierTwoEnd"]]]}</script>
|
||||
|
||||
<script type="application/json" data-content-len="161" data-sjs>{"require":[["qplTimingsServerJS",null,null,["7385063454178867264","tierThree"]],["qplTimingsServerJS",null,null,["7385063454178867264-server","tierThree",51]]]}</script>
|
||||
<link rel="preload" href="https://static.whatsapp.net/rsrc.php/v3/yw/l/0,cross/J-dbIEvJjBeMKI_FluK_QX.css?_nc_x=Ij3Wp8lg5Kz" as="style" crossorigin="anonymous" />
|
||||
<script type="application/json" data-content-len="2043" data-sjs>{"require":[["HasteSupportData","handle",null,[{"bxData":{"482":{"uri":"https:\/\/static.whatsapp.net\/rsrc.php\/v3\/yS\/r\/aGcVD59xVTb.png"},"484":{"uri":"https:\/\/static.whatsapp.net\/rsrc.php\/v3\/y6\/r\/wa669aeJeom.png"},"9544":{"uri":"https:\/\/static.whatsapp.net\/rsrc.php\/yv\/r\/6Ib-MkrweJ6.ttf"},"9545":{"uri":"https:\/\/static.whatsapp.net\/rsrc.php\/yk\/r\/ln-uwGsChQW.ttf"},"9546":{"uri":"https:\/\/static.whatsapp.net\/rsrc.php\/v3\/yf\/r\/4l5Jzvrg84p.png"},"9551":{"uri":"https:\/\/static.whatsapp.net\/rsrc.php\/v3\/yq\/r\/xCHrap4ZvgY.png"},"9552":{"uri":"https:\/\/static.whatsapp.net\/rsrc.php\/v3\/y6\/r\/R6k6NrF2Cbl.png"},"9553":{"uri":"https:\/\/static.whatsapp.net\/rsrc.php\/v3\/yd\/r\/jLlIEwASpgj.png"},"9554":{"uri":"https:\/\/static.whatsapp.net\/rsrc.php\/v3\/y5\/r\/_U7GO8EnPFR.png"},"9559":{"uri":"https:\/\/static.whatsapp.net\/rsrc.php\/v3\/yy\/r\/noBSk9ItwJJ.png"},"9563":{"uri":"https:\/\/static.whatsapp.net\/rsrc.php\/yQ\/r\/WElxnwAGYY2.mp3"},"9564":{"uri":"https:\/\/static.whatsapp.net\/rsrc.php\/yv\/r\/ze2kHBOq8T0.mp3"},"9583":{"uri":"https:\/\/static.whatsapp.net\/rsrc.php\/v3\/yP\/r\/re6de1QF8pQ.png"},"10047":{"uri":"https:\/\/static.whatsapp.net\/rsrc.php\/v1\/y6\/r\/kNeGAbVVWRf.jpg"},"10591":{"uri":"https:\/\/static.whatsapp.net\/rsrc.php\/v3\/yp\/r\/ReKDgNfJEFB.png"},"14943":{"uri":"https:\/\/static.whatsapp.net\/rsrc.php\/v3\/ye\/r\/W2MDyeo0zkf.png"},"9557":{"uri":"https:\/\/static.whatsapp.net\/rsrc.php\/v3\/y5\/r\/cJXjAoffU5Q.png"},"9558":{"uri":"https:\/\/static.whatsapp.net\/rsrc.php\/v3\/yB\/r\/7Y1jh45L_8V.png"},"9565":{"uri":"https:\/\/static.whatsapp.net\/rsrc.php\/yn\/r\/6swO8kJM8z2.mp4"},"9566":{"uri":"https:\/\/static.whatsapp.net\/rsrc.php\/yk\/r\/GoZyw2bTK6s.mp4"},"9560":{"uri":"https:\/\/static.whatsapp.net\/rsrc.php\/v3\/yo\/r\/Oh4t5oP-H5G.png"}},"clpData":{"1837559":{"r":1},"1848815":{"r":10000,"s":1},"1828945":{"r":100,"s":1},"5779":{"r":100,"s":1}},"justknobxData":{"2454":{"r":false},"2450":{"r":true},"2451":{"r":true},"2452":{"r":false},"2453":{"r":false}}}]]]}</script>
|
||||
<script type="application/json" data-content-len="53047" data-sjs>{"require":[["Bootloader","handlePayload",null,[{"consistency":{"rev":1014522270},"rsrcMap":{"XGxTRzz":{"type":"js","src":"https:\/\/static.whatsapp.net\/rsrc.php\/v3iMK54\/yW\/l\/rt\/bMaImlcHRUG.js?_nc_x=Ij3Wp8lg5Kz","tsrc":"https:\/\/static.whatsapp.net\/rsrc-translations.php\/v6iMK54\/y2\/l\/en_US\/bMaImlcHRUG.js?_nc_x=Ij3Wp8lg5Kz","c":1,"p":":32","m":"1014522270_main"},"mRZc\/dz":{"type":"js","src":"https:\/\/static.whatsapp.net\/rsrc.php\/v3\/yx\/r\/m96dxlCrt6Y.js?_nc_x=Ij3Wp8lg5Kz","c":1,"p":":26","m":"1014522270_main"},"eGE9FvR":{"type":"js","src":"https:\/\/static.whatsapp.net\/rsrc.php\/v3i0ta4\/yi\/l\/rt\/Ad_ijr7_dvd.js?_nc_x=Ij3Wp8lg5Kz","tsrc":"https:\/\/static.whatsapp.net\/rsrc-translations.php\/v6i0ta4\/ym\/l\/en_US\/Ad_ijr7_dvd.js?_nc_x=Ij3Wp8lg5Kz","c":1,"p":":23","m":"1014522270_main"},"ssxIBpF":{"type":"js","src":"https:\/\/static.whatsapp.net\/rsrc.php\/v3\/yZ\/r\/FPlNguXyB9e0gGYzfda2TufH5Ma2uwGKhY6Mk0m90rCLDQVEH-g-FDC.js?_nc_x=Ij3Wp8lg5Kz","c":1,"p":":30,22,15,16,9","m":"1014522270_main"},"GkupZaS":{"type":"js","src":"https:\/\/static.whatsapp.net\/rsrc.php\/v3iCqe4\/yL\/l\/rt\/2gPPVV2aJv5.js?_nc_x=Ij3Wp8lg5Kz","tsrc":"https:\/\/static.whatsapp.net\/rsrc-translations.php\/v6iCqe4\/yu\/l\/en_US\/2gPPVV2aJv5.js?_nc_x=Ij3Wp8lg5Kz","c":1,"p":":25","m":"1014522270_main"},"EKhm2GG":{"type":"css","src":"https:\/\/static.whatsapp.net\/rsrc.php\/v3\/yw\/l\/0,cross\/J-dbIEvJjBeMKI_FluK_QX.css?_nc_x=Ij3Wp8lg5Kz","c":1,"p":":27,31"},"IYqO5iZ":{"type":"js","src":"https:\/\/static.whatsapp.net\/rsrc.php\/v3\/yt\/r\/ZqmYe3zRs7G.js?_nc_x=Ij3Wp8lg5Kz","c":1,"p":":17","m":"1014522270_main"},"tslua06":{"type":"js","src":"https:\/\/static.whatsapp.net\/rsrc.php\/v3i9SF4\/yS\/l\/rt\/Z3qyCROs6-7.js?_nc_x=Ij3Wp8lg5Kz","tsrc":"https:\/\/static.whatsapp.net\/rsrc-translations.php\/v6i9SF4\/yp\/l\/en_US\/Z3qyCROs6-7.js?_nc_x=Ij3Wp8lg5Kz","c":1,"p":":24","m":"1014522270_main"},"aQxqT5q":{"type":"js","src":"https:\/\/static.whatsapp.net\/rsrc.php\/v3ifY44\/yE\/l\/rt\/YaSkaFGEr-I.js?_nc_x=Ij3Wp8lg5Kz","tsrc":"https:\/\/static.whatsapp.net\/rsrc-translations.php\/v6ifY44\/y6\/l\/en_US\/YaSkaFGEr-I.js?_nc_x=Ij3Wp8lg5Kz","c":1,"p":":35","m":"1014522270_main"},"DrHIMI8":{"type":"js","src":"https:\/\/static.whatsapp.net\/rsrc.php\/v3iteW4\/yc\/l\/rt\/3apowRxtrqR.js?_nc_x=Ij3Wp8lg5Kz","tsrc":"https:\/\/static.whatsapp.net\/rsrc-translations.php\/v6iteW4\/yE\/l\/en_US\/3apowRxtrqR.js?_nc_x=Ij3Wp8lg5Kz","c":1,"p":":131","m":"1014522270_main"},"sevwJOE":{"type":"js","src":"https:\/\/static.whatsapp.net\/rsrc.php\/v3iOwq4\/y0\/l\/rt\/k5EhubSdxpm.js?_nc_x=Ij3Wp8lg5Kz","tsrc":"https:\/\/static.whatsapp.net\/rsrc-translations.php\/v6iOwq4\/y6\/l\/en_US\/k5EhubSdxpm.js?_nc_x=Ij3Wp8lg5Kz","c":1,"p":":21","m":"1014522270_main"},"DcG2ejT":{"type":"js","src":"https:\/\/static.whatsapp.net\/rsrc.php\/v3ipMd4\/y0\/l\/rt\/GquHjbFn54u.js?_nc_x=Ij3Wp8lg5Kz","tsrc":"https:\/\/static.whatsapp.net\/rsrc-translations.php\/v6ipMd4\/yL\/l\/en_US\/GquHjbFn54u.js?_nc_x=Ij3Wp8lg5Kz","c":1,"p":":153","m":"1014522270_main"},"YS\/UrnR":{"type":"js","src":"https:\/\/static.whatsapp.net\/rsrc.php\/v3iXDa4\/yV\/l\/rt\/Zz-Ouubt7zy.js?_nc_x=Ij3Wp8lg5Kz","tsrc":"https:\/\/static.whatsapp.net\/rsrc-translations.php\/v6iXDa4\/yS\/l\/en_US\/Zz-Ouubt7zy.js?_nc_x=Ij3Wp8lg5Kz","c":1,"p":":34","m":"1014522270_main"},"zG5Hlng":{"type":"js","src":"https:\/\/static.whatsapp.net\/rsrc.php\/v3\/yy\/r\/OkuaRXwmGgU.js?_nc_x=Ij3Wp8lg5Kz","c":1,"p":":28","m":"1014522270_main"},"0SXrVmz":{"type":"js","src":"https:\/\/static.whatsapp.net\/rsrc.php\/v3iuDY4\/y0\/l\/rt\/vA2-0lRViF_.js?_nc_x=Ij3Wp8lg5Kz","tsrc":"https:\/\/static.whatsapp.net\/rsrc-translations.php\/v6iuDY4\/yM\/l\/en_US\/vA2-0lRViF_.js?_nc_x=Ij3Wp8lg5Kz","c":1,"p":":29","m":"1014522270_main"},"kLGrgwC":{"type":"js","src":"https:\/\/static.whatsapp.net\/rsrc.php\/v3inji4\/yG\/l\/rt\/vWzRuWZmn1W.js?_nc_x=Ij3Wp8lg5Kz","tsrc":"https:\/\/static.whatsapp.net\/rsrc-translations.php\/v6inji4\/yj\/l\/en_US\/vWzRuWZmn1W.js?_nc_x=Ij3Wp8lg5Kz","c":1,"p":":174","m":"1014522270_main"},"hikr3RH":{"type":"js","src":"https:\/\/static.whatsapp.net\/rsrc.php\/v3iYv84\/yJ\/l\/rt\/N41CKm1lGBa.js?_nc_x=Ij3Wp8lg5Kz","tsrc":"https:\/\/static.whatsapp.net\/rsrc-translations.php\/v6iYv84\/yj\/l\/en_US\/N41CKm1lGBa.js?_nc_x=Ij3Wp8lg5Kz","c":1,"p":":57","m":"1014522270_main"},"fY+RSol":{"type":"js","src":"https:\/\/static.whatsapp.net\/rsrc.php\/v3iNj84\/y9\/l\/rt\/6rn8EUgGGpB.js?_nc_x=Ij3Wp8lg5Kz","tsrc":"https:\/\/static.whatsapp.net\/rsrc-translations.php\/v6iNj84\/yI\/l\/en_US\/6rn8EUgGGpB.js?_nc_x=Ij3Wp8lg5Kz","c":1,"p":":122","m":"1014522270_main"},"\/9QLqMI":{"type":"js","src":"https:\/\/static.whatsapp.net\/rsrc.php\/v3\/yb\/r\/qXYvgBWQS1q.js?_nc_x=Ij3Wp8lg5Kz","c":1,"p":":113","m":"1014522270_main"},"rfKxJtC":{"type":"js","src":"https:\/\/static.whatsapp.net\/rsrc.php\/v3iJyQ4\/yM\/l\/rt\/Ul0GyaekHhI.js?_nc_x=Ij3Wp8lg5Kz","tsrc":"https:\/\/static.whatsapp.net\/Line truncated
|
||||
<script type="application/json" data-content-len="130" data-sjs>{"require":[["DeferredJSResourceScheduler",null,null,[["XGxTRzz","mRZc\/dz","eGE9FvR","ssxIBpF","GkupZaS","IYqO5iZ","tslua06"]]]]}</script>
|
||||
<script type="application/json" data-content-len="7789" data-sjs>{"require":[["ScheduledServerJSWithCSS","handle",null,[{"__bbox":{"define":[["cr:104",[],{"__rc":[null,null]},-1],["cr:291",["ReactDOMServer-prod.modern"],{"__rc":["ReactDOMServer-prod.modern",null]},-1],["cr:293",["LexicalHistoryPlugin.prod"],{"__rc":["LexicalHistoryPlugin.prod",null]},-1],["cr:370",["Lexical.prod"],{"__rc":["Lexical.prod",null]},-1],["cr:427",["LexicalComposer.prod"],{"__rc":["LexicalComposer.prod",null]},-1],["cr:509",["LexicalComposerContext.prod"],{"__rc":["LexicalComposerContext.prod",null]},-1],["cr:629",[],{"__rc":[null,null]},-1],["cr:739",["LexicalPlainTextPlugin.prod"],{"__rc":["LexicalPlainTextPlugin.prod",null]},-1],["cr:988",["LexicalTreeView.prod"],{"__rc":["LexicalTreeView.prod",null]},-1],["cr:1066",[],{"__rc":[null,null]},-1],["cr:2015",[],{"__rc":[null,null]},-1],["cr:2120",["LexicalClearEditorPlugin.prod"],{"__rc":["LexicalClearEditorPlugin.prod",null]},-1],["cr:2180",["LexicalSelection.prod"],{"__rc":["LexicalSelection.prod",null]},-1],["cr:2260",["LexicalErrorBoundary.prod"],{"__rc":["LexicalErrorBoundary.prod",null]},-1],["cr:2774",["LexicalUtils.prod"],{"__rc":["LexicalUtils.prod",null]},-1],["cr:3107",[],{"__rc":[null,null]},-1],["cr:5037",[],{"__rc":[null,null]},-1],["cr:5685",[],{"__rc":[null,null]},-1],["cr:5735",[],{"__rc":[null,null]},-1],["cr:5736",[],{"__rc":[null,null]},-1],["cr:5923",[],{"__rc":[null,null]},-1],["cr:5932",[],{"__rc":[null,null]},-1],["cr:5959",[],{"__rc":[null,null]},-1],["cr:5990",[],{"__rc":[null,null]},-1],["cr:5993",[],{"__rc":[null,null]},-1],["cr:5994",[],{"__rc":[null,null]},-1],["cr:5997",[],{"__rc":[null,null]},-1],["cr:6003",[],{"__rc":[null,null]},-1],["cr:6004",[],{"__rc":[null,null]},-1],["cr:6005",[],{"__rc":[null,null]},-1],["cr:6006",[],{"__rc":[null,null]},-1],["cr:6010",[],{"__rc":[null,null]},-1],["cr:6018",[],{"__rc":[null,null]},-1],["cr:6019",[],{"__rc":[null,null]},-1],["cr:6842",[],{"__rc":[null,null]},-1],["cr:6862",[],{"__rc":[null,null]},-1],["cr:7456",[],{"__rc":[null,null]},-1],["cr:8723",[],{"__rc":[null,null]},-1],["cr:8750",[],{"__rc":[null,null]},-1],["cr:8751",[],{"__rc":[null,null]},-1],["cr:8752",[],{"__rc":[null,null]},-1],["cr:8753",[],{"__rc":[null,null]},-1],["cr:9051",[],{"__rc":[null,null]},-1],["cr:30",[],{"__rc":[null,null]},-1],["cr:8828",[],{"__rc":[null,null]},-1],["cr:1094907",[],{"__rc":[null,null]},-1],["WAWebWebpToAnimationFramesWorkerResource",[],{"url":"https:\/\/static.whatsapp.net\/rsrc.php\/v3\/yj\/r\/keURb6s4gBJ.js?_nc_x=Ij3Wp8lg5Kz","name":"WAWebWebpToAnimationFramesWorkerBundle","dynamicModules":{"cr:6640":{"__bbox":{"__rc":[null,null]}}},"hsdp":{}},7485],["cr:4986",[],{"__rc":[null,null]},-1],["cr:7004",["ReactReconcilerConstants.classic"],{"__rc":["ReactReconcilerConstants.classic",null]},-1],["cr:7007",["ReactReconciler.classic"],{"__rc":["ReactReconciler.classic",null]},-1],["cr:2695",[],{"__rc":[null,null]},-1],["cr:6008",[],{"__rc":[null,null]},-1],["cr:2904",["LexicalHistory.prod"],{"__rc":["LexicalHistory.prod",null]},-1],["cr:2181",["LexicalText.prod"],{"__rc":["LexicalText.prod",null]},-1],["cr:2903",["LexicalDragon.prod"],{"__rc":["LexicalDragon.prod",null]},-1],["cr:2909",["LexicalPlainText.prod"],{"__rc":["LexicalPlainText.prod",null]},-1],["cr:4793",["useLexicalEditable.prod"],{"__rc":["useLexicalEditable.prod",null]},-1],["cr:9917",["LexicalDevtoolsCore.prod"],{"__rc":["LexicalDevtoolsCore.prod",null]},-1],["cr:7002",["ReactReconcilerConstants-prod.classic"],{"__rc":["ReactReconcilerConstants-prod.classic",null]},-1],["cr:6994",["ReactReconciler-prod.classic"],{"__rc":["ReactReconciler-prod.classic",null]},-1],["cr:862",["LexicalClipboard.prod"],{"__rc":["LexicalClipboard.prod",null]},-1],["cr:1497",["LexicalHtml.prod"],{"__rc":["LexicalHtml.prod",null]},-1],["cr:1950",["LexicalTable.prod"],{"__rc":["LexicalTable.prod",null]},-1],["cr:2905",["LexicalLink.prod"],{"__rc":["LexicalLink.prod",null]},-1],["cr:3295",["LexicalMark.prod"],{"__rc":["LexicalMark.prod",null]},-1]],"require":[["CometSuspenseFalcoEvent"],["WAWebMainWrapper"],["WAFtsMultiLangTokenizer"],["WAWebEmojiAssetLoaderConfig"],["asn1js-2.1.1"],["WAWebEmojiAssetLoader"],["WAWebSetWorkerSafeHandlerApi"],["WAWebSetFrontendHandlerApi"],["WAWebPipVideoStreaming"],["WAWebLinkDevice.react"],["WAWebClearAppStates"],["FbtLogging"],["IntlQtEventFalcoEvent"],["emptyFunction","thatReturns",["RequireDeferredReference"],[[{"__dr":"CometSuspenseFalcoEvent"},{"__dr":"WAWebMainWrapper"},{"__dr":"WAFtsMultiLangTokenizer"},{"__dr":"WAWebEmojiAssetLoaderConfig"},{"__dr":"asn1js-2.1.1"},{"__dr":"WAWebEmojiAssetLoader"},{"__dr":"WAWebSetWorkerSafeHandlerApi"},{"__dr":"WAWebSetFrontendHandlerApi"},{"__dr":"WAWebPipVideoStreaming"},{"__dr":"WAWebLinkDevice.react"},{"__dr":"WAWebClearAppStates"},{"__dr":"FbtLogging"},{"__dr":"IntlQtEventFalcoEvent"}]]],["CometBTManifestLoader","init",[],[]],["ServiceWorkerCacheManagement","manageCache",[],[]],["WAWebEmojiSetup"],["BLine truncated
|
||||
<script type="application/json" data-content-len="85" data-sjs>{"require":[["qplTimingsServerJS",null,null,["7385063454178867264","tierThreeEnd"]]]}</script>
|
||||
|
||||
<script type="application/json" data-content-len="3705" data-sjs>{"require":[["CometQPLPayloadStore","storePayloadMap",null,[{"7385063454178867264":{"css\/7f2fjyjrk0cocg0s.pkg,css\/dkp7c20r8cg08o40.pkg,css\/a2k3bgidd4gsc8g0.pkg.__composite__.css":{"url":"https:\/\/static.whatsapp.net\/rsrc.php\/v3\/yv\/l\/0,cross\/C-WCp2i_C8v1k_iwjHwlnlCsHvfmgKJ5t.css?_nc_x=Ij3Wp8lg5Kz","refs":["htmlStart","tierOne","tierTwo","tierThree"]},"js\/1uuwsb5hks5c8c8s.pkg.js":{"url":"https:\/\/static.whatsapp.net\/rsrc.php\/v3\/yH\/r\/ht8oIC_2H5Y.js?_nc_x=Ij3Wp8lg5Kz","refs":["htmlStart","tierOne","tierTwo","tierThree"]},"js\/cuwb8pci4r48scww.pkg.js":{"url":"https:\/\/static.whatsapp.net\/rsrc.php\/v3\/yV\/r\/yJJyU7535wL.js?_nc_x=Ij3Wp8lg5Kz","refs":["htmlStart","tierOne","tierTwo"]},"js\/74ff0y83uq044k80.pkg,js\/7l01ghsx2tc04kcc.pkg,js\/boe4n1nexrwc0ws8.pkg.__composite__.js":{"url":"https:\/\/static.whatsapp.net\/rsrc.php\/v3\/y3\/r\/GpR8XYkiC6brtOgQ7kvhNmryQSgyNOW8X.js?_nc_x=Ij3Wp8lg5Kz","refs":["htmlStart","tierOne","tierTwo","tierThree"]},"js\/bjjs1pmj96gwss4w.pkg.js":{"url":"https:\/\/static.whatsapp.net\/rsrc.php\/v3\/y3\/r\/y6YE2mWjTXs.js?_nc_x=Ij3Wp8lg5Kz","refs":["htmlStart","tierOne","tierTwo","tierThree"]},"js\/ckuz7ky8y9cssc8s.pkg.js":{"url":"https:\/\/static.whatsapp.net\/rsrc.php\/v3iPAE4\/yj\/l\/rt\/3A4_-xOlNcy.js?_nc_x=Ij3Wp8lg5Kz","refs":["htmlStart","tierTwo","tierThree"]},"js\/45xcw3rbngqo884k.pkg.js":{"url":"https:\/\/static.whatsapp.net\/rsrc.php\/v3iL1C4\/yq\/l\/rt\/D6yUOPmld1J.js?_nc_x=Ij3Wp8lg5Kz","refs":["htmlStart","tierTwo","tierThree"]},"js\/93xi0q6qcx0ksosc.pkg,js\/ajfm1uik02woo48s.pkg,js\/csa9qir353c4goc0.pkg.__composite__.js":{"url":"https:\/\/static.whatsapp.net\/rsrc.php\/v3iD_74\/yY\/l\/rt\/OC6RHC1oEdtsQ3d6kRL04kvsWqehdEtWw.js?_nc_x=Ij3Wp8lg5Kz","refs":["htmlStart","tierTwo","tierThree"]},"js\/cq8i5dizka0oow8o.pkg.js":{"url":"https:\/\/static.whatsapp.net\/rsrc.php\/v3\/yD\/r\/mY5slrRbdpI.js?_nc_x=Ij3Wp8lg5Kz","refs":["htmlStart","tierTwo","tierThree"]},"js\/bm1z2ax0ttkwg8w8.pkg.js":{"url":"https:\/\/static.whatsapp.net\/rsrc.php\/v3\/yt\/r\/fyeayg1M_Ow.js?_nc_x=Ij3Wp8lg5Kz","refs":["htmlStart","tierTwo","tierThree"]},"css\/whatsapp\/web\/WhatsAppWebSplashScreen.css":{"url":"https:\/\/static.whatsapp.net\/rsrc.php\/v3\/yt\/l\/0,cross\/ZrtiQW1FOc2.css?_nc_x=Ij3Wp8lg5Kz","refs":["tierOne"]},"js\/3iilb6qol88wkw0k.pkg.js":{"url":"https:\/\/static.whatsapp.net\/rsrc.php\/v3iMK54\/yW\/l\/rt\/bMaImlcHRUG.js?_nc_x=Ij3Wp8lg5Kz","refs":["tierThree"]},"js\/38kxevuv39q8kgkc.pkg.js":{"url":"https:\/\/static.whatsapp.net\/rsrc.php\/v3\/yx\/r\/m96dxlCrt6Y.js?_nc_x=Ij3Wp8lg5Kz","refs":["tierThree"]},"js\/dy1urilp36ogkcc8.pkg.js":{"url":"https:\/\/static.whatsapp.net\/rsrc.php\/v3i0ta4\/yi\/l\/rt\/Ad_ijr7_dvd.js?_nc_x=Ij3Wp8lg5Kz","refs":["tierThree"]},"js\/cdkr9wbfnr40ssg0.pkg,js\/7mvpi199fpwc44ss.pkg,js\/3121sq3e8pmo44k0.pkg,js\/7y8xjqkbm0w0kks8.pkg,js\/529myxqorq4gkcgw.pkg.__composite__.js":{"url":"https:\/\/static.whatsapp.net\/rsrc.php\/v3\/yZ\/r\/FPlNguXyB9e0gGYzfda2TufH5Ma2uwGKhY6Mk0m90rCLDQVEH-g-FDC.js?_nc_x=Ij3Wp8lg5Kz","refs":["tierThree"]},"js\/bc0aut869zk84448.pkg.js":{"url":"https:\/\/static.whatsapp.net\/rsrc.php\/v3iCqe4\/yL\/l\/rt\/2gPPVV2aJv5.js?_nc_x=Ij3Wp8lg5Kz","refs":["tierThree"]},"css\/ezxomokk1nw4w04k.pkg,css\/7bsaf1zhofk8c440.pkg.__composite__.css":{"url":"https:\/\/static.whatsapp.net\/rsrc.php\/v3\/yw\/l\/0,cross\/J-dbIEvJjBeMKI_FluK_QX.css?_nc_x=Ij3Wp8lg5Kz","refs":["tierThree"]},"js\/dt5kj0sja4o4kwos.pkg.js":{"url":"https:\/\/static.whatsapp.net\/rsrc.php\/v3\/yt\/r\/ZqmYe3zRs7G.js?_nc_x=Ij3Wp8lg5Kz","refs":["tierThree"]},"js\/8v1j52evupkwsc0o.pkg.js":{"url":"https:\/\/static.whatsapp.net\/rsrc.php\/v3i9SF4\/yS\/l\/rt\/Z3qyCROs6-7.js?_nc_x=Ij3Wp8lg5Kz","refs":["tierThree"]}}}]]]}</script>
|
||||
<script type="application/json" data-content-len="69" data-sjs>{"require":[["qplTagServerJS",null,null,[["lastServerTagFlushed"]]]]}</script>
|
||||
<script type="application/json" data-content-len="1381" data-sjs>{"require":[["qplTimingsServerJS",null,null,["7385063454178867264-server","earlyFlushStart",6]],["qplTimingsServerJS",null,null,["7385063454178867264-server","earlyFlushEnd",12]],["qplTimingsServerJS",null,null,["7385063454178867264-server","earlyFlush",12]],["qplTimingsServerJS",null,null,["7385063454178867264-server","cometPageRender",13]],["qplTimingsServerJS",null,null,["7385063454178867264-server","genTierOneHasteResponseStart",14]],["qplTimingsServerJS",null,null,["7385063454178867264-server","genTierOneHasteResponseEnd",15]],["qplTimingsServerJS",null,null,["7385063454178867264-server","tierOnePreloadersFinished",16]],["qplTimingsServerJS",null,null,["7385063454178867264-server","genTierTwoHasteResponseStart",26]],["qplTimingsServerJS",null,null,["7385063454178867264-server","genTierTwoHasteResponseEnd",32]],["qplTimingsServerJS",null,null,["7385063454178867264-server","tierTwoPreloadersFinished",34]],["qplTimingsServerJS",null,null,["7385063454178867264-server","genTierThreeHasteResponseStart",39]],["qplTimingsServerJS",null,null,["7385063454178867264-server","genTierThreeHasteResponseEnd",50]],["qplTimingsServerJS",null,null,["7385063454178867264-server","tierThreePreloadersFinished",52]],["qplTimingsServerJS",null,null,["7385063454178867264-server","responseStart",1]],["qplTimingsServerJS",null,null,["7385063454178867264-server","responseEnd",53]]]}</script>
|
||||
<script type="application/json" data-content-len="2" data-sjs>{}</script>
|
||||
<script type="application/json" data-content-len="167" data-sjs>{"require":[["CometQPLPayloadStore","storePayloadBytesSent",null,[{"htmlStart":[15482,4730],"tierOne":[7727,2381],"tierTwo":[54050,14562],"tierThree":[63872,9826]}]]]}</script>
|
||||
<div id='has-finished-comet-page' hidden style='display: none;'></div></body></html>
|
||||
|
||||
@@ -7,8 +7,11 @@ const { LoadUtils } = require('./WahaInjected');
|
||||
const ChatFactory = require('whatsapp-web.js/src/factories/ChatFactory');
|
||||
|
||||
export class WebjsClient extends Client {
|
||||
async initialize() {
|
||||
await super.initialize();
|
||||
async inject() {
|
||||
// Even tho this.inject is not defined as interface in Client.ts
|
||||
// We can still call and override it
|
||||
// @ts-ignore
|
||||
await super.inject();
|
||||
//Load util functions (serializers, helper functions)
|
||||
await this.pupPage.evaluate(LoadUtils);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { UnprocessableEntityException } from '@nestjs/common/exceptions/unprocessable-entity.exception';
|
||||
import { IEngineMediaProcessor } from '@waha/core/abc/media.abc';
|
||||
import {
|
||||
getChannelInviteLink,
|
||||
WAHAInternalEvent,
|
||||
@@ -10,15 +9,17 @@ import {
|
||||
AvailableInPlusVersion,
|
||||
NotImplementedByEngineError,
|
||||
} from '@waha/core/exceptions';
|
||||
import { IMediaEngineProcessor } from '@waha/core/media/IMediaEngineProcessor';
|
||||
import { QR } from '@waha/core/QR';
|
||||
import { parseBool } from '@waha/helpers';
|
||||
import { CallData } from '@waha/structures/calls.dto';
|
||||
import {
|
||||
Channel,
|
||||
ChannelRole,
|
||||
CreateChannelRequest,
|
||||
ListChannelsQuery,
|
||||
} from '@waha/structures/channels.dto';
|
||||
import { GetChatsQuery } from '@waha/structures/chats.dto';
|
||||
import { ChatArchiveEvent, GetChatsQuery } from '@waha/structures/chats.dto';
|
||||
import {
|
||||
ChatRequest,
|
||||
CheckNumberStatusQuery,
|
||||
@@ -50,16 +51,22 @@ import {
|
||||
ParticipantsRequest,
|
||||
SettingsSecurityChangeInfo,
|
||||
} from '@waha/structures/groups.dto';
|
||||
import { Label, LabelID } from '@waha/structures/labels.dto';
|
||||
import { ReplyToMessage } from '@waha/structures/message.dto';
|
||||
import { WAMessage, WAMessageReaction } from '@waha/structures/responses.dto';
|
||||
import { MeInfo } from '@waha/structures/sessions.dto';
|
||||
import { WAMessageRevokedBody } from '@waha/structures/webhooks.dto';
|
||||
import { waitUntil } from '@waha/utils/promiseTimeout';
|
||||
import { SingleDelayedJobRunner } from '@waha/utils/SingleDelayedJobRunner';
|
||||
import {
|
||||
Call,
|
||||
Channel as WEBJSChannel,
|
||||
Chat,
|
||||
ClientOptions,
|
||||
Contact,
|
||||
Events,
|
||||
GroupChat,
|
||||
Label as WEBJSLabel,
|
||||
Location,
|
||||
Message,
|
||||
Reaction,
|
||||
@@ -79,7 +86,7 @@ export class WhatsappSessionWebJSCore extends WhatsappSession {
|
||||
engine = WAHAEngine.WEBJS;
|
||||
protected engineConfig?: WebJSConfig;
|
||||
|
||||
private startTimeoutId: null | ReturnType<typeof setTimeout> = null;
|
||||
private startDelayedJob: SingleDelayedJobRunner;
|
||||
private shouldRestart: boolean;
|
||||
|
||||
whatsapp: WebjsClient;
|
||||
@@ -89,6 +96,13 @@ export class WhatsappSessionWebJSCore extends WhatsappSession {
|
||||
super(config);
|
||||
this.qr = new QR();
|
||||
this.shouldRestart = true;
|
||||
|
||||
// Restart job if session failed
|
||||
this.startDelayedJob = new SingleDelayedJobRunner(
|
||||
'start-engine',
|
||||
this.START_ATTEMPT_DELAY_SECONDS * SECOND,
|
||||
this.logger,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -128,24 +142,21 @@ export class WhatsappSessionWebJSCore extends WhatsappSession {
|
||||
private restartClient() {
|
||||
if (!this.shouldRestart) {
|
||||
this.logger.debug(
|
||||
"Shouldn't restart the client, ignoring restart request",
|
||||
'Should not 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);
|
||||
this.startDelayedJob.schedule(async () => {
|
||||
if (!this.shouldRestart) {
|
||||
this.logger.warn(
|
||||
'Should not restart the client, ignoring restart request',
|
||||
);
|
||||
return;
|
||||
}
|
||||
await this.end();
|
||||
await this.start();
|
||||
});
|
||||
}
|
||||
|
||||
protected addProxyConfig(clientOptions: ClientOptions) {
|
||||
@@ -166,7 +177,6 @@ export class WhatsappSessionWebJSCore extends WhatsappSession {
|
||||
}
|
||||
|
||||
protected async init() {
|
||||
await this.end();
|
||||
this.shouldRestart = true;
|
||||
this.whatsapp = await this.buildClient();
|
||||
this.whatsapp
|
||||
@@ -225,9 +235,19 @@ export class WhatsappSessionWebJSCore extends WhatsappSession {
|
||||
private async end() {
|
||||
try {
|
||||
this.whatsapp?.removeAllListeners();
|
||||
clearInterval(this.startTimeoutId);
|
||||
this.startDelayedJob.cancel();
|
||||
// It's possible that browser yet starting
|
||||
await waitUntil(
|
||||
async () => {
|
||||
const result = !!this.whatsapp.pupBrowser;
|
||||
this.logger.debug(`Browser is ready to be closed: ${result}`);
|
||||
return result;
|
||||
},
|
||||
1_000,
|
||||
10_000,
|
||||
);
|
||||
this.whatsapp?.destroy().catch((error) => {
|
||||
this.logger.debug('Failed to destroy the client', error);
|
||||
this.logger.warn(error, 'Failed to destroy the client');
|
||||
});
|
||||
} catch (error) {
|
||||
this.logger.error(error);
|
||||
@@ -333,10 +353,7 @@ export class WhatsappSessionWebJSCore extends WhatsappSession {
|
||||
}
|
||||
|
||||
sendText(request: MessageTextRequest) {
|
||||
const options = {
|
||||
// It's fine to sent just ids instead of Contact object
|
||||
mentions: request.mentions as unknown as string[],
|
||||
};
|
||||
const options = this.getMessageOptions(request);
|
||||
return this.whatsapp.sendMessage(
|
||||
this.ensureSuffix(request.chatId),
|
||||
request.text,
|
||||
@@ -363,11 +380,7 @@ export class WhatsappSessionWebJSCore extends WhatsappSession {
|
||||
}
|
||||
|
||||
reply(request: MessageReplyRequest) {
|
||||
const options = {
|
||||
quotedMessageId: request.reply_to,
|
||||
// It's fine to sent just ids instead of Contact object
|
||||
mentions: request.mentions as unknown as string[],
|
||||
};
|
||||
const options = this.getMessageOptions(request);
|
||||
return this.whatsapp.sendMessage(request.chatId, request.text, options);
|
||||
}
|
||||
|
||||
@@ -387,7 +400,8 @@ export class WhatsappSessionWebJSCore extends WhatsappSession {
|
||||
const location = new Location(request.latitude, request.longitude, {
|
||||
name: request.title,
|
||||
});
|
||||
return this.whatsapp.sendMessage(request.chatId, location);
|
||||
const options = this.getMessageOptions(request);
|
||||
return this.whatsapp.sendMessage(request.chatId, location, options);
|
||||
}
|
||||
|
||||
async sendSeen(request: SendSeenRequest) {
|
||||
@@ -449,12 +463,12 @@ export class WhatsappSessionWebJSCore extends WhatsappSession {
|
||||
const messages = await chat.fetchMessages({
|
||||
limit: limit,
|
||||
});
|
||||
// Go over messages, download media, and convert to right format.
|
||||
const result = [];
|
||||
for (const message of messages) {
|
||||
const msg = await this.processIncomingMessage(message, downloadMedia);
|
||||
result.push(msg);
|
||||
const promises = [];
|
||||
for (const msg of messages) {
|
||||
promises.push(this.processIncomingMessage(msg, downloadMedia));
|
||||
}
|
||||
let result = await Promise.all(promises);
|
||||
result = result.filter(Boolean);
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -468,6 +482,52 @@ export class WhatsappSessionWebJSCore extends WhatsappSession {
|
||||
return chat.clearMessages();
|
||||
}
|
||||
|
||||
public chatsArchiveChat(chatId: string): Promise<any> {
|
||||
const id = this.ensureSuffix(chatId);
|
||||
return this.whatsapp.archiveChat(id);
|
||||
}
|
||||
|
||||
public chatsUnarchiveChat(chatId: string): Promise<any> {
|
||||
const id = this.ensureSuffix(chatId);
|
||||
return this.whatsapp.unarchiveChat(id);
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* Label methods
|
||||
*/
|
||||
|
||||
public async getLabels(): Promise<Label[]> {
|
||||
const labels = await this.whatsapp.getLabels();
|
||||
return labels.map(this.toLabel);
|
||||
}
|
||||
|
||||
public getChatsByLabelId(labelId: string) {
|
||||
return this.whatsapp.getChatsByLabelId(labelId);
|
||||
}
|
||||
|
||||
public async getChatLabels(chatId: string): Promise<Label[]> {
|
||||
const id = this.ensureSuffix(chatId);
|
||||
const labels = await this.whatsapp.getChatLabels(id);
|
||||
return labels.map(this.toLabel);
|
||||
}
|
||||
|
||||
public async putLabelsToChat(chatId: string, labels: LabelID[]) {
|
||||
const labelIds = labels.map((label) => label.id);
|
||||
const chatIds = [this.ensureSuffix(chatId)];
|
||||
await this.whatsapp.addOrRemoveLabels(labelIds, chatIds);
|
||||
}
|
||||
|
||||
protected toLabel(label: WEBJSLabel): Label {
|
||||
const color = label.colorIndex;
|
||||
return {
|
||||
id: label.id,
|
||||
name: label.name,
|
||||
color: color,
|
||||
colorHex: Label.toHex(color),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Contacts methods
|
||||
*/
|
||||
@@ -811,6 +871,28 @@ export class WhatsappSessionWebJSCore extends WhatsappSession {
|
||||
case WAHAEvents.GROUP_LEAVE:
|
||||
this.whatsapp.on(Events.GROUP_LEAVE, handler);
|
||||
return true;
|
||||
case WAHAEvents.CHAT_ARCHIVE:
|
||||
this.whatsapp.on('chat_archived', (chat, archived, _) => {
|
||||
const body: ChatArchiveEvent = {
|
||||
id: chat.id._serialized,
|
||||
archived: archived,
|
||||
timestamp: chat.timestamp,
|
||||
};
|
||||
handler(body);
|
||||
});
|
||||
return true;
|
||||
case WAHAEvents.CALL_RECEIVED:
|
||||
this.whatsapp.on('call', (call: Call) => {
|
||||
const body: CallData = {
|
||||
id: call.id,
|
||||
from: call.from,
|
||||
timestamp: call.timestamp,
|
||||
isVideo: call.isVideo,
|
||||
isGroup: call.isGroup,
|
||||
};
|
||||
handler(body);
|
||||
});
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
@@ -844,6 +926,7 @@ export class WhatsappSessionWebJSCore extends WhatsappSession {
|
||||
}
|
||||
|
||||
protected toWAMessage(message: Message): Promise<WAMessage> {
|
||||
const replyTo = this.extractReplyTo(message);
|
||||
// @ts-ignore
|
||||
return Promise.resolve({
|
||||
id: message.id._serialized,
|
||||
@@ -864,10 +947,24 @@ export class WhatsappSessionWebJSCore extends WhatsappSession {
|
||||
ackName: WAMessageAck[message.ack] || ACK_UNKNOWN,
|
||||
location: message.location,
|
||||
vCards: message.vCards,
|
||||
replyTo: replyTo,
|
||||
_data: message.rawData,
|
||||
});
|
||||
}
|
||||
|
||||
protected extractReplyTo(message: Message): ReplyToMessage | null {
|
||||
// @ts-ignore
|
||||
const quotedMsg = message.rawData.quotedMsg;
|
||||
if (!quotedMsg) {
|
||||
return;
|
||||
}
|
||||
return {
|
||||
id: quotedMsg.id?.id,
|
||||
participant: quotedMsg.author || quotedMsg.from,
|
||||
body: quotedMsg.caption || quotedMsg.body,
|
||||
};
|
||||
}
|
||||
|
||||
public async getEngineInfo() {
|
||||
// Add 1 seconds timeout
|
||||
return {
|
||||
@@ -884,11 +981,23 @@ export class WhatsappSessionWebJSCore extends WhatsappSession {
|
||||
|
||||
protected downloadMedia(message: Message) {
|
||||
const processor = new EngineMediaProcessor();
|
||||
return this.mediaManager.processMedia(processor, message);
|
||||
return this.mediaManager.processMedia(processor, message, this.name);
|
||||
}
|
||||
|
||||
protected getMessageOptions(request: any): any {
|
||||
let mentions = request.mentions;
|
||||
mentions = mentions ? mentions.map(this.ensureSuffix) : undefined;
|
||||
|
||||
const quotedMessageId = request.reply_to;
|
||||
|
||||
return {
|
||||
mentions: mentions,
|
||||
quotedMessageId: quotedMessageId,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export class EngineMediaProcessor implements IEngineMediaProcessor<Message> {
|
||||
export class EngineMediaProcessor implements IMediaEngineProcessor<Message> {
|
||||
hasMedia(message: Message): boolean {
|
||||
if (!message.hasMedia) {
|
||||
return false;
|
||||
|
||||
+97
-68
@@ -1,12 +1,12 @@
|
||||
import {
|
||||
ConsoleLogger,
|
||||
Injectable,
|
||||
LogLevel,
|
||||
NotFoundException,
|
||||
UnprocessableEntityException,
|
||||
} from '@nestjs/common';
|
||||
import { MediaNoopStorage } from '@waha/core/media/MediaNoopStorage';
|
||||
import { getPinoLogLevel, LoggerBuilder } from '@waha/utils/logging';
|
||||
import { promiseTimeout } from '@waha/utils/promiseTimeout';
|
||||
import { promiseTimeout, sleep } from '@waha/utils/promiseTimeout';
|
||||
import { EventEmitter } from 'events';
|
||||
import { PinoLogger } from 'nestjs-pino';
|
||||
|
||||
@@ -18,11 +18,9 @@ import {
|
||||
} from '../structures/enums.dto';
|
||||
import {
|
||||
ProxyConfig,
|
||||
SessionConfig,
|
||||
SessionDTO,
|
||||
SessionInfo,
|
||||
SessionLogoutRequest,
|
||||
SessionStartRequest,
|
||||
SessionStopRequest,
|
||||
} from '../structures/sessions.dto';
|
||||
import { WebhookConfig } from '../structures/webhooks.config.dto';
|
||||
import { SessionManager } from './abc/manager.abc';
|
||||
@@ -33,9 +31,8 @@ import { WhatsappSessionVenomCore } from './engines/venom/session.venom.core';
|
||||
import { WhatsappSessionWebJSCore } from './engines/webjs/session.webjs.core';
|
||||
import { DOCS_URL } from './exceptions';
|
||||
import { getProxyConfig } from './helpers.proxy';
|
||||
import { CoreMediaManager, MediaStorageCore } from './media.core';
|
||||
import { MediaManagerCore } from './media/MediaManagerCore';
|
||||
import { LocalSessionAuthRepository } from './storage/LocalSessionAuthRepository';
|
||||
import { LocalSessionConfigRepository } from './storage/LocalSessionConfigRepository';
|
||||
import { LocalStoreCore } from './storage/LocalStoreCore';
|
||||
import { WebhookConductorCore } from './webhooks.core';
|
||||
|
||||
@@ -49,7 +46,13 @@ export class OnlyDefaultSessionIsAllowed extends UnprocessableEntityException {
|
||||
|
||||
@Injectable()
|
||||
export class SessionManagerCore extends SessionManager {
|
||||
private session?: WhatsappSession;
|
||||
SESSION_STOP_TIMEOUT = 3000;
|
||||
|
||||
// session - exists and running (or failed or smth)
|
||||
// null - stopped
|
||||
// undefined - removed
|
||||
private session: WhatsappSession | undefined | null;
|
||||
private sessionConfig?: SessionConfig;
|
||||
DEFAULT = 'default';
|
||||
|
||||
// @ts-ignore
|
||||
@@ -63,20 +66,20 @@ export class SessionManagerCore extends SessionManager {
|
||||
) {
|
||||
super();
|
||||
this.events = new EventEmitter();
|
||||
this.session = null;
|
||||
this.sessionConfig = null;
|
||||
this.log.setContext(SessionManagerCore.name);
|
||||
this.session = undefined;
|
||||
const engineName = this.engineConfigService.getDefaultEngineName();
|
||||
this.EngineClass = this.getEngine(engineName);
|
||||
this.store = new LocalStoreCore(engineName.toLowerCase());
|
||||
this.sessionAuthRepository = new LocalSessionAuthRepository(this.store);
|
||||
this.sessionConfigRepository = new LocalSessionConfigRepository(this.store);
|
||||
this.startPredefinedSessions();
|
||||
}
|
||||
|
||||
protected startPredefinedSessions() {
|
||||
const startSessions = this.config.startSessions;
|
||||
startSessions.forEach((sessionName) => {
|
||||
this.start({ name: sessionName });
|
||||
this.start(sessionName);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -102,32 +105,48 @@ export class SessionManagerCore extends SessionManager {
|
||||
if (!this.session) {
|
||||
return;
|
||||
}
|
||||
await this.stop({ name: this.DEFAULT, logout: false });
|
||||
await this.stop(this.DEFAULT, true);
|
||||
}
|
||||
|
||||
//
|
||||
// API Methods
|
||||
//
|
||||
async start(request: SessionStartRequest): Promise<SessionDTO> {
|
||||
this.onlyDefault(request.name);
|
||||
async exists(name: string): Promise<boolean> {
|
||||
this.onlyDefault(name);
|
||||
return this.session !== undefined;
|
||||
}
|
||||
|
||||
isRunning(name: string): boolean {
|
||||
this.onlyDefault(name);
|
||||
return !!this.session;
|
||||
}
|
||||
|
||||
async upsert(name: string, config?: SessionConfig): Promise<void> {
|
||||
this.onlyDefault(name);
|
||||
this.sessionConfig = config;
|
||||
}
|
||||
|
||||
async start(name: string): Promise<SessionDTO> {
|
||||
this.onlyDefault(name);
|
||||
if (this.session) {
|
||||
throw new UnprocessableEntityException(
|
||||
`Session '${this.DEFAULT}' is already started.`,
|
||||
);
|
||||
}
|
||||
|
||||
const name = request.name;
|
||||
this.log.info(`'${name}' - starting session...`);
|
||||
const mediaManager = new CoreMediaManager(
|
||||
new MediaStorageCore(),
|
||||
const mediaManager = new MediaManagerCore(
|
||||
new MediaNoopStorage(),
|
||||
this.config.mimetypes,
|
||||
);
|
||||
const logger = this.log.logger.child({ session: name });
|
||||
logger.level = getPinoLogLevel(request.config?.debug);
|
||||
const logger = this.log.logger.child({
|
||||
session: name,
|
||||
sessionRunTimestamp: Date.now(),
|
||||
});
|
||||
logger.level = getPinoLogLevel(this.sessionConfig?.debug);
|
||||
const loggerBuilder: LoggerBuilder = logger;
|
||||
|
||||
const webhook = new this.WebhookConductorClass(loggerBuilder);
|
||||
const proxyConfig = this.getProxyConfig(request);
|
||||
const proxyConfig = this.getProxyConfig();
|
||||
const sessionConfig: SessionParams = {
|
||||
name,
|
||||
mediaManager,
|
||||
@@ -135,7 +154,7 @@ export class SessionManagerCore extends SessionManager {
|
||||
printQR: this.engineConfigService.shouldPrintQR,
|
||||
sessionStore: this.store,
|
||||
proxyConfig: proxyConfig,
|
||||
sessionConfig: request.config,
|
||||
sessionConfig: this.sessionConfig,
|
||||
};
|
||||
await this.sessionAuthRepository.init(name);
|
||||
// @ts-ignore
|
||||
@@ -143,7 +162,7 @@ export class SessionManagerCore extends SessionManager {
|
||||
this.session = session;
|
||||
|
||||
// configure webhooks
|
||||
const webhooks = this.getWebhooks(request);
|
||||
const webhooks = this.getWebhooks();
|
||||
webhook.configure(session, webhooks);
|
||||
|
||||
// configure events
|
||||
@@ -154,6 +173,7 @@ export class SessionManagerCore extends SessionManager {
|
||||
|
||||
// start session
|
||||
await session.start();
|
||||
logger.info('Session has been started.');
|
||||
return {
|
||||
name: session.name,
|
||||
status: session.status,
|
||||
@@ -161,13 +181,46 @@ export class SessionManagerCore extends SessionManager {
|
||||
};
|
||||
}
|
||||
|
||||
async stop(name: string, silent: boolean): Promise<void> {
|
||||
this.onlyDefault(name);
|
||||
if (!this.isRunning(name)) {
|
||||
this.log.debug(`Session is not running.`, { session: name });
|
||||
return;
|
||||
}
|
||||
|
||||
this.log.info(`Stopping session...`, { session: name });
|
||||
try {
|
||||
const session = this.getSession(name);
|
||||
await session.stop();
|
||||
} catch (err) {
|
||||
this.log.warn(`Error while stopping session '${name}'`);
|
||||
if (!silent) {
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
this.log.info(`Session has been stopped.`, { session: name });
|
||||
this.session = null;
|
||||
await sleep(this.SESSION_STOP_TIMEOUT);
|
||||
}
|
||||
|
||||
async logout(name: string): Promise<void> {
|
||||
this.onlyDefault(name);
|
||||
await this.sessionAuthRepository.clean(name);
|
||||
}
|
||||
|
||||
async delete(name: string): Promise<void> {
|
||||
this.onlyDefault(name);
|
||||
this.session = undefined;
|
||||
this.sessionConfig = undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Combine per session and global webhooks
|
||||
*/
|
||||
private getWebhooks(request: SessionStartRequest) {
|
||||
private getWebhooks() {
|
||||
let webhooks: WebhookConfig[] = [];
|
||||
if (request.config?.webhooks) {
|
||||
webhooks = webhooks.concat(request.config.webhooks);
|
||||
if (this.sessionConfig?.webhooks) {
|
||||
webhooks = webhooks.concat(this.sessionConfig.webhooks);
|
||||
}
|
||||
const globalWebhookConfig = this.config.getWebhookConfig();
|
||||
if (globalWebhookConfig) {
|
||||
@@ -179,71 +232,44 @@ export class SessionManagerCore extends SessionManager {
|
||||
/**
|
||||
* Get either session's or global proxy if defined
|
||||
*/
|
||||
protected getProxyConfig(
|
||||
request: SessionStartRequest,
|
||||
): ProxyConfig | undefined {
|
||||
if (request.config?.proxy) {
|
||||
return request.config.proxy;
|
||||
protected getProxyConfig(): ProxyConfig | undefined {
|
||||
if (this.sessionConfig?.proxy) {
|
||||
return this.sessionConfig.proxy;
|
||||
}
|
||||
if (!this.session) {
|
||||
return undefined;
|
||||
}
|
||||
const sessions = { [request.name]: this.session };
|
||||
return getProxyConfig(this.config, sessions, request.name);
|
||||
}
|
||||
|
||||
async stop(request: SessionStopRequest): Promise<void> {
|
||||
this.onlyDefault(request.name);
|
||||
|
||||
const name = request.name;
|
||||
this.log.info(`Stopping ${name} session...`);
|
||||
const session = this.getSession(name);
|
||||
await session.stop();
|
||||
this.log.info(`"${name}" has been stopped.`);
|
||||
this.session = undefined;
|
||||
}
|
||||
|
||||
async logout(request: SessionLogoutRequest) {
|
||||
const name = request.name;
|
||||
this.onlyDefault(request.name);
|
||||
this.stop({ name: name, logout: false })
|
||||
.then(() => {
|
||||
this.log.info(`Session '${name}' has been stopped.`);
|
||||
})
|
||||
.catch((err) => {
|
||||
this.log.error(
|
||||
`Error while stopping session '${name}' while logging out`,
|
||||
err,
|
||||
);
|
||||
});
|
||||
await this.sessionAuthRepository.clean(request.name);
|
||||
const sessions = { [this.DEFAULT]: this.session };
|
||||
return getProxyConfig(this.config, sessions, this.DEFAULT);
|
||||
}
|
||||
|
||||
getSession(name: string): WhatsappSession {
|
||||
this.onlyDefault(name);
|
||||
const session = this.session;
|
||||
if (!session) {
|
||||
if (session === undefined) {
|
||||
throw new NotFoundException(
|
||||
`We didn't find a session with name '${name}'. Please start it first by using POST /sessions/start request`,
|
||||
`We didn't find a session with name '${name}'.
|
||||
Please start it first by using POST /sessions/${name}/start request`,
|
||||
);
|
||||
}
|
||||
return session;
|
||||
}
|
||||
|
||||
async getSessions(all: boolean): Promise<SessionInfo[]> {
|
||||
if (!this.session) {
|
||||
if (!all) {
|
||||
return [];
|
||||
}
|
||||
if (this.session === null && all) {
|
||||
return [
|
||||
{
|
||||
name: this.DEFAULT,
|
||||
status: WAHASessionStatus.STOPPED,
|
||||
config: undefined,
|
||||
config: this.sessionConfig,
|
||||
me: null,
|
||||
},
|
||||
];
|
||||
}
|
||||
if (this.session === undefined && all) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const me = this.session.getSessionMeInfo();
|
||||
// Get engine info
|
||||
let engineInfo = {};
|
||||
@@ -251,7 +277,10 @@ export class SessionManagerCore extends SessionManager {
|
||||
try {
|
||||
engineInfo = await promiseTimeout(10, this.session.getEngineInfo());
|
||||
} catch (e) {
|
||||
this.log.warn('Error while getting engine info', e);
|
||||
this.log.warn(
|
||||
{ session: this.session },
|
||||
'Error while getting engine info',
|
||||
);
|
||||
}
|
||||
}
|
||||
const engine = {
|
||||
|
||||
@@ -1,47 +0,0 @@
|
||||
import { WAMedia } from '../structures/responses.dto';
|
||||
import {
|
||||
IEngineMediaProcessor,
|
||||
MediaManager,
|
||||
MediaStorage,
|
||||
} from './abc/media.abc';
|
||||
import { DOCS_URL } from './exceptions';
|
||||
|
||||
export class MediaStorageCore implements MediaStorage {
|
||||
async save(
|
||||
messageId: string,
|
||||
mimetype: string,
|
||||
buffer: Buffer,
|
||||
): Promise<string> {
|
||||
return Promise.resolve(
|
||||
`Media attachment's available only in WAHA Plus version. ${DOCS_URL}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export class CoreMediaManager implements MediaManager {
|
||||
constructor(
|
||||
private storage: MediaStorage,
|
||||
private mimetypes: string[],
|
||||
) {}
|
||||
|
||||
async processMedia<Message>(
|
||||
processor: IEngineMediaProcessor<Message>,
|
||||
message: Message,
|
||||
): Promise<Message> {
|
||||
if (!processor.hasMedia(message)) {
|
||||
return message;
|
||||
}
|
||||
const mimetype = '';
|
||||
const filename = processor.getFilename(message);
|
||||
const url = await this.storage.save('', mimetype, Buffer.from(''));
|
||||
const media: WAMedia = {
|
||||
mimetype: mimetype,
|
||||
filename: filename,
|
||||
url: url,
|
||||
};
|
||||
|
||||
// @ts-ignore
|
||||
message.media = media;
|
||||
return message;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
/**
|
||||
* Engine specific media processor
|
||||
* Knows how to extract necessary attributes and fetch the data from Message
|
||||
*/
|
||||
interface IMediaEngineProcessor<Message> {
|
||||
hasMedia(message: Message): boolean;
|
||||
|
||||
getFilename(message: Message): string | null;
|
||||
|
||||
getMimetype(message: Message): string;
|
||||
|
||||
getMessageId(message: Message): string;
|
||||
|
||||
getMediaBuffer(message: Message): Promise<Buffer | null>;
|
||||
}
|
||||
|
||||
export { IMediaEngineProcessor };
|
||||
@@ -0,0 +1,15 @@
|
||||
import { IMediaEngineProcessor } from '@waha/core/media/IMediaEngineProcessor';
|
||||
|
||||
/**
|
||||
* General interface for MediaManager - one that handles the logic
|
||||
* and manipulates MediaStorage and MediaEngineProcessor
|
||||
*/
|
||||
interface IMediaManager {
|
||||
processMedia<Message>(
|
||||
processor: IMediaEngineProcessor<Message>,
|
||||
message: Message,
|
||||
session: string,
|
||||
): Promise<Message>;
|
||||
}
|
||||
|
||||
export { IMediaManager };
|
||||
@@ -0,0 +1,42 @@
|
||||
import { S3MediaData } from '@waha/structures/media.s3.dto';
|
||||
|
||||
/**
|
||||
* Handles saving data to the physical storage
|
||||
*/
|
||||
interface Message {
|
||||
// false_111111111@c.us_AAAAAAAAAAAAAA
|
||||
id: string;
|
||||
}
|
||||
|
||||
interface File {
|
||||
extension: string;
|
||||
filename?: string;
|
||||
}
|
||||
|
||||
export interface MediaData {
|
||||
session: string;
|
||||
message: Message;
|
||||
file: File;
|
||||
}
|
||||
|
||||
/**
|
||||
* For stored media data
|
||||
*/
|
||||
export interface MediaStorageData {
|
||||
url: string;
|
||||
s3?: S3MediaData;
|
||||
}
|
||||
|
||||
abstract class IMediaStorage {
|
||||
abstract init(): Promise<void>;
|
||||
|
||||
abstract save(buffer: Buffer, data: MediaData): Promise<boolean>;
|
||||
|
||||
abstract exists(data: MediaData): Promise<boolean>;
|
||||
|
||||
abstract getStorageData(data: MediaData): Promise<MediaStorageData>;
|
||||
|
||||
abstract purge(): Promise<void>;
|
||||
}
|
||||
|
||||
export { IMediaStorage };
|
||||
@@ -0,0 +1,44 @@
|
||||
import { IMediaEngineProcessor } from '@waha/core/media/IMediaEngineProcessor';
|
||||
import { IMediaStorage, MediaData } from '@waha/core/media/IMediaStorage';
|
||||
import { WAMedia } from '@waha/structures/media.dto';
|
||||
|
||||
import { IMediaManager } from './IMediaManager';
|
||||
|
||||
export class MediaManagerCore implements IMediaManager {
|
||||
constructor(
|
||||
private storage: IMediaStorage,
|
||||
private mimetypes: string[],
|
||||
) {}
|
||||
|
||||
async processMedia<Message>(
|
||||
processor: IMediaEngineProcessor<Message>,
|
||||
message: Message,
|
||||
session: string,
|
||||
): Promise<Message> {
|
||||
if (!processor.hasMedia(message)) {
|
||||
return message;
|
||||
}
|
||||
const mimetype = '';
|
||||
const mediaData: MediaData = {
|
||||
session: session,
|
||||
message: {
|
||||
id: '',
|
||||
},
|
||||
file: {
|
||||
extension: '',
|
||||
},
|
||||
};
|
||||
const { url } = await this.storage.getStorageData(mediaData);
|
||||
|
||||
const filename = processor.getFilename(message);
|
||||
const media: WAMedia = {
|
||||
mimetype: mimetype,
|
||||
filename: filename,
|
||||
url: url,
|
||||
};
|
||||
|
||||
// @ts-ignore
|
||||
message.media = media;
|
||||
return message;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
import { DOCS_URL } from '@waha/core/exceptions';
|
||||
import { IMediaStorage, MediaData } from '@waha/core/media/IMediaStorage';
|
||||
|
||||
export class MediaNoopStorage implements IMediaStorage {
|
||||
async init() {
|
||||
return;
|
||||
}
|
||||
|
||||
async save(buffer: Buffer, data: MediaData): Promise<boolean> {
|
||||
return Promise.resolve(true);
|
||||
}
|
||||
|
||||
async exists(data: MediaData): Promise<boolean> {
|
||||
return false;
|
||||
}
|
||||
|
||||
async getStorageData(data: MediaData) {
|
||||
const url = `Media attachment's available only in WAHA Plus version. ${DOCS_URL}`;
|
||||
return { url };
|
||||
}
|
||||
|
||||
async purge() {
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { WhatsappConfigService } from '@waha/config.service';
|
||||
|
||||
@Injectable()
|
||||
export class MediaLocalStorageConfig {
|
||||
public filesUri = '/api/files';
|
||||
|
||||
constructor(private config: WhatsappConfigService) {}
|
||||
|
||||
get filesURL(): string {
|
||||
return `${this.config.schema}://${this.config.hostname}:${this.config.port}${this.filesUri}/`;
|
||||
}
|
||||
|
||||
get filesFolder(): string {
|
||||
return this.config.get('WHATSAPP_FILES_FOLDER', '/tmp/whatsapp-files');
|
||||
}
|
||||
|
||||
get filesLifetime(): number {
|
||||
return parseInt(this.config.get('WHATSAPP_FILES_LIFETIME', '180'));
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,4 @@ export abstract class ISessionAuthRepository {
|
||||
abstract init(sessionName?: string): Promise<void>;
|
||||
|
||||
abstract clean(sessionName: string): Promise<void>;
|
||||
|
||||
abstract getAll(): Promise<string[]>;
|
||||
}
|
||||
@@ -8,5 +8,9 @@ export abstract class ISessionConfigRepository {
|
||||
|
||||
abstract get(sessionName: string): Promise<SessionConfig | null>;
|
||||
|
||||
abstract exists(sessionName: string): Promise<boolean>;
|
||||
|
||||
abstract delete(sessionName: string): Promise<void>;
|
||||
|
||||
abstract getAll(): Promise<string[]>;
|
||||
}
|
||||
@@ -1,7 +1,10 @@
|
||||
import * as fs from 'fs/promises';
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
const fs = require('fs-extra');
|
||||
|
||||
import { ISessionAuthRepository } from './ISessionAuthRepository';
|
||||
import { LocalStore } from './LocalStore';
|
||||
// Keep all waha related files, ".waha.session.*"
|
||||
const KEEP_FILES = /^\.waha\.session\..*$/;
|
||||
|
||||
export class LocalSessionAuthRepository extends ISessionAuthRepository {
|
||||
private store: LocalStore;
|
||||
@@ -16,17 +19,17 @@ export class LocalSessionAuthRepository extends ISessionAuthRepository {
|
||||
}
|
||||
|
||||
async clean(sessionName: string) {
|
||||
const folder = this.store.getSessionDirectory(sessionName);
|
||||
await fs.rm(folder, { recursive: true });
|
||||
}
|
||||
|
||||
async getAll(): Promise<string[]> {
|
||||
await this.init();
|
||||
const content = await fs.readdir(this.store.getEngineDirectory(), {
|
||||
withFileTypes: true,
|
||||
});
|
||||
return content
|
||||
.filter((dirent) => dirent.isDirectory())
|
||||
.map((dirent) => dirent.name);
|
||||
// Remove all files and directories recursively, but keep waha files
|
||||
const sessionDirectory = this.store.getSessionDirectory(sessionName);
|
||||
// Check it exists and it's directory
|
||||
const exists = await fs.pathExists(sessionDirectory);
|
||||
if (!exists) {
|
||||
return;
|
||||
}
|
||||
const files = await fs.readdir(sessionDirectory);
|
||||
const filesToRemove = files.filter((file) => !file.match(KEEP_FILES));
|
||||
for (const file of filesToRemove) {
|
||||
await fs.remove(`${sessionDirectory}/${file}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,7 @@
|
||||
import * as fs from 'fs/promises';
|
||||
import * as path from 'path';
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
const fs = require('fs-extra');
|
||||
|
||||
import { fileExists } from '@waha/utils/files';
|
||||
|
||||
import { SessionConfig } from '../../structures/sessions.dto';
|
||||
import { ISessionConfigRepository } from './ISessionConfigRepository';
|
||||
@@ -14,11 +16,13 @@ export class LocalSessionConfigRepository extends ISessionConfigRepository {
|
||||
this.store = store;
|
||||
}
|
||||
|
||||
private async fileExists(filepath: string) {
|
||||
try {
|
||||
await fs.access(filepath, fs.constants.F_OK);
|
||||
} catch (error) {
|
||||
return false;
|
||||
async exists(sessionName: string): Promise<boolean> {
|
||||
const filepath = this.getFilePath(sessionName);
|
||||
const exists = await fileExists(filepath);
|
||||
if (!exists) {
|
||||
// check directory exist for empty config sessions
|
||||
const folder = this.store.getSessionDirectory(sessionName);
|
||||
return await fileExists(folder);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
@@ -26,7 +30,7 @@ export class LocalSessionConfigRepository extends ISessionConfigRepository {
|
||||
async get(sessionName: string): Promise<SessionConfig | null> {
|
||||
const filepath = this.getFilePath(sessionName);
|
||||
// Check file exists
|
||||
if (!(await this.fileExists(filepath))) {
|
||||
if (!(await fileExists(filepath))) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -56,10 +60,17 @@ export class LocalSessionConfigRepository extends ISessionConfigRepository {
|
||||
}
|
||||
|
||||
async delete(sessionName: string): Promise<void> {
|
||||
const filepath = this.getFilePath(sessionName);
|
||||
if (!(await this.fileExists(filepath))) {
|
||||
return;
|
||||
}
|
||||
await fs.unlink(filepath);
|
||||
const sessionDirectory = this.store.getSessionDirectory(sessionName);
|
||||
await fs.remove(sessionDirectory);
|
||||
}
|
||||
|
||||
async getAll(): Promise<string[]> {
|
||||
await this.store.init();
|
||||
const content = await fs.readdir(this.store.getEngineDirectory(), {
|
||||
withFileTypes: true,
|
||||
});
|
||||
return content
|
||||
.filter((dirent) => dirent.isDirectory())
|
||||
.map((dirent) => dirent.name);
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,3 @@
|
||||
import { ConsoleLogger } from '@nestjs/common';
|
||||
|
||||
import { WAHAEvents } from '../structures/enums.dto';
|
||||
import { WebhookConfig } from '../structures/webhooks.config.dto';
|
||||
import { WAHAWebhook } from '../structures/webhooks.dto';
|
||||
@@ -137,6 +135,7 @@ export class WebhookConductorCore implements WebhookConductor {
|
||||
const json: WAHAWebhook = {
|
||||
event: event,
|
||||
session: session.name,
|
||||
metadata: session.sessionConfig?.metadata,
|
||||
me: me,
|
||||
payload: data,
|
||||
engine: session.engine,
|
||||
|
||||
@@ -13,11 +13,6 @@ function parseBool(value: any): boolean {
|
||||
return false;
|
||||
}
|
||||
|
||||
console.error(
|
||||
'parseBool got an unexpected value:',
|
||||
value,
|
||||
'(accepted values : "true", "false")',
|
||||
);
|
||||
throw new Error(
|
||||
'Error: parseBool got unexpected value - use "true" or "false" values',
|
||||
);
|
||||
|
||||
+21
-5
@@ -7,13 +7,14 @@ import {
|
||||
} from '@waha/utils/logging';
|
||||
import { json, urlencoded } from 'express';
|
||||
import { Logger as NestJSPinoLogger } from 'nestjs-pino';
|
||||
import { LoggerErrorInterceptor } 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 { AllExceptionsFilter } from './nestjs/AllExceptionsFilter';
|
||||
import { WAHA_WEBHOOKS } from './structures/webhooks.dto';
|
||||
import { getWAHAVersion, VERSION, WAHAVersion } from './version';
|
||||
|
||||
@@ -22,7 +23,7 @@ const logger: Logger = pino({
|
||||
transport: getPinoTransport(),
|
||||
}).child({ name: 'Bootstrap' });
|
||||
|
||||
logger.info('NODE - Catching unhandled exceptions enabled');
|
||||
logger.info('NODE - Catching unhandled rejection enabled');
|
||||
process.on('unhandledRejection', (reason, promise) => {
|
||||
logger.error('Unhandled Rejection at:', promise, 'reason:', reason);
|
||||
// @ts-ignore
|
||||
@@ -33,7 +34,6 @@ async function loadModules(): Promise<
|
||||
[typeof AppModuleCore, typeof SwaggerConfiguratorCore]
|
||||
> {
|
||||
const version = getWAHAVersion();
|
||||
logger.info(`WAHA (WhatsApp HTTP API) - Running ${version} version...`);
|
||||
|
||||
if (version === WAHAVersion.CORE) {
|
||||
const { AppModuleCore } = await import('./core/app.module.core');
|
||||
@@ -51,15 +51,28 @@ async function loadModules(): Promise<
|
||||
return [AppModulePlus, SwaggerConfiguratorPlus];
|
||||
}
|
||||
|
||||
let app = undefined;
|
||||
|
||||
export function getApp() {
|
||||
return app;
|
||||
}
|
||||
|
||||
async function bootstrap() {
|
||||
const version = getWAHAVersion();
|
||||
logger.info(`WAHA (WhatsApp HTTP API) - Running ${version} version...`);
|
||||
const [AppModule, SwaggerModule] = await loadModules();
|
||||
const httpsOptions = AppModule.getHttpsOptions(logger);
|
||||
const app = await NestFactory.create(AppModule, {
|
||||
app = await NestFactory.create(AppModule, {
|
||||
logger: getNestJSLogLevels(),
|
||||
httpsOptions: httpsOptions,
|
||||
bufferLogs: true,
|
||||
});
|
||||
app.useLogger(app.get(NestJSPinoLogger));
|
||||
|
||||
// Print original stack, not pino one
|
||||
// https://github.com/iamolegga/nestjs-pino?tab=readme-ov-file#expose-stack-trace-and-error-class-in-err-property
|
||||
app.useGlobalInterceptors(new LoggerErrorInterceptor());
|
||||
|
||||
app.enableShutdownHooks();
|
||||
app.useGlobalFilters(new AllExceptionsFilter());
|
||||
app.enableCors();
|
||||
@@ -83,4 +96,7 @@ async function bootstrap() {
|
||||
logger.info(VERSION, 'Environment');
|
||||
}
|
||||
|
||||
bootstrap();
|
||||
bootstrap().catch((error) => {
|
||||
logger.error(error, `Failed to start WAHA: ${error}`);
|
||||
process.exit(1);
|
||||
});
|
||||
File renamed without changes.
@@ -0,0 +1,37 @@
|
||||
import { applyDecorators } from '@nestjs/common';
|
||||
import { ApiExtraModels, ApiResponse, getSchemaPath } from '@nestjs/swagger';
|
||||
import { Base64File } from '@waha/structures/files.dto';
|
||||
|
||||
function getRefSchemaPaths(models) {
|
||||
return models.map((model) => {
|
||||
return { $ref: getSchemaPath(model) };
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Decorator to add a file accept header to the swagger documentation
|
||||
*/
|
||||
export function ApiFileAcceptHeader(...models) {
|
||||
models = models.length ? models : [Base64File];
|
||||
return applyDecorators(
|
||||
// Add extra models, otherwise it'll give a error
|
||||
// $ref not found
|
||||
ApiExtraModels(...models),
|
||||
ApiResponse({
|
||||
status: 200,
|
||||
content: {
|
||||
'image/png': {
|
||||
schema: {
|
||||
type: 'string',
|
||||
format: 'binary',
|
||||
},
|
||||
},
|
||||
'application/json': {
|
||||
schema: {
|
||||
oneOf: getRefSchemaPaths(models),
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
);
|
||||
}
|
||||
File renamed without changes.
File renamed without changes.
@@ -0,0 +1,17 @@
|
||||
import { ApiParam } from '@nestjs/swagger';
|
||||
|
||||
export const ChatIdApiParam = ApiParam({
|
||||
name: 'chatId',
|
||||
required: true,
|
||||
type: 'string',
|
||||
description: 'Chat ID',
|
||||
example: '123456789@c.us',
|
||||
});
|
||||
|
||||
export const GroupIdApiParam = ApiParam({
|
||||
name: 'id',
|
||||
required: true,
|
||||
type: 'string',
|
||||
description: 'Group ID',
|
||||
example: '123123123@g.us',
|
||||
});
|
||||
@@ -0,0 +1,9 @@
|
||||
import { ApiParam } from '@nestjs/swagger';
|
||||
|
||||
export const MessageIdApiParam = ApiParam({
|
||||
name: 'messageId',
|
||||
required: true,
|
||||
type: 'string',
|
||||
description: 'Message ID',
|
||||
example: 'true_123456789@c.us_BAE6A33293978B16',
|
||||
});
|
||||
@@ -0,0 +1,20 @@
|
||||
import { Param } from '@nestjs/common';
|
||||
import { ApiParam } from '@nestjs/swagger';
|
||||
import { SessionPipe } from '@waha/nestjs/pipes/SessionPipe';
|
||||
|
||||
/**
|
||||
* Decorator for a method that uses SessionPipe above
|
||||
*/
|
||||
export const SessionApiParam = ApiParam({
|
||||
name: 'session',
|
||||
required: true,
|
||||
type: 'string',
|
||||
schema: {
|
||||
default: 'default',
|
||||
},
|
||||
description: 'Session name',
|
||||
});
|
||||
/**
|
||||
* Session param
|
||||
*/
|
||||
export const SessionParam = Param('session', SessionPipe);
|
||||
@@ -0,0 +1,17 @@
|
||||
import { Injectable, PipeTransform } from '@nestjs/common';
|
||||
import { SessionManager } from '@waha/core/abc/manager.abc';
|
||||
import { WhatsappSession } from '@waha/core/abc/session.abc';
|
||||
|
||||
/**
|
||||
* Get session name and return Whatsapp session back (if exists)
|
||||
* use it as
|
||||
@Param('session', SessionPipe) session: WhatsappSession,
|
||||
*/
|
||||
@Injectable()
|
||||
export class SessionPipe implements PipeTransform<WhatsappSession> {
|
||||
constructor(private manager: SessionManager) {}
|
||||
|
||||
async transform(value: any) {
|
||||
return this.manager.getSession(value);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
import { ValidationPipe, ValidationPipeOptions } from '@nestjs/common';
|
||||
import { parseBool } from '@waha/helpers';
|
||||
|
||||
// So we can change it to True during development and testing
|
||||
const WAHA_HTTP_STRICT_MODE = parseBool(process.env.WAHA_HTTP_STRICT_MODE);
|
||||
|
||||
export class WAHAValidationPipe extends ValidationPipe {
|
||||
constructor(options?: ValidationPipeOptions) {
|
||||
options = options || {};
|
||||
options.transform = true;
|
||||
options.whitelist = true;
|
||||
options.forbidNonWhitelisted = WAHA_HTTP_STRICT_MODE;
|
||||
super(options);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
/**
|
||||
* Convert string to boolean
|
||||
* @param value
|
||||
* @constructor
|
||||
*/
|
||||
export function BooleanString({ value }: { value: any }) {
|
||||
switch (value) {
|
||||
case 'true':
|
||||
return true;
|
||||
case 'True':
|
||||
return true;
|
||||
case '1':
|
||||
return true;
|
||||
case 'false':
|
||||
return false;
|
||||
case 'False':
|
||||
return false;
|
||||
case '0':
|
||||
return false;
|
||||
default:
|
||||
return value;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
import {
|
||||
registerDecorator,
|
||||
ValidationArguments,
|
||||
ValidationOptions,
|
||||
} from 'class-validator';
|
||||
|
||||
export function IsDynamicObject(validationOptions?: ValidationOptions) {
|
||||
return function (object: Record<string, any>, propertyName: string) {
|
||||
registerDecorator({
|
||||
name: 'IsDynamicObject',
|
||||
target: object.constructor,
|
||||
propertyName: propertyName,
|
||||
constraints: [],
|
||||
options: validationOptions,
|
||||
validator: {
|
||||
validate(value: any, args: ValidationArguments) {
|
||||
if (!value || typeof value !== 'object') {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (const key in value) {
|
||||
if (typeof key !== 'string' || typeof value[key] !== 'string') {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
},
|
||||
defaultMessage(validationArguments?: ValidationArguments): string {
|
||||
return `${validationArguments.property} accepts string key-value pairs only`;
|
||||
},
|
||||
},
|
||||
});
|
||||
};
|
||||
}
|
||||
@@ -1,8 +1,8 @@
|
||||
import { LoggerService } from '@nestjs/common';
|
||||
import { WebSocket } from '@waha/utils/ws';
|
||||
import { WebSocket } from '@waha/nestjs/ws/ws';
|
||||
import { WebSocketServer } from 'ws';
|
||||
|
||||
export class HeartbeatJob {
|
||||
export class WebsocketHeartbeatJob {
|
||||
private interval: ReturnType<typeof setInterval>;
|
||||
|
||||
constructor(
|
||||
File renamed without changes.
@@ -4,6 +4,7 @@ export enum QRCodeFormat {
|
||||
IMAGE = 'image',
|
||||
RAW = 'raw',
|
||||
}
|
||||
|
||||
export class QRCodeQuery {
|
||||
format: QRCodeFormat = QRCodeFormat.IMAGE;
|
||||
}
|
||||
|
||||
@@ -15,5 +15,5 @@ export class SessionQuery {
|
||||
*/
|
||||
|
||||
export class SessionBaseRequest {
|
||||
session = WHATSAPP_DEFAULT_SESSION_NAME;
|
||||
session: string = WHATSAPP_DEFAULT_SESSION_NAME;
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
/**
|
||||
* Events
|
||||
*/
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { ChatIdProperty } from '@waha/structures/properties.dto';
|
||||
|
||||
function CallIdProperty() {
|
||||
return ApiProperty({
|
||||
description: 'Call ID',
|
||||
example: 'ABCDEFGABCDEFGABCDEFGABCDEFG',
|
||||
});
|
||||
}
|
||||
|
||||
export class CallData {
|
||||
@CallIdProperty()
|
||||
id: string;
|
||||
|
||||
@ChatIdProperty()
|
||||
from?: string;
|
||||
|
||||
timestamp: number;
|
||||
|
||||
isVideo: boolean;
|
||||
|
||||
isGroup: boolean;
|
||||
}
|
||||
@@ -45,6 +45,10 @@ export class Channel {
|
||||
})
|
||||
id: string;
|
||||
|
||||
@ApiProperty({
|
||||
description: 'Channel name',
|
||||
example: 'Channel Name',
|
||||
})
|
||||
name: string;
|
||||
|
||||
description?: string;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { ChatIdProperty } from '@waha/structures/properties.dto';
|
||||
import { Type } from 'class-transformer';
|
||||
import { IsNumber, IsOptional } from 'class-validator';
|
||||
|
||||
@@ -32,5 +33,14 @@ export class GetChatsQuery {
|
||||
}
|
||||
|
||||
/**
|
||||
* Requests
|
||||
* Events
|
||||
*/
|
||||
|
||||
export class ChatArchiveEvent {
|
||||
@ChatIdProperty()
|
||||
id: string;
|
||||
|
||||
archived: boolean;
|
||||
|
||||
timestamp: number;
|
||||
}
|
||||
@@ -15,12 +15,16 @@ import {
|
||||
VoiceBinaryFile,
|
||||
VoiceRemoteFile,
|
||||
} from './files.dto';
|
||||
import { ChatIdProperty } from './properties.dto';
|
||||
import { ChatIdProperty, ReplyToProperty } from './properties.dto';
|
||||
|
||||
/**
|
||||
* Queries
|
||||
*/
|
||||
export class CheckNumberStatusQuery extends SessionQuery {
|
||||
@ApiProperty({
|
||||
description: 'The phone number to check',
|
||||
example: '1213213213',
|
||||
})
|
||||
@IsString()
|
||||
phone: string;
|
||||
}
|
||||
@@ -72,6 +76,7 @@ export class SendSeenRequest extends ChatRequest {
|
||||
@ApiProperty({
|
||||
example: '11111111111@c.us',
|
||||
required: false,
|
||||
default: null,
|
||||
description:
|
||||
'NOWEB engine only - the ID of the user that sent the message (undefined for individual chats)',
|
||||
})
|
||||
@@ -141,30 +146,44 @@ export class MessageContactVcardRequest extends ChatRequest {
|
||||
}
|
||||
|
||||
export class MessageTextRequest extends ChatRequest {
|
||||
text = 'Hi there!';
|
||||
text: string = 'Hi there!';
|
||||
|
||||
@ApiHideProperty()
|
||||
mentions?: string[];
|
||||
|
||||
@ReplyToProperty()
|
||||
reply_to?: string;
|
||||
}
|
||||
|
||||
export class EditMessageRequest {
|
||||
text = 'Hello, world!';
|
||||
text: string = 'Hello, world!';
|
||||
|
||||
@ApiHideProperty()
|
||||
mentions?: string[];
|
||||
}
|
||||
|
||||
export class MessageReplyRequest extends MessageTextRequest {
|
||||
text = 'Reply text';
|
||||
@ApiProperty({
|
||||
example: 'false_11111111111@c.us_AAAAAAAAAAAAAAAAAAAA',
|
||||
})
|
||||
reply_to: string;
|
||||
text: string = 'Reply text';
|
||||
}
|
||||
|
||||
export class MessageLocationRequest extends ChatRequest {
|
||||
@ApiProperty({
|
||||
example: 38.8937255,
|
||||
})
|
||||
latitude: number;
|
||||
|
||||
@ApiProperty({
|
||||
example: -77.0969763,
|
||||
})
|
||||
longitude: number;
|
||||
|
||||
@ApiProperty({
|
||||
example: 'Our office',
|
||||
})
|
||||
title: string;
|
||||
|
||||
@ReplyToProperty()
|
||||
reply_to?: string;
|
||||
}
|
||||
|
||||
@ApiExtraModels(BinaryFile, RemoteFile)
|
||||
@@ -179,11 +198,17 @@ class FileRequest extends ChatRequest {
|
||||
}
|
||||
|
||||
export class MessageImageRequest extends FileRequest {
|
||||
caption: string;
|
||||
caption?: string;
|
||||
|
||||
@ReplyToProperty()
|
||||
reply_to?: string;
|
||||
}
|
||||
|
||||
export class MessageFileRequest extends FileRequest {
|
||||
caption: string;
|
||||
caption?: string;
|
||||
|
||||
@ReplyToProperty()
|
||||
reply_to?: string;
|
||||
}
|
||||
|
||||
@ApiExtraModels(VoiceBinaryFile, VoiceRemoteFile)
|
||||
@@ -195,6 +220,9 @@ export class MessageVoiceRequest extends ChatRequest {
|
||||
],
|
||||
})
|
||||
file: VoiceBinaryFile | VoiceRemoteFile;
|
||||
|
||||
@ReplyToProperty()
|
||||
reply_to?: string;
|
||||
}
|
||||
|
||||
@ApiExtraModels(VideoRemoteFile, VideoBinaryFile)
|
||||
@@ -207,7 +235,14 @@ export class MessageVideoRequest extends ChatRequest {
|
||||
})
|
||||
file: VideoRemoteFile | VideoBinaryFile;
|
||||
|
||||
caption: string = 'Just watch at this!';
|
||||
caption?: string = 'Just watch at this!';
|
||||
|
||||
@ApiProperty({
|
||||
description:
|
||||
'The ID of the message to reply to - false_11111111111@c.us_AAAAAAAAAAAAAAAAAAAA',
|
||||
example: null,
|
||||
})
|
||||
reply_to?: string;
|
||||
}
|
||||
|
||||
export class MessageLinkPreviewRequest extends ChatRequest {
|
||||
@@ -256,6 +291,13 @@ export class MessagePoll {
|
||||
|
||||
export class MessagePollRequest extends ChatRequest {
|
||||
poll: MessagePoll;
|
||||
|
||||
@ApiProperty({
|
||||
description:
|
||||
'The ID of the message to reply to - false_11111111111@c.us_AAAAAAAAAAAAAAAAAAAA',
|
||||
example: null,
|
||||
})
|
||||
reply_to?: string;
|
||||
}
|
||||
|
||||
export class MessageDestination {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { ChatIdProperty } from '@waha/structures/properties.dto';
|
||||
import { IsString } from 'class-validator';
|
||||
|
||||
import { SessionBaseRequest, SessionQuery } from './base.dto';
|
||||
@@ -7,6 +8,7 @@ import { SessionBaseRequest, SessionQuery } from './base.dto';
|
||||
*/
|
||||
|
||||
export class ContactQuery extends SessionQuery {
|
||||
@ChatIdProperty()
|
||||
@IsString()
|
||||
contactId: string;
|
||||
}
|
||||
@@ -16,6 +18,7 @@ export class ContactQuery extends SessionQuery {
|
||||
*/
|
||||
|
||||
export class ContactRequest extends SessionBaseRequest {
|
||||
@ChatIdProperty()
|
||||
@IsString()
|
||||
contactId: string;
|
||||
}
|
||||
@@ -13,6 +13,14 @@ export enum WAHAEvents {
|
||||
PRESENCE_UPDATE = 'presence.update',
|
||||
POLL_VOTE = 'poll.vote',
|
||||
POLL_VOTE_FAILED = 'poll.vote.failed',
|
||||
CHAT_ARCHIVE = 'chat.archive',
|
||||
CALL_RECEIVED = 'call.received',
|
||||
CALL_ACCEPTED = 'call.accepted',
|
||||
CALL_REJECTED = 'call.rejected',
|
||||
LABEL_UPSERT = 'label.upsert',
|
||||
LABEL_DELETED = 'label.deleted',
|
||||
LABEL_CHAT_ADDED = 'label.chat.added',
|
||||
LABEL_CHAT_DELETED = 'label.chat.deleted',
|
||||
}
|
||||
|
||||
export enum WAHASessionStatus {
|
||||
|
||||
@@ -2,7 +2,7 @@ import { ApiProperty } from '@nestjs/swagger';
|
||||
|
||||
export class WAHAEnvironment {
|
||||
@ApiProperty({
|
||||
example: '2029.10.29',
|
||||
example: 'YYYY.MM.BUILD',
|
||||
})
|
||||
version: string;
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { IsArray, IsString } from 'class-validator';
|
||||
|
||||
/**
|
||||
@@ -5,6 +6,9 @@ import { IsArray, IsString } from 'class-validator';
|
||||
*/
|
||||
export class Participant {
|
||||
@IsString()
|
||||
@ApiProperty({
|
||||
example: '123456789@c.us',
|
||||
})
|
||||
id: string;
|
||||
}
|
||||
|
||||
@@ -29,6 +33,7 @@ export class DescriptionRequest {
|
||||
@IsString()
|
||||
description: string;
|
||||
}
|
||||
|
||||
export class SubjectRequest {
|
||||
@IsString()
|
||||
subject: string;
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { ChatIdProperty } from '@waha/structures/properties.dto';
|
||||
|
||||
const Colors = [
|
||||
'#ff9485',
|
||||
'#64c4ff',
|
||||
'#ffd429',
|
||||
'#dfaef0',
|
||||
'#99b6c1',
|
||||
'#55ccb3',
|
||||
'#ff9dff',
|
||||
'#d3a91d',
|
||||
'#6d7cce',
|
||||
'#d7e752',
|
||||
'#00d0e2',
|
||||
'#ffc5c7',
|
||||
'#93ceac',
|
||||
'#f74848',
|
||||
'#00a0f2',
|
||||
'#83e422',
|
||||
'#ffaf04',
|
||||
'#b5ebff',
|
||||
'#9ba6ff',
|
||||
'#9368cf',
|
||||
];
|
||||
|
||||
export class Label {
|
||||
@ApiProperty({
|
||||
example: '1',
|
||||
description: 'Label ID',
|
||||
})
|
||||
id: string;
|
||||
|
||||
@ApiProperty({
|
||||
example: 'Lead',
|
||||
description: 'Label name',
|
||||
})
|
||||
name: string;
|
||||
|
||||
@ApiProperty({
|
||||
example: 0,
|
||||
description: 'Internal color number, not hex',
|
||||
})
|
||||
color: number;
|
||||
|
||||
@ApiProperty({
|
||||
example: '#ff9485',
|
||||
description: 'Color in hex',
|
||||
})
|
||||
colorHex: string;
|
||||
|
||||
static toHex(color: number) {
|
||||
if (color >= Colors.length) {
|
||||
return '#000000';
|
||||
}
|
||||
return Colors[color];
|
||||
}
|
||||
}
|
||||
|
||||
export class LabelID {
|
||||
@ApiProperty({
|
||||
example: '1',
|
||||
description: 'Label ID',
|
||||
})
|
||||
id: string;
|
||||
}
|
||||
|
||||
export class SetLabelsRequest {
|
||||
labels: LabelID[];
|
||||
}
|
||||
|
||||
export class LabelChatAssociation {
|
||||
@ApiProperty({
|
||||
example: '1',
|
||||
description: 'Label ID',
|
||||
})
|
||||
labelId: string;
|
||||
|
||||
label: Label | null;
|
||||
|
||||
@ChatIdProperty({
|
||||
description: 'Chat ID',
|
||||
})
|
||||
chatId: string;
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { S3MediaData } from '@waha/structures/media.s3.dto';
|
||||
|
||||
export class WAMedia {
|
||||
@ApiProperty({
|
||||
description: 'The URL for the media in the message if any',
|
||||
example:
|
||||
'http://localhost:3000/api/files/false_11111111111@c.us_AAAAAAAAAAAAAAAAAAAA.oga',
|
||||
})
|
||||
url?: string;
|
||||
|
||||
@ApiProperty({
|
||||
description: 'mimetype for the media in the message if any',
|
||||
example: 'audio/jpeg',
|
||||
})
|
||||
mimetype?: string;
|
||||
|
||||
@ApiProperty({
|
||||
description: 'The original filename in mediaUrl in the message if any',
|
||||
example: 'example.pdf',
|
||||
})
|
||||
filename?: string;
|
||||
|
||||
@ApiProperty({
|
||||
description:
|
||||
'S3 attributes for the media in the message ' +
|
||||
'if you are using S3 media storage',
|
||||
})
|
||||
s3?: S3MediaData;
|
||||
|
||||
@ApiProperty({
|
||||
description: "Error message if there's an error downloading the media",
|
||||
example: null,
|
||||
})
|
||||
// eslint-disable-next-line @typescript-eslint/ban-types
|
||||
error?: object;
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
|
||||
export class S3MediaData {
|
||||
@ApiProperty({
|
||||
description: 'The name of the S3 bucket',
|
||||
example: 'my-bucket',
|
||||
})
|
||||
Bucket: string;
|
||||
|
||||
@ApiProperty({
|
||||
description: 'The key of the object in the S3 bucket',
|
||||
example: 'default/false_11111111111@c.us_AAAAAAAAAAAAAAAAAAAA.oga',
|
||||
})
|
||||
Key: string;
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import {
|
||||
ChatIdProperty,
|
||||
MessageIdOnlyProperty,
|
||||
} from '@waha/structures/properties.dto';
|
||||
|
||||
export class ReplyToMessage {
|
||||
@MessageIdOnlyProperty()
|
||||
id: string;
|
||||
|
||||
@ChatIdProperty()
|
||||
participant?: string;
|
||||
|
||||
@ApiProperty({
|
||||
example: 'Hello!',
|
||||
})
|
||||
body?: string;
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
export class PingResponse {
|
||||
message: string = 'pong';
|
||||
}
|
||||
Loaded 100 of 116 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user