Compare commits

...
69 Commits
Author SHA1 Message Date
allburov 2f6ed2fb58 [core] Add chats endpoints
Release / deploy-docker (push) Waiting to run
2023-07-16 16:14:12 +07:00
allburov 4eb23d2c09 [core] Bump version to 2023.7.16 2023-07-16 16:14:08 +07:00
allburov ec3de8dbcf [core] Init folder for a session 2023-07-16 13:30:40 +07:00
allburov f6f09747dc [core] pin node:20-bullseye
Release / deploy-docker (push) Waiting to run
2023-07-14 10:10:28 +07:00
allburov 27a534a605 [core] add set session presence endpoint 2023-07-12 16:46:15 +07:00
allburov 3cbd940843 [core] Add swagger image at the top of quick start 2023-07-07 14:26:23 +07:00
allburov e728eec01a [core] Move tiers
Release / deploy-docker (push) Waiting to run
2023-07-05 10:29:10 +07:00
allburov d06d2b78a8 [core] Move donations to top, add links 2023-07-05 10:27:32 +07:00
allburov 5878a9196e [core] Pin yarn version to 3.6.1 2023-07-03 21:52:32 +07:00
allburov 85dd061f96 [core] ignore .yaml folder 2023-07-03 21:52:29 +07:00
allburov 8381c3e946 [core] Set retention days 5 2023-07-03 11:01:51 +07:00
allburov 6ff8a9705d [core] Add cleanup github script 2023-07-03 10:59:56 +07:00
allburov fb5819b966 [core] Create folder if there's no yet
Release / deploy-docker (push) Waiting to run
2023-06-29 19:05:26 +07:00
allburov 5cc1af3f82 [core] Bump to 2023.7.1 2023-06-29 18:56:12 +07:00
allburov c279de694c [core] Update NOWEB engine 2023-06-29 18:55:20 +07:00
allburov f7a1787f1f [core] Add command to update NOWEB 2023-06-29 18:55:20 +07:00
allburov 5442fd6e93 [core] Update WEBJS engine 2023-06-29 18:55:20 +07:00
allburov b8c8ca7653 [core] Use yarn v2 commands to update package 2023-06-29 18:55:20 +07:00
allburov 5ca7dbc84b [core] Update swagger 2023-06-29 18:38:41 +07:00
allburov d836c9706f [core] Add for-swagger make command 2023-06-29 18:38:34 +07:00
allburov 318be43a4f [core] fix start\stop issue with NOWEB engine 2023-06-29 18:31:36 +07:00
allburov 95bebce731 [core] fix sessions with no config 2023-06-29 18:31:34 +07:00
allburov 27b8c9d058 [core] Make config optional 2023-06-29 18:14:23 +07:00
allburov 10d4d24526 [core] Place Boosty first 2023-06-29 18:07:14 +07:00
allburov b19917bd3d [core] Always logout from session 2023-06-29 18:06:07 +07:00
allburov 4b6774b1db [core] WAHA Plus links last 2023-06-26 11:29:31 +07:00
allburov 360e0a7951 [core] All tiers 2023-06-26 11:28:01 +07:00
allburov 171f516eee [core] Patreon first 2023-06-26 11:17:58 +07:00
allburov 01943f6970 [core] Remove global env.varilabels from webhooks 2023-06-25 14:01:22 +07:00
allburov 953d48ee1f [core] Add links to Quick Start 2023-06-25 14:00:51 +07:00
allburov 2b6028bcb1 [core] Make alert dismissible 2023-06-25 13:51:36 +07:00
allburov 79b7b7d2a2 [core] Adjust headers 2023-06-25 13:34:11 +07:00
allburov 93a6a7b87f [core] Remove youtube video from other pages 2023-06-25 13:14:30 +07:00
allburov 3b499d6af5 [core] Change headers to mobile first 2023-06-23 15:04:22 +07:00
allburov 6481919597 [core] Change swagger title to WAHA 2023-06-23 14:57:17 +07:00
allburov 228f6ec8c5 [core] No one-time 2023-06-23 14:48:03 +07:00
allburov 6723f201b7 [core] Add make start-proxy command 2023-06-23 14:45:22 +07:00
allburov c250bb49f0 [core] Make proxy per session 2023-06-23 14:44:14 +07:00
allburov 3b607662e5 [core] Flexible webhooks 2023-06-22 18:20:27 +07:00
allburov f2d7041cb0 [core] Fix Dockerfile with yarn v2 2023-06-19 13:10:43 +07:00
allburov afc06794c2 [core] No mobile yet 2023-06-19 12:16:52 +07:00
allburov 33eaafcff1 [core] update NOWEB engine, use yarn v2 2023-06-18 17:38:06 +07:00
allburov e5a8743b9a [core] Remove Get on links 2023-06-18 15:17:44 +07:00
allburov 3322c4e2a5 [core] Add eslint fix import plugin 2023-06-16 16:17:31 +07:00
allburov aa39aaf489 [core] You can run, not configure 2023-06-16 15:01:30 +07:00
allburov a238581f44 [core] Add presence event 2023-06-16 15:00:44 +07:00
allburov 00814ef64e [core] Add dedicated webhooks page 2023-06-16 14:56:35 +07:00
allburov 6362d2966b [core] Add active link to docker-compose.yaml file 2023-06-14 12:36:44 +07:00
allburov 4e181a82b6 [core] Use plus image in sessions 2023-06-14 12:35:45 +07:00
allburov 374199905d [core][noweb] Add presence endpoints and webhook 2023-06-14 12:27:39 +07:00
allburov 642079ab1f [core] Add changelog about mentions 2023-06-12 15:31:35 +07:00
allburov 215c9d96b9 [core] Add mentions
Fix #105
2023-06-12 15:27:53 +07:00
allburov 5369eda705 [core] Fix engine field 2023-06-09 16:25:31 +07:00
allburov 5e42bbe1eb [core] ignore webjs cache 2023-06-09 16:25:26 +07:00
allburov 79dd2ee03e [core] bump version to 2023.6.9 2023-06-09 16:25:24 +07:00
allburov 78d3eae44b [core] Fix storage issue 2023-06-09 16:25:22 +07:00
allburovandPierluca Lino a9ee68fe1a [core] Add proxy support
Co-authored-by: Pierluca Lino <pierluca.lino@dontouch.ch>

Co-authored-by: puntolino <pierluca.lino@dontouch.ch>
2023-06-07 17:02:18 +07:00
allburov ee5678ccfb [core] yarn upgrade instead of add 2023-06-07 17:02:18 +07:00
allburov 248a675c1a [core] No core images 2023-06-05 17:14:21 +07:00
allburov 6b1d84cf38 [core] Show core features first 2023-06-05 16:59:52 +07:00
allburov c0977538ff [core] change header 2023-06-03 18:51:23 +07:00
allburov c8727e8b2f [core] Add a link to PLUS 2023-06-03 18:49:01 +07:00
allburov 55a41e19f4 [core] Rename to WAHA Plus 2023-06-03 18:47:09 +07:00
allburov 6096c0b158 [core] Move help's files to overview 2023-06-03 18:44:35 +07:00
allburov 161c6dfd95 [core] Receive message 2023-06-03 18:39:16 +07:00
allburov 078bcf7652 [core] env variables block 2023-06-03 18:33:47 +07:00
allburov 57c6df3970 [core] Adjust sessions how to 2023-06-03 18:27:58 +07:00
allburov 1130ee1b21 [core] Add docker run on index 2023-06-01 13:15:02 +07:00
allburov c9c0a29740 Use 2023 year in changelog 2023-06-01 12:10:37 +07:00
74 changed files with 15690 additions and 9932 deletions

No files matched your search

+1
View File
@@ -11,6 +11,7 @@ examples
# compiled output
/dist
/node_modules
.yarn
# Logs
logs
+31 -25
View File
@@ -1,27 +1,33 @@
module.exports = {
parser: '@typescript-eslint/parser',
parserOptions: {
project: 'tsconfig.json',
sourceType: 'module',
},
plugins: ['@typescript-eslint/eslint-plugin'],
extends: [
'plugin:@typescript-eslint/eslint-recommended',
'plugin:@typescript-eslint/recommended',
'prettier',
'prettier/@typescript-eslint',
],
root: true,
env: {
node: true,
jest: true,
},
rules: {
'@typescript-eslint/interface-name-prefix': 'off',
'@typescript-eslint/explicit-function-return-type': 'off',
'@typescript-eslint/explicit-module-boundary-types': 'off',
'@typescript-eslint/no-explicit-any': 'off',
"@typescript-eslint/ban-ts-ignore": "off",
"@typescript-eslint/ban-ts-comment": "off"
},
parser: "@typescript-eslint/parser",
parserOptions: {
project: "tsconfig.json",
sourceType: "module"
},
plugins: [
"@typescript-eslint/eslint-plugin",
"simple-import-sort"
],
extends: [
"plugin:@typescript-eslint/eslint-recommended",
"plugin:@typescript-eslint/recommended",
"prettier",
"prettier/@typescript-eslint"
],
root: true,
env: {
node: true,
jest: true
},
rules: {
"simple-import-sort/imports": "error",
"simple-import-sort/exports": "error",
"@typescript-eslint/interface-name-prefix": "off",
"@typescript-eslint/explicit-function-return-type": "off",
"@typescript-eslint/explicit-module-boundary-types": "off",
"@typescript-eslint/no-inferrable-types": "off",
"@typescript-eslint/no-explicit-any": "off",
"@typescript-eslint/ban-ts-ignore": "off",
"@typescript-eslint/ban-ts-comment": "off",
}
};
+18
View File
@@ -0,0 +1,18 @@
# Idea from
# https://stackoverflow.com/a/65539398/6753144
#
OWNER=devlikeapro
REPO=whatsapp-http-api-plus
# list workflows
gh api -X GET /repos/$OWNER/$REPO/actions/workflows | jq '.workflows[] | .name,.id'
# copy the ID of the workflow you want to clear and set it
WORKFLOW_ID=40218610
# list runs
gh api -X GET /repos/$OWNER/$REPO/actions/workflows/$WORKFLOW_ID/runs --paginate | jq '.workflow_runs[] | .id'
# delete runs
gh api -X GET /repos/$OWNER/$REPO/actions/workflows/$WORKFLOW_ID/runs --paginate | jq '.workflow_runs[] | .id' | xargs -I{} gh api -X DELETE /repos/$OWNER/$REPO/actions/runs/{} --silent
+1
View File
@@ -31,6 +31,7 @@ jobs:
with:
path: docs/site/public
name: public
retention-days: 5
deploy-github-pages:
needs: build
+2
View File
@@ -1,9 +1,11 @@
.yarn
.*sessions
tokens
files
venv
.wwebjs_auth
test.js
.wwebjs_cache
# compiled output
/dist
+1 -1
View File
@@ -3,7 +3,7 @@ repos:
rev: 'v2.7.1'
hooks:
- id: prettier
types_or: [javascript, ts]
types_or: [ javascript, ts ]
files: |
(?x)^(
src/.*|
+3
View File
@@ -0,0 +1,3 @@
nodeLinker: node-modules
yarnPath: .yarn/releases/yarn-3.6.1.cjs
+6 -13
View File
@@ -1,33 +1,26 @@
#
# Build
#
FROM node:18 as build
FROM node:20-bullseye as build
ENV PUPPETEER_SKIP_DOWNLOAD=True
# Install https://github.com/WhiskeySockets/Baileys
WORKDIR /
RUN git clone https://github.com/WhiskeySockets/Baileys WhiskeySockets-Baileys && \
cd WhiskeySockets-Baileys && \
git checkout e3ac5f596f16b706420ed016a349fa75bd3a157f && \
yarn install && \
yarn build:all
# npm packages
WORKDIR /src
COPY package.json .
COPY yarn.lock .
RUN yarn set version 3.6.1
RUN yarn install
# App
WORKDIR /src
ADD . /src
RUN npm run build && find ./dist -name "*.d.ts" -delete
RUN yarn install
RUN yarn build && find ./dist -name "*.d.ts" -delete
#
# Final
#
FROM node:18 as release
FROM node:20-bullseye as release
ENV PUPPETEER_SKIP_DOWNLOAD=True
ARG USE_BROWSER=chromium
@@ -66,4 +59,4 @@ COPY --from=build /src/dist ./dist
# Run command, etc
EXPOSE 3000
CMD npm run start:prod
CMD yarn start:prod
+9 -2
View File
@@ -13,9 +13,16 @@ clean: stop
push:
docker push devlikeapro/whatsapp-http-api
for-swagger:
WHATSAPP_SWAGGER_CONFIG_ADVANCED=true npm run start
update-swagger:
wget http://localhost:3000/-json -O ./docs/site/static/swagger/openapi.json
update-dependencies:
. ${NVM_DIR}/nvm.sh && nvm exec yarn add whatsapp-web.js@https://github.com/pedroslopez/whatsapp-web.js
. ${NVM_DIR}/nvm.sh && nvm exec yarn add venom-bot
. ${NVM_DIR}/nvm.sh && nvm exec yarn up whatsapp-web.js@https://github.com/pedroslopez/whatsapp-web.js
. ${NVM_DIR}/nvm.sh && nvm exec yarn up @adiwajshing/baileys@github:WhiskeySockets/Baileys
. ${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
+1
View File
@@ -75,6 +75,7 @@
- Block/unblock contacts
- Get contact info
- Get profile pictures
- Get presence (online\offline\typing) status
# Quick start
+55 -16
View File
@@ -19,42 +19,81 @@
[[main]]
name = "Documentation"
identifier = "docs"
url = "/docs/overview/introduction/"
# url = "/docs/1.0/overview/introduction/"
weight = 10
#[[main]]
# name = "Blog"
# url = "/blog/"
# weight = 20
[[main]]
name = "Introduction"
weight = 11
url = "/docs/overview/introduction"
parent = "docs"
[[main]]
name = "Get Plus"
weight = 30
identifier = "get-plus"
url = ""
name = "Quick Start"
weight = 12
url = "/docs/overview/introduction"
parent = "docs"
[[main]]
name = "Send messages"
weight = 14
url = "/docs/how-to/send-messages"
parent = "docs"
[[main]]
name = "Receive Messages"
weight = 15
url = "/docs/how-to/receive-messages"
parent = "docs"
[[main]]
name = "How to guides"
weight = 20
url = "/docs/how-to"
parent = "docs"
[[main]]
name = "Swagger ->"
weight = 99
url = "https://waha.devlike.pro/swagger"
parent = "docs"
[[main]]
name = "WAHA Plus"
weight = 30
identifier = "waha-plus"
[[main]]
name = "About WAHA Plus"
weight = 40
identifier = "plus-version"
weight = 10
url = "/docs/how-to/plus-version"
parent = "get-plus"
parent = "waha-plus"
[[main]]
name = "Get on Boosty ->"
weight = 50
identifier = "boosty"
url = "https://boosty.to/wa-http-api"
parent = "get-plus"
parent = "waha-plus"
[[main]]
name = "Get on Patreon ->"
weight = 60
identifier = "patreon"
url = "https://patreon.com/wa_http_api"
parent = "get-plus"
parent = "waha-plus"
[[main]]
name = "Examples"
weight = 20
identifier = "examples"
[[main]]
name = "Python ->"
weight = 21
url = "https://github.com/devlikeapro/whatsapp-http-api/tree/core/examples/python"
parent = "examples"
[[social]]
name = "GitHub"
+3 -4
View File
@@ -4,7 +4,7 @@
title = "WAHA"
titleSeparator = "-"
titleAddition = "WhatsApp HTTP API"
description = "WAHA - WhatsApp HTTP API that you can configure in a click!"
description = "WAHA - WhatsApp HTTP API that you can run in a click!"
## Documentation
# docsVersion = "0.3"
@@ -72,9 +72,8 @@ copyRight = "Copyright (c) 2021-2023 Dev Like A Pro"
# Alert
alert = true
alertDismissable = false
# alertText = "Introducing the Doks child theme, several DX + UX updates, and more! <a class=\"alert-link stretched-link\" href=\"https://getdoks.org/blog/doks-v0.2/\" target=\"_blank\" rel=\"noopener\">Check out Doks v0.2</a>"
alertText = "Enjoy Core version or want to get WAHA Plus verison? <a class=\"alert-link stretched-link\" href=\"https://waha.devlike.pro/docs/how-to/plus-version/\">Support the project -></a>"
alertDismissable = true
alertText = "<a class=\"alert-link stretched-link\" href=\"https://waha.devlike.pro/docs/how-to/plus-version/\">Support the project and get WAHA Plus version! -></a>"
# Edit Page
# repoHost [Github | Gitea | GitLab | Bitbucket | BitbucketServer ] is used for building the edit link based on git hoster
+2 -2
View File
@@ -1,7 +1,7 @@
---
title : "WAHA"
description: "WAHA - WhatsApp HTTP API that you can configure in a click!"
lead: "WAHA - WhatsApp HTTP API that you can configure in a click!"
description: "WAHA - WhatsApp HTTP API that you can run in a click!"
lead: "WAHA - WhatsApp HTTP API that you can run in a click!"
date: 2020-10-06T08:47:36+00:00
lastmod: 2020-10-06T08:47:36+00:00
draft: false
-10
View File
@@ -1,10 +0,0 @@
---
title: "Help"
description: "Help Doks."
lead: ""
date: 2020-10-06T08:49:15+00:00
lastmod: 2020-10-06T08:49:15+00:00
draft: false
images: []
weight: 800
---
@@ -0,0 +1,41 @@
---
title : "Chats"
description: "Chats"
lead: ""
date: 2020-10-06T08:48:45+00:00
lastmod: 2020-10-06T08:48:45+00:00
draft: false
images: []
weight: 126
---
Chats methods.
Parameters in path that you can find in below endpoints:
- `{session}` - use the session name for Whatsapp instance that you created with `POST /api/session/start` endpoint
- `{chatId}` - chat id in format `123123123123@[c.us|g.us]`, `c.us` for direct chats and `g.us` for groups.
## Methods
### Get all chats
Get all chats
`GET /api/{session}/chats`
### Get messages from chat
Get 100 messages from the chat
`GET /api/{session}/chats/{chatId/messages?limit=100`
Get 100 messages from the chat, skip downloading media (images, files)
`GET /api/{session}/chats/{chatId/messages?limit=100&downloadMedia=false`
### Delete chat
Use the method to delete chat
`DELETE /api/{session}/chats/{chatId}`
### Clear messages
Use the method to clear all messages from the chat
`DELETE /api/{session}/chats/{chatId}/messages`
@@ -22,17 +22,14 @@ file as described on [How to deploy page ->]({{< relref "/docs/how-to/deploy" >}
## Environment variables
The following environment variables can be used to configure the WAHA:
The following environment variables can be used to configure the WAHA.
### Common
- `DEBUG=1`: Set this variable to any value to enable debug and verbose logs.
- `WHATSAPP_API_PORT=3000`: The port number that the HTTP server will listen on. The default value is `3000`.
- `WHATSAPP_API_HOSTNAME=localhost`: The hostname for the HTTP server. The default value is `localhost`.
- `WHATSAPP_API_KEY=mysecret`: If you set this variable, you must include the `X-Api-Key: mysecret` header in all
requests to the API. This will protect the API with a secret code.
- `WHATSAPP_SWAGGER_USERNAME=admin` and `WHATSAPP_SWAGGER_PASSWORD=admin`: These variables can be used to protect the
Swagger panel with `admin / admin` credentials. This does not affect API access.
- `WHATSAPP_SWAGGER_CONFIG_ADVANCED=true` - enables advanced configuration options for Swagger documentation - you can customize host, port and base URL for the requests.
Disabled by default.
### Sessions
- `WHATSAPP_RESTART_ALL_SESSIONS=True`: Set this variable to `True` to start all **STOPPED** sessions after container
restarts. By default, this variable is set to `False`.
- Please note that this will start all **STOPPED** sessions, not just the sessions that were working before the restart. You can maintain the session list by
@@ -42,7 +39,48 @@ The following environment variables can be used to configure the WAHA:
- `WHATSAPP_START_SESSION=session1,session2`: This variable can be used to start sessions with the specified names right
after launching the API. Separate session names with a comma.
### Examples
### Swagger
- `WHATSAPP_SWAGGER_CONFIG_ADVANCED=true` - enables advanced configuration options for Swagger documentation - you can customize host, port and base URL for the requests.
Disabled by default.
### Proxy
#### Global proxy configuration
If you need to use a proxy, you can set the following environment variables:
- `WHATSAPP_PROXY_SERVER=localhost:3128`: Use this variable to set the proxy server in the format `host:port`, without http or https.
- `WHATSAPP_PROXY_SERVER_USERNAME=username` and `WHATSAPP_PROXY_SERVER_PASSWORD=password`: Use these variables to set up credentials for the proxy.
- `WHATSAPP_PROXY_SERVER_LIST=host1.example.com:3138,host2.example.com:3138`: Use this variable to set a comma-separated list of addresses to use, using a round-robin algorithm to choose the server for the session.
- `WHATSAPP_PROXY_SERVER_INDEX_PREFIX=proxy-`: Use this variable to parse the session name for the prefix and find the appropriate session.
For example, if you have set `WHATSAPP_PROXY_SERVER_LIST=host-first:80,host-second:80,host-third:80` and `WHATSAPP_PROXY_SERVER_INDEX_PREFIX=proxy-` and you run `proxy-3` session, the `host-third:80` proxy will be chosen for that session.
This is a way to select a proxy from while you start session.
#### Session proxy configuration
You can configure proxy when you start session by setting `config.proxy` fields.
Read more about it on [**Session page** ->]({{< relref "/docs/how-to/sessions#configure-proxy" >}}).
Keep in mind that session's proxy configuration takes precedence over proxy configuration set by environment variables!
### Security ![](/images/versions/plus.png)
- `WHATSAPP_API_KEY=mysecret`: If you set this variable, you must include the `X-Api-Key: mysecret` header in all
requests to the API. This will protect the API with a secret code.
- `WHATSAPP_SWAGGER_USERNAME=admin` and `WHATSAPP_SWAGGER_PASSWORD=admin`: These variables can be used to protect the
Swagger panel with `admin / admin` credentials. This does not affect API access.
### Files ![](/images/versions/plus.png)
The following environment variables can be used to configure the file storage options for the WAHA:
- `WHATSAPP_FILES_MIMETYPES`: This variable can be used to download only specific mimetypes from messages. By default,
all files are downloaded. The mimetypes must be separated by a comma, without spaces. For
example: `audio,image/png,image/gif`. To choose a specific type, use a prefix (like `audio,image`).
- `WHATSAPP_FILES_LIFETIME`: This variable can be used to set the time (in seconds) after which files will be removed to
free up space. The default value is `180`.
- `WHATSAPP_FILES_FOLDER`: This variable can be used to set the folder where files from chats (images, voice messages)
will be stored. The default value is `/tmp/whatsapp-files`.
## Examples
#### Debug Mode
@@ -90,20 +128,6 @@ WHATSAPP_SWAGGER_USERNAME=admin
WHATSAPP_SWAGGER_PASSWORD=admin
```
## File storage variables ![](/images/versions/plus.png)
The following environment variables can be used to configure the file storage options for the WAHA:
- `WHATSAPP_FILES_MIMETYPES`: This variable can be used to download only specific mimetypes from messages. By default,
all files are downloaded. The mimetypes must be separated by a comma, without spaces. For
example: `audio,image/png,image/gif`. To choose a specific type, use a prefix (like `audio,image`).
- `WHATSAPP_FILES_LIFETIME`: This variable can be used to set the time (in seconds) after which files will be removed to
free up space. The default value is `180`.
- `WHATSAPP_FILES_FOLDER`: This variable can be used to set the folder where files from chats (images, voice messages)
will be stored. The default value is `/tmp/whatsapp-files`.
### Examples
#### Downloading Specific Mimetypes
@@ -6,7 +6,7 @@ date: 2020-10-06T08:48:45+00:00
lastmod: 2020-10-06T08:48:45+00:00
draft: false
images: [ ]
weight: 801
weight: 899
---
This page provides useful information to assist you in deploying the project.
@@ -27,7 +27,7 @@ instructions [here](https://docs.docker.com/compose/install/).
### Getting Started
To get started, you need to clone the docker-compose file to your local machine.
To get started, you need to clone the [docker-compose.yaml](https://github.com/devlikeapro/whatsapp-http-api/blob/core/docker-compose.yaml) file to your local machine.
Open your terminal and run the following command:
```
@@ -39,7 +39,7 @@ Not running Chromium saves you CPU and Memory, so you can run more instances on
️Working with the engine requires more attention, because it's easy to be blocked with it!
⚠ Read the article before using **NOWEB** engine [How to avoid blocking ->]({{< relref "/docs/help/how-to-avoid-blocking" >}}).
⚠ Read the article before using **NOWEB** engine [How to avoid blocking ->]({{< relref "/docs/overview/how-to-avoid-blocking" >}}).
- `WHATSAPP_DEFAULT_ENGINE=NOWEB`
- https://github.com/WhiskeySockets/Baileys
Binary file not shown.

Before

Width:  |  Height:  |  Size: 301 KiB

@@ -0,0 +1 @@
<?xml version="1.0" encoding="UTF-8"?><svg width="145px" height="40px" viewBox="0 0 145 40" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><defs><linearGradient x1="61.5370404%" y1="13%" x2="34.2376801%" y2="129.365079%" id="linearGradient-1"><stop stop-color="#EF7829" offset="0%"/><stop stop-color="#F0692A" offset="28%"/><stop stop-color="#F15E2C" offset="63%"/><stop stop-color="#F15A2C" offset="100%"/></linearGradient></defs><g id="Symbols" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd"><g id="Blocks/Header_Unlogged" transform="translate(-15.000000, -15.000000)"><g id="icons/boosty_logo" transform="translate(15.000000, 15.000000)"><g id="Shape"><path d="M138.119363,13.14856 L132.002653,22.63144 L131.397878,13.14856 L122.575597,13.14856 C123.384615,10.366 124.037135,8.13256 124.037135,8.13256 L124.381963,7.01848 L117.6313,7.01848 L117.312997,8.13256 L115.851459,13.14856 L108.660477,13.14856 C102.750663,13.14856 99.4244032,14.8276 98.0503979,18.00352 C97.2546419,15.09952 94.6206897,13.14856 90.5782493,13.14856 C87.2902077,13.135208 84.1128672,14.3300173 81.6551724,16.504 C80.464191,14.45536 78.1061008,13.1512 74.8116711,13.1512 C71.2975199,13.1360778 67.9189855,14.5000837 65.4084881,16.94752 C64.3103448,14.656 61.8541114,13.14856 58.3262599,13.14856 C57.1586378,13.1510938 55.9961737,13.302814 54.867374,13.6 L55.397878,11.77312 C55.4066092,11.7439195 55.4181572,11.7156279 55.4323607,11.68864 L56.7904509,7 L50.066313,7 L45.7798408,21.73384 C45.668435,22.02952 45.5676393,22.32784 45.4801061,22.63144 C45.2902336,23.2752559 45.1623364,23.9355911 45.0981432,24.60352 C44.5384615,28.714 46.3474801,31.816 51.3660477,32.0272 C51.8475451,32.0843575 52.332067,32.1125687 52.8169761,32.11168 C56.2925823,32.0722669 59.6211655,30.7100879 62.1193634,28.3048 C63.1564987,30.6148 65.5676393,32.11168 69.2970822,32.11168 C72.5436452,32.0790306 75.6705362,30.8873894 78.1087533,28.7536 C79.2493369,30.80488 81.5862069,32.11168 85.061008,32.11168 L101.917772,32.11168 C107.143236,32.11168 110.095491,31.08472 111.6313,28.85392 C111.6313,30.98176 112.851459,32.11168 115.941645,32.11168 C118.437666,32.11168 121.777188,31.5652 126.366048,30.43792 L119.687003,39.9208 L126.411141,39.9208 L144.846154,13.14856 L138.119363,13.14856 Z M58.9310345,22.63144 C58.3023873,24.79096 56.2785146,26.53336 54.4217507,26.53336 C52.5649867,26.53336 51.5676393,24.79096 52.1962865,22.63144 C52.8249337,20.47192 54.8488064,18.72688 56.6923077,18.72688 C58.535809,18.72688 59.5596817,20.464 58.9310345,22.63144 Z M75.4164456,22.63144 C74.7877984,24.79096 72.7639257,26.53336 70.9071618,26.53336 C69.0503979,26.53336 68.0769231,24.79096 68.6923077,22.63144 C69.3076923,20.47192 71.3448276,18.72688 73.2015915,18.72688 C75.0583554,18.72688 76.0344828,20.464 75.4164456,22.63144 Z M84.4562334,22.63144 C85.0848806,20.46928 87.1087533,18.72688 88.9522546,18.72688 C90.795756,18.72688 91.8090186,20.46928 91.1803714,22.63144 C90.5517241,24.7936 88.5941645,26.4832 86.7586207,26.536 L86.6074271,26.536 C84.801061,26.4832 83.8355438,24.76192 84.4562334,22.63144 Z M106.127321,25.85752 C105.787798,26.64952 103.132626,26.53072 102.466844,26.54128 L95.928382,26.536 C96.8103436,25.360374 97.4758216,24.0383816 97.8938992,22.63144 C97.9363395,22.48888 97.9734748,22.34896 98.0079576,22.20904 C98.7108753,23.2096 100.201592,24.11248 103.100796,24.68536 C105.864721,25.216 106.3687,25.05496 106.127321,25.85752 Z M112.777188,23.7244 C112.204244,21.49624 109.721485,20.6224 106.241379,20.2 C104.785146,20.02576 103.965517,19.95712 104.148541,19.3288 C104.289125,18.8404 105.018568,18.73744 106.586207,18.73744 L114.230769,18.73744 L112.777188,23.7244 Z M119.496021,23.74552 C119.496021,23.70064 120.151194,21.4804 120.952255,18.72952 L125.217507,18.72952 L125.915119,25.8364 C118.888594,27.328 118.713528,26.92672 119.496021,23.74552 L119.496021,23.74552 Z" fill="#242B2C" fill-rule="nonzero"/><path d="M1.03841146,23.8920635 L7.81508464,0 L18.2158138,0 L16.1119922,7.40740741 C16.0897554,7.44994435 16.0716695,7.49460291 16.0579948,7.54074074 L10.5357227,27.1005291 L15.6903971,27.1005291 C13.537424,32.5848325 11.8524284,36.8846561 10.6354102,40 C1.12771484,39.8920635 -1.53892578,32.9481481 0.795423177,24.7322751 L1.03841146,23.8920635 Z M10.672793,40 L23.2084961,21.6 L17.8897526,21.6 L22.5169141,9.81375661 C30.4586849,10.6603175 34.1824284,17.0285714 31.993457,24.7301587 C29.6424935,33.015873 20.1285677,40 10.8659375,40 L10.672793,40 Z" fill="url(#linearGradient-1)" fill-rule="nonzero"/></g></g></g></g></svg>

After

Width:  |  Height:  |  Size: 4.4 KiB

@@ -1,6 +1,6 @@
---
title : "Plus version"
description: "Plus version"
title : "WAHA Plus"
description: "WAHA Plus"
lead: ""
date: 2020-10-06T08:48:45+00:00
lastmod: 2020-10-06T08:48:45+00:00
@@ -16,38 +16,21 @@ weight: 900
1. Core ![](/images/versions/core.png) the basic version that meet almost 80% people’s needs. 100% free and open source.
2. Plus ![](/images/versions/plus.png) the version with advanced messages, security, and reliability features.
**If you enjoy Core ![](/images/versions/core.png) version and would like to support the project** - you can subscribe on Core level.
It's not expensive, but it supports the project a lot!
- [Boosty ->](https://boosty.to/wa-http-api)
- [Patreon ->](https://patreon.com/wa_http_api)
You support the project and get **WAHA Plus** on
<a href="https://boosty.to/wa-http-api" target="_blank">
<b>
Boosty ->
</b>
</a>
or
<a href="https://patreon.com/wa_http_api" target="_blank">
<b>
Patreon ->
</b>
</a>
.
## Features
The Plus additional features are:
1. [**Send images\files\voices** →]({{< relref "/docs/how-to/send-messages" >}})
2. [**Receive images\files\voices** →]({{< relref "/docs/how-to/receive-messages" >}})
3. [**Webhook retries** →]({{< relref "/docs/how-to/receive-messages#retries" >}})
4. [**Security** →]({{< relref "/docs/how-to/security" >}})
5. [**Saving sessions** →]({{< relref "/docs/how-to/sessions" >}})
6. [**Support multiple sessions** →]({{< relref "/docs/how-to/sessions" >}})
7. **Priority in support** - on both bugs and features
## Differences
What is the difference between **WAHA Plus** and other SaaS solutions for WhatsApp HTTP API?
| | WAHA Plus | Others |
|--------------------|:-------------------------------------------------:|----------------------------------------------------------------------------------:|
| **Sessions** | ✔️ No limits on **accounts** or **servers**! | ➖ One account for $50/month. |
| **Infrastructure** | ✔️ On-Premise - your server, your infrastructure! | ➖ Insecure cloud solution. |
| **Security** | ✔️ No data leaks! | ➖ You have to give access to customers' data - phone number, name, your messages. |
| **License Term** | ✔️ No license checks! | ➖ One month |
| **Message price** | ✔️ All messages are Free! | ➖ Some solutions have a price for messages - $1/100 messages. |
| **Expenses** | ✔️ $19 **donation**! | ➖ Monthly payment starts from $50 |
## Donations
### Donations
WAHA Plus version is available through **donations** (subscriptions).
@@ -66,29 +49,83 @@ Treat **WAHA Plus** it as an additional episode, behind-the-scenes videos, our n
We donate most of the donations down to the stream to the underlying libraries and tools - JS-community, Node, other libraries authors, personal contributors.
By donating to the **WAHA** you donate to the Open Source world!
### Features
The Plus additional features are:
1. [**Send images\files\voices** →]({{< relref "/docs/how-to/send-messages" >}})
2. [**Receive images\files\voices** →]({{< relref "/docs/how-to/receive-messages" >}})
3. [**Webhook retries** →]({{< relref "/docs/how-to/receive-messages#retries" >}})
4. [**Security** →]({{< relref "/docs/how-to/security" >}})
5. [**Saving sessions** →]({{< relref "/docs/how-to/sessions" >}})
6. [**Support multiple sessions** →]({{< relref "/docs/how-to/sessions" >}})
7. **Priority in support** - on both bugs and features
### Why WAHA?
What is the difference between **WAHA Plus** and **other SaaS solutions** for WhatsApp HTTP API?
| | WAHA Plus | Others |
|--------------------|:-------------------------------------------------:|----------------------------------------------------------------------------------:|
| **Sessions** | ✔️ No limits on **accounts** or **servers**! | ➖ One account for $50/month. |
| **Infrastructure** | ✔️ On-Premise - your server, your infrastructure! | ➖ Insecure cloud solution. |
| **Security** | ✔️ No data leaks! | ➖ You have to give access to customers' data - phone number, name, your messages. |
| **License Term** | ✔️ No license checks! | ➖ One month |
| **Message price** | ✔️ All messages are Free! | ➖ Some solutions have a price for messages - $1/100 messages. |
| **Expenses** | ✔️ $19 **donation**! | ➖ Monthly payment starts from $50 |
## Get Plus ![](/images/versions/plus.png)
For $19 one-time donation you get:
The more you support the project - the more you get!
Here's available tiers for our supporters:
| | Plus | Advanced | Pro |
|-------------------------------------------------------------------------------------|---------|:--------:|:-------:|
| Access to WAHA Plus ![](/images/versions/plus.png) docker image. | ✔️ | ✔️ | ✔️ |
| An invitation to a private Github repository so you have the WAHA Plus source code. | ➖️ | ✔️ | ✔️️ |
| Access to a Telegram group where we provide support and assistance. | ➖️ | ➖ | ✔️ |
| **Donation** | **$19** | **$39** | **$99** |
For **$19** donation you get:
1. **Secret password** for Docker Hub to download Plus image: `devlikeapro/whatsapp-http-api-plus`.
2. **All updates for one month** for the Plus image.
3. **NO LICENSE CHECKS** on installed instances!
4. **NO LICENSE EXPIRATION** on installed instances! We repeated it twice to make sure that you've got it 😊
{{< alert icon="👉" text="We don't mind if you stay subscribed with us after one month :)" />}}
### Boosty
We use Boosty to get donations (Patreon analogue) - [https://boosty.to/wa-http-api](https://boosty.to/wa-http-api).
Go ahead and get your monthly password to get no license expiration WhatsApp HTTP API!
{{< alert icon="💡" text="If you stay subscribed - the donation amount will never change for you and always be $19 to get monthly updates!" />}}
![](boosty.png)
### Boosty
<p align="center">
<img src="boosty.svg" alt="Patreon" />
<br/>
<br/>
</p>
Support the project and get WAHA Plus version on
<a href="https://boosty.to/wa-http-api" target="_blank">
<b>
Boosty ->
</b>
</a>
Go ahead and get your own WhatsApp HTTP API!
### Patreon
You can support the project and get WAHA Plus version on Patreon: [https://patreon.com/wa_http_api](https://patreon.com/wa_http_api)
<p align="center">
<img src="patreon.png" alt="Patreon" />
</p>
{{< alert icon="💡" text="Please look at Boosty - we prefer it because of lower fees, so we can donate more to the projects!" />}}
Support the project and get WAHA Plus version on
<a href="https://patreon.com/wa_http_api" target="_blank">
<b>
Patreon ->
</b>
</a>
Go ahead and get your own WhatsApp HTTP API!
### Install Plus
@@ -99,3 +136,4 @@ docker pull devlikeapro/whatsapp-http-api-plus
docker logout
```
Then in all commands use **Plus** image `devlikeapro/whatsapp-http-api-plus` instead of Core `devlikeapro/whatsapp-http-api`.
Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

@@ -0,0 +1,152 @@
---
title: "Presence"
description: "Presence"
lead: ""
date: 2020-10-06T08:48:45+00:00
lastmod: 2020-10-06T08:48:45+00:00
draft: false
images: [ ]
weight: 801
---
You can get presence information (online, offline with last seen, typing status) for a contact if they share their
presence information.
Possible presence statuses that you can set or get for chats:
- `online`
- `offline`
- `typing`
- `recording`
- `paused` resets the chat presence after you were `typing`
## Endpoints
### Set presence
You can set your global or chat-related presence with `POST /api/{session}/presence` endpoint
Start typing to a chat (you can use `POST /startTyping` instead)
```json
{
"chatId": "111111111@c.us",
"presence": "typing"
}
```
Clear "typing" state (you can use `POST /stopTyping` instead)
```json
{
"chatId": "111111111@c.us",
"presence": "paused"
}
```
Set global "online", all contacts will see it
```json
{
"presence": "online"
}
```
💡 In the multi-device version of WhatsApp - if a desktop client is active, WhatsApp doesn't send push notifications
to the device.
If you would like to receive said notifications - you need to mark a session's presence as `offline`.
```json
{
"presence": "offline"
}
```
### Get all chats presence
{{< alert icon="👉" text="Get presence endpoints and webhook is available only in **NOWEB** engine." />}}
[Read more about engines to choose right for you ->]({{< relref "/docs/how-to/engines" >}})
Here's few notes about fields:
- `chatId` - either contact id (`213213213@c.us`) or group chat id (`1111111111111@g.us`).
- `lastSeen` - contains Unix timestamps indicating when a participant was last online
- `lastKnownPresence` - contains the last known presence status, which can be
`offline`, `online`, `typing`, `recording`, or `paused`
You can get all presence information available for a session by calling `GET /api/{session}/presence/`.
It returns both groups' and personal chats' presence information.
```json
[
{
"id": "2132132130@c.us",
"presences": [
{
"participant": "2132132130@c.us",
"lastKnownPresence": "offline",
"lastSeen": 1686719326
}
]
},
{
"id": "11111111111111111111111@g.us",
"presences": [
{
"participant": "11111111111111111111111@g.us",
"lastKnownPresence": "online",
"lastSeen": null
},
{
"participant": "2132132130@c.us",
"lastKnownPresence": "offline",
"lastSeen": 1686719326
}
]
}
]
```
### Get chat presence
To get presence information for a single chat - call `GET /api/{session}/presence/{chatId}`.
For a group, you'll get participants' statuses.
```json
{
"id": "2132132130@c.us",
"presences": [
{
"participant": "2132132130@c.us",
"lastKnownPresence": "online",
"lastSeen": null
}
]
}
```
### Subscribe to presence
You can subscribe to presence information by calling `POST /api/{session}/presence/{chatId}/subscribe` request
(no body required).
You can get later presence information for the chat with above `GET` endpoints or by listening to `presence.update`
webhook.
## Webhook
You can subscribe to `presence.update` webhook event to get the most recent presence information.
```json
{
"event": "presence.update",
"session": "default",
"payload": {
"id": "111111111111111111@g.us",
"presences": [
{
"participant": "11111111111@c.us",
"lastKnownPresence": "typing",
"lastSeen": null
}
]
},
"engine": "NOWEB"
}
```
@@ -8,65 +8,37 @@ draft: false
images: []
weight: 120
---
We consider that you've run docker container and authenticated the session with QR code.
The project uses [Webhooks](https://en.wikipedia.org/wiki/Webhook) to send the messages and events from WhatsApp to your
application.
If you haven't yet - please follow the steps from [**Quick Start →**]({{< relref "/docs/overview/quick-start" >}}).
Webhooks are a way for two different applications to communicate with each other in real-time.
When a certain event happens in one application, it sends a message to another application through a webhook URL.
The receiving application can then take action based on the information received.
## Webhooks
To receive incoming messages with webhooks,
you first need to set up a webhook URL in your application and pass it to `WHATSAPP_HOOK_URL` environment variable.
This URL is where WhatsApp will send incoming messages and other events -
define which events you want to receive with `WHATSAPP_HOOK_EVENTS` environment variable.
## Webhooks ![](/images/versions/core.png)
You can configure where you want to receive events in environment variables:
- `WHATSAPP_HOOK_URL=https://httpbin.org/post` - to set up a URL for the webhook
- `WHATSAPP_HOOK_EVENTS=message,message.any,state.change,group.join,group.leave` - specify events. DO NOT specify all of
them, it's too heavy payload, choose the right for you.
- `WHATSAPP_HOOK_EVENTS=*` - subscribe to all events. It's not recommended for production, but it's fine for
development.
On the URL that you set via `WHATSAPP_HOOK_URL` you receive JSON-data with following format:
You must use [Webhooks]({{< relref "/docs/how-to/webhooks" >}}) to receive messages from WhatsApp to your application.
Start a new session with configured `message` event in webhooks - call `POST /api/sessions/start` with the payload:
```json
{
"event": "message",
"session": "default",
"engine": "WEBJS",
"payload": {}
"name": "default",
"config": {
"webhooks": [
{
"url": "https://httpbin.org/post",
"events": [
"message"
]
}
]
}
}
```
Where `event` value helps you identify the incoming event with `payload` for that events.
Below the list of all events that WhatsApp HTTP API sends to your.
{{< alert icon="👉" text="If you want to look at a webhook body - use the url `https://httpbin.org/post`" />}}
Run the bellow command and see look at the logs - it prints body request for all events that happen in your WhatsApp!
```bash
docker run -it -e "WHATSAPP_HOOK_EVENTS=*" -e WHATSAPP_HOOK_URL=https://httpbin.org/post -p 3000:3000 devlikeapro/whatsapp-http-api
```
### Examples
Here's few examples of how to handle webhook in different languages:
1. [Python guide]({{< relref "/docs/examples/python" >}})
**Do you use another language?**
Please create a short guide how to handle webhook and send message after you finish your setup!
You can create a pull request with your favorite language in the [GitHub, in examples folder](https://github.com/devlikeapro/whatsapp-http-api/tree/core/examples).
Just put the code and **README.md** file in the folder - we'll do the rest!
After that WAHA'll send events (see below) about new messages to an endpoint you provided.
## Events ![](/images/versions/core.png)
## Events
Here's examples of message-related events.
Read [Webhooks ->]({{< relref "/docs/how-to/webhooks" >}}) to find how to set them up.
### message
@@ -153,36 +125,6 @@ Fired on all message creations, including your own. The payload is the same as f
}
```
### state.change
```json
{
"event": "state.change",
"session": "default",
"payload": {}
}
```
### group.join
```json
{
"event": "group.join",
"session": "default",
"payload": {}
}
```
### group.leave
```json
{
"event": "group.left",
"session": "default",
"payload": {}
}
```
## Files ![](/images/versions/plus.png)
When people send you files - images, voice messages, and documents - WAHA saves it in the file storage.
@@ -206,17 +148,33 @@ Then you can use the link to download the file `http://localhost:3000/api/files/
To configure the url you can use environment variables `WHATSAPP_API_HOSTNAME` and `WHATSAPP_API_PORT`.
[Read more about file storage configuration and variables ->]({{< relref "config" >}}).
By default, WAHA download all files that the account receive.
If you want to limit files lifetime, specify file types for download or change directory for files -
[read more about file storage configuration and variables ->]({{< relref "config" >}}).
## Webhook retries ![](/images/versions/plus.png)
## Endpoints
**WAHA** retries to reach your webhook URL 15 times and with 2 seconds delay between attempts.
### Get messages
Read messages from the history - use `GET /api/messages` endpoint.
## Get messages
You can read messages from the history by using `GET /api/messages` endpoint.
You also can get messages for chats by using
[**Get messages from chat ->**]({{< relref "/docs/how-to/chats" >}}) endpoint.
{{< alert icon="👉" text="We do not recommend using this method in production, consider using webhooks instead!" />}}
```bash
curl -X 'GET' \
'http://localhost:3000/api/messages?chatId=11111111111%40c.us&limit=1000&session=default' \
-H 'accept: application/json'
```
## Examples
Here's few examples of how to receive messages in different languages:
1. [Python guide ->]({{< relref "/docs/examples/python" >}})
**Do you use another language?**
Please create a short guide how to handle webhook and send message after you finish your setup!
You can create a pull request with your favorite language in the
[GitHub, in examples folder ->](https://github.com/devlikeapro/whatsapp-http-api/tree/core/examples).
@@ -65,3 +65,7 @@ headers = {
requests.get("http://localhost:3000/api/sessions", headers=headers)
```
## Webhook security ![](/images/versions/plus.png)
To make sure that you get a webhook from your WAHA instance - you can use **HMAC authentication**.
Read more about it on [Webhooks page ->]({{< relref "/docs/how-to/webhooks#hmac-authentication" >}})
@@ -8,13 +8,10 @@ draft: false
images: []
weight: 110
---
On your swagger documentation [http://localhost:3000/](http://localhost:3000/)
or in [the example swagger](/swagger) you can find:
1. List of all supported methods - we have not documented all of them on the page
1. Requests JSON schemas
1. Responses JSON schemas
1. **Try it** button - no additional setup to play around with a method
We consider that you've run docker container and authenticated the session with QR code.
If you haven't yet - please follow the steps from [**Quick Start →**]({{< relref "/docs/overview/quick-start" >}}).
## Fields
There are common fields that you can find in almost all requests:
@@ -37,6 +34,23 @@ To send text message - use `POST /api/sendText` with example payload.
"text": "Hi there!"
}
```
### Mention contact
If you send a message in a group and want to mention a participant in the message -
use `mentions` field for that in `POST /api/sendText` request.
Please note that you MUST mention a number in the text as well in the format `@2132132130` and
also mention it in `mentions` in format `2132132130@c.us`
```json
{
"session": "default",
"chatId": "12132132130@c.us",
"text": "Hi there! @2132132130",
"mentions": [
"2132132130@c.us"
]
}
```
## Reply on message ![](/images/versions/core.png)
To reply on a message - use `POST /api/reply` with example payload.
@@ -1,44 +1,15 @@
---
title : "Sessions"
title: "Sessions"
description: "Sessions"
lead: ""
date: 2020-10-06T08:48:45+00:00
lastmod: 2020-10-06T08:48:45+00:00
draft: false
images: []
weight: 200
images: [ ]
weight: 125
---
## Saving session ![](/images/versions/plus.png)
Plus ![](/images/versions/plus.png) version allows you to save "session" state and avoid scanning QR code everytime when
you start a container.
### File storage
If you want to save your session and do not scan QR code everytime when you launch WAHA - connect a local file storage
to the container. WAHA stores authentication information in the directory and reuses it after restart.
[Attach volume](https://docs.docker.com/storage/volumes/) part to the command:
```bash
-v `pwd`/.sessions:/app/.sessions
```
The full command would be:
```bash
docker run --rm -d -v `pwd`/.sessions:/app/.sessions -p 3000:3000/tcp --name whatsapp-http-api devlikeapro/whatsapp-http-api
```
### Remote storage ![](/images/versions/soon.png)
If you're interested in using some "remote" storage (like Redis or other Databases) to save sessions - please create an
issue on GitHub.
For instances, it may be useful if you run WAHA in a cluster of servers and do not have shared file storage
## Multiple sessions ![](/images/versions/plus.png)
If you want to save server's CPU and Memory - run multiple sessions inside one docker container!
## Endpoints
### Start
@@ -49,6 +20,68 @@ In order to start a new session - call `POST /api/sessions/start`
"name": "default"
}
```
#### Configure webhooks
You can configure webhooks for a session:
```json
{
"name": "default",
"config": {
"webhooks": [
{
"url": "https://httpbin.org/post",
"events": [
"message"
]
}
]
}
}
```
Read more about available options on [**Webhooks page ->**]({{< relref "/docs/how-to/webhooks#hmac-authentication" >}})
The configuration is saved and will be applied if the docker container restarts,
and you set `WHATSAPP_RESTART_ALL_SESSIONS` environment variables.
Read more about it in [Autostart section](#autostart).
#### Configure proxy
You can configure proxy for a session by setting `config.proxy` fields:
- `server` - proxy server address, without `http://` or `https://` prefixes
- `username` and `password` - set this if the proxy requires authentication
**No authentication**
```json
{
"name": "default",
"config": {
"proxy": {
"server": "localhost:3128"
}
}
}
```
**Proxy with authentication**
```json
{
"name": "default",
"config": {
"proxy": {
"server": "localhost:3128",
"username": "username",
"password": "P@ssw0rd"
}
}
}
```
The configuration is saved and will be applied if the docker container restarts,
and you set `WHATSAPP_RESTART_ALL_SESSIONS` environment variables.
Read more about it in [Autostart section](#autostart).
You can configure proxy when for all sessions by set up environment variables.
Read more about it on [**Configuration page** ->]({{< relref "/docs/how-to/config#proxy" >}}).
### List
@@ -65,6 +98,10 @@ The response:
]
```
You can add `?all=true` parameter to the request `GET /api/session?all=True` it'll show you ALL session,
including **STOPPED**,
so you can know which one will be restarted if you set `WHATSAPP_RESTART_ALL_SESSIONS=True` environment variable.
### Stop
In order to stop a new session - call `POST /api/sessions/stop`
@@ -78,7 +115,6 @@ In order to stop a new session - call `POST /api/sessions/stop`
}
```
### Logout
In order to log out the session - call `POST /api/sessions/logout`
@@ -92,3 +128,54 @@ In order to log out the session - call `POST /api/sessions/logout`
}
```
## Advanced sessions ![](/images/versions/plus.png)
With [WAHA Plus version]({{< relref "plus-version" >}}) you can save session state to avoid scanning QR code everytime,
configure autostart options so when the docker container restarts - it restores all previously run sessions!
### Session persistent
#### File storage
If you want to save your session and do not scan QR code everytime when you launch WAHA - connect a local file storage
to the container. WAHA stores authentication information in the directory and reuses it after restart.
[Attach volume](https://docs.docker.com/storage/volumes/) part to the command:
```bash
-v `pwd`/.sessions:/app/.sessions
```
The full command would be:
```bash
docker run --rm -d -v `pwd`/.sessions:/app/.sessions -p 3000:3000/tcp --name whatsapp-http-api devlikeapro/whatsapp-http-api-plus
```
#### Remote storage ![](/images/versions/soon.png)
If you're interested in using some "remote" storage (like Redis or other Databases) to save sessions - please create an
issue on GitHub.
For instances, it may be useful if you run WAHA in a cluster of servers and do not have shared file storage
### Autostart
If you don't want to call `POST /api/sessions/start` for every session each time when the container restart -
you can use set of these environment variables to start sessions for you:
- `WHATSAPP_RESTART_ALL_SESSIONS=True`: Set this variable to `True` to start all **STOPPED** sessions after container
restarts. By default, this variable is set to `False`.
- Please note that this will start all **STOPPED** sessions, not just the sessions that were working before the restart. You can maintain the session list by
using `POST /api/session/stop` with the `logout: True` parameter or by calling `POST /api/session/logout` to remove
**STOPPED** sessions. You can see all sessions, including **STOPPED** sessions, in the `GET /api/sessions/all=True`
response.
- `WHATSAPP_START_SESSION=session1,session2`: This variable can be used to start sessions with the specified names right
after launching the API. Separate session names with a comma.
### Multiple sessions
If you want to save server's CPU and Memory - run multiple sessions inside one docker container!
[Plus version]({{< relref "plus-version" >}}) supports multiple sessions in one container.
@@ -0,0 +1,374 @@
---
title : "Webhooks"
description: "Webhooks"
lead: ""
date: 2020-10-06T08:48:45+00:00
lastmod: 2020-10-06T08:48:45+00:00
draft: false
images: []
weight: 121
---
The project uses [Webhooks](https://en.wikipedia.org/wiki/Webhook) to send the messages and events from WhatsApp to your
application.
Webhooks are a way for two different applications to communicate with each other in real-time.
When a certain event happens in one application, it sends a message to another application through a webhook URL.
The receiving application can then take action based on the information received.
## Setup webhooks
### Session webhooks
You can define webhooks configuration per session when you start it with `POST /api/sessions/start` request data.
Here's a simple example:
```json
{
"name": "default",
"config": {
"webhooks": [
{
"url": "https://httpbin.org/post",
"events": [
"message"
]
}
]
}
}
```
Here's available configuration options for webhooks
```json
{
"name": "default",
"config": {
"webhooks": [
{
"url": "https://httpbin.org/post",
"events": [
"message"
],
"hmac": {
"key": "your-secret-key"
},
"retries": {
"delaySeconds": 2,
"attempts": 15
},
"customHeaders": [
{
"name": "X-My-Custom-Header",
"value": "Value"
}
]
}
]
}
}
```
### Global webhooks
There's a way how you can configure webhooks for ALL sessions - by settings these environment variables:
- `WHATSAPP_HOOK_URL=https://httpbin.org/post` - to set up a URL for the webhook
- `WHATSAPP_HOOK_EVENTS=message,message.any,state.change` - specify events. Do not specify all of
them, it's too heavy payload, choose the right for you.
- `WHATSAPP_HOOK_EVENTS=*` - subscribe to all events. It's not recommended for production, but it's fine for
development.
That webhook configuration **does not appear** in `session.config` field in `GET /api/sessions/` request.
## Webhook payload
On the URL that you set you'll receive **HTTP POST** request with a JSON string with following format:
```json
{
"event": "message",
"session": "default",
"engine": "WEBJS",
"payload": {
...
}
}
```
Where `event` value helps you identify the incoming event with `payload` for that events.
Below the list of all events that WhatsApp HTTP API sends to your.
{{< alert icon="👉" text="If you want to look at a webhook body - use the url https://httpbin.org/post" />}}
Run the bellow command and see look at the logs - it prints body request for all events that happen in your WhatsApp!
```bash
docker run -it -e "WHATSAPP_HOOK_EVENTS=*" -e WHATSAPP_HOOK_URL=https://httpbin.org/post -p 3000:3000 devlikeapro/whatsapp-http-api
```
## Events
### message
Incoming message (text/audio/files)
```json
{
"event": "message",
"session": "default",
"engine": "WEBJS",
"payload": {
"id": "true_11111111111@c.us_AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA",
"timestamp": 1667561485,
"from": "11111111111@c.us",
"fromMe": true,
"to": "11111111111@c.us",
"body": "Hi there!",
"hasMedia": false,
"ack": 1,
"vCards": [],
"_data": {
"id": {
"fromMe": true,
"remote": "11111111111@c.us",
"id": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA",
"_serialized": "true_11111111111@c.us_AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"
},
"body": "Hi there!",
"type": "chat",
"t": 1667561485,
"notifyName": "MyName",
"from": "11111111111@c.us",
"to": "11111111111@c.us",
"self": "in",
"ack": 1,
"isNewMsg": true,
"star": false,
"kicNotified": false,
"recvFresh": true,
"isFromTemplate": false,
"pollInvalidated": false,
"latestEditMsgKey": null,
"latestEditSenderTimestampMs": null,
"broadcast": false,
"mentionedJidList": [],
"isVcardOverMmsDocument": false,
"isForwarded": false,
"hasReaction": false,
"ephemeralOutOfSync": false,
"productHeaderImageRejected": false,
"lastPlaybackProgress": 0,
"isDynamicReplyButtonsMsg": false,
"isMdHistoryMsg": false,
"stickerSentTs": 0,
"isAvatar": false,
"requiresDirectConnection": false,
"pttForwardedFeaturesEnabled": true,
"isEphemeral": false,
"isStatusV3": false,
"links": []
}
}
}
```
### message.any
Fired on all message creations, including your own. The payload is the same as for [message](#message) event.
```json
{
"event": "message.any",
"session": "default",
"engine": "WEBJS",
"payload": {
...
}
}
```
### message.ack
```json
{
"event": "message.ack",
"session": "default",
"engine": "WEBJS",
"payload": {
...
}
}
```
### state.change
It's an internal engine's state, not **session** `status`.
```json
{
"event": "state.change",
"session": "default",
"engine": "WEBJS",
"payload": {
...
}
}
```
### group.join
```json
{
"event": "group.join",
"session": "default",
"engine": "WEBJS",
"payload": {
...
}
}
```
### group.leave
```json
{
"event": "group.left",
"session": "default",
"engine": "WEBJS",
"payload": {
...
}
}
```
### presence.update
- `payload.id` indicates the chat - either direct chat with a contact or a group chat.
- `payload.id.[].participant` - certain participant presence status. For a direct chat there's only one participant.
```json
{
"event": "presence.update",
"session": "default",
"engine": "NOWEB",
"payload": {
"id": "111111111111111111@g.us",
"presences": [
{
"participant": "11111111111@c.us",
"lastKnownPresence": "typing",
"lastSeen": null
}
]
}
}
```
## Webhooks Advanced ![](/images/versions/plus.png)
### HMAC authentication
You can authenticate webhook sender by using [HMAC Authentication](https://www.okta.com/identity-101/hmac/).
1. Define you secret key in `config.hmac.key` field when you start session with `POST /api/sessions/start`:
```json
{
"name": "default",
"config": {
"webhooks": [
{
"url": "https://httpbin.org/post",
"events": [
"message"
],
"hmac": {
"key": "your-secret-key"
}
}
]
}
}
```
2. After that you'll receive all webhooks payload with two additional headers:
- `X-Webhook-Hmac` - message authentication code for the raw **body** in HTTP POST request that send to your endpoint.
- `X-Webhook-Hmac-Algorithm` - `sha512` - algorithm that have been used to create `X-Webhook-Hmac` value.
3. Implement the authentication algorithm by hashing body and using secret key and then verifying it with `X-Webhook-Hmac`
value. Please [check your implementation here ->](https://www.devglan.com/online-tools/hmac-sha256-online)
Here's example for
```
# Full body
{"event":"message","session":"default","engine":"WEBJS"}
# Secret key
my-secret-key
# X-Webhook-Hmac-Algorithm
sha512
# X-Webhook-Hmac
208f8a55dde9e05519e898b10b89bf0d0b3b0fdf11fdbf09b6b90476301b98d8097c462b2b17a6ce93b6b47a136cf2e78a33a63f6752c2c1631777076153fa89
```
### Retries
**WAHA** retries to reach your webhook URL **15 times** with **2 seconds delay** between attempts by default in
[Plus Version →]({{< relref "plus-version" >}})
You can configure those parameters by settings `config.retries` structure when `POST /api/sessions/start`:
```json
{
"name": "default",
"config": {
"webhooks": [
{
"url": "https://httpbin.org/post",
"events": [
"message"
],
"retries": {
"delaySeconds": 2,
"attempts": 15
}
}
]
}
}
```
### Custom Headers
You can send any customer headers by defining `config.webhooks.customHeaders` fields this way:
```json
{
"name": "default",
"config": {
"webhooks": [
{
"url": "https://httpbin.org/post",
"events": [
"message"
],
"customHeaders": [
{
"name": "X-My-Custom-Header",
"value": "Value"
}
]
}
]
}
}
```
## Examples
Here's few examples of how to handle webhook in different languages:
1. [Python guide]({{< relref "/docs/examples/python" >}})
**Do you use another language?**
Please create a short guide how to handle webhook and send message after you finish your setup!
You can create a pull request with your favorite language in the
[GitHub, in examples folder ->](https://github.com/devlikeapro/whatsapp-http-api/tree/core/examples).
@@ -9,11 +9,38 @@ images: [ ]
menu:
docs:
parent: "help"
weight: 600
weight: 199
toc: true
---
## 2023.8
August 2023
- Added [chats endpoints](https://waha.devlike.pro/docs/how-to/chats)
- Get all chats - `GET /api/{session/chats`
- Delete chat - `DELETE /api/{session/chats/{chatId}`
- Get messages from chat - `GET /api/{session/chats/{chatId}/messages?limit=1000?downloadMedia=true`
- Clear messages in the chat `DELETE /api/{session/chats/{chatId}/messages`
- Added `downloadMedia` flag to speed up `GET /api/messages` and `GET /api/{session/chats/{chatId}/messages` performance.
- Added [set presence endpoint](https://waha.devlike.pro/docs/how-to/presence)!
- With `online` presence you can show your status as online [#121](https://github.com/devlikeapro/whatsapp-http-api/issues/121)
- With `offline` presence you can get notifications for your device [#28](https://github.com/devlikeapro/whatsapp-http-api/issues/28)
## 2021.6 - June 2023
## 2023.7
July 2023
- Now session can have their own [Webhooks](https://waha.devlike.pro/docs/how-to/webhooks) -
you can define webhook configuration when you start a session with `POST /api/session/start`!
- Add HMAC authentication for webhooks
- Configure retries
- Add you custom headers
- Added [proxy configuration](https://waha.devlike.pro/docs/how-to/config/#proxy) with supporting proxy authentication.
Thanks **puntolino** for the contribution!
You can control proxy's settings per session with `POST /api/session/start` or globally with environment variables.
- Added [presence information](https://waha.devlike.pro/docs/how-to/presence) - now you can get online status for
a contact by calling endpoints or receiving a webhook event!
- Now you can mention contact in groups by settings `mentions` field in `POST /api/sendText`
[read more about it in Send Messages ->]({{< relref "/docs/how-to/send-messages" >}})
## 2023.6
June 2023
Improvements on session management, restarting sessions and more:
@@ -46,7 +73,8 @@ Improvements on session management, restarting sessions and more:
}
```
## 2021.5 - May 2023
## 2023.5
May 2023
- Added new [NOWEB engine]({{< relref "/docs/how-to/engines" >}}). **NOWEB** engine does not require a browser to work
with
@@ -54,14 +82,16 @@ Improvements on session management, restarting sessions and more:
- Less CPU and RAM usage!
- Send Locations API works!
- Send Link Preview API works!
- ⚠ Read the article before using it [How to avoid blocking ->]({{< relref "/docs/help/how-to-avoid-blocking" >}}).
- ⚠ Read the article before using it [How to avoid blocking ->]({{< relref "/docs/overview/how-to-avoid-blocking" >}}).
## 2021.4 - March 2023
## 2023.4
March 2023
- Add [Groups API]({{< relref "/docs/how-to/groups" >}})
- Use Chromium by default instead of Chrome
## 2021.1 - January 2023
## 2023.1
January 2023
- Added [Contacts API]({{< relref "/docs/how-to/contacts" >}})
- Get all contacts
@@ -71,7 +101,8 @@ Improvements on session management, restarting sessions and more:
- Check number exists (is registered in WhatsApp) - works even if the number is not in the contact list
- Block and unblock contact
## 2022.12 - December 2023
## 2022.12
December 2023
- Added `GET /messages/` endpoint to get chat messages [#31](https://github.com/devlikeapro/whatsapp-http-api/issues/31)
@@ -9,7 +9,7 @@ images: [ ]
menu:
docs:
parent: "help"
weight: 630
weight: 130
toc: true
---
@@ -8,7 +8,7 @@ images: [ ]
menu:
docs:
parent: "help"
weight: 610
weight: 120
toc: true
---
@@ -23,58 +23,29 @@ toc: true
{{< alert icon="👉" text="Run WhatsApp HTTP API in less than 5 minutes!" />}}
We will guide you through the necessary steps to successfully send your first text message using WhatsApp HTTP API in
[Quick Start →]({{< relref "quick-start" >}}).
If you find it easier to follow a video tutorial rather than reading through text, we've got you covered!
A 3-minute video tutorial is available to help you navigate WhatsApp HTTP API smoothly.
<div class="d-flex justify-content-center my-4">
<iframe
width="560" height="315"
src="https://www.youtube.com/embed/RFerMyAUPRg"
title="YouTube video player"
frameborder="0"
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
allowfullscreen
></iframe>
</div>
[**Quick Start →**]({{< relref "quick-start" >}}).
## Features
WAHA is distributed in two versions:
- Core ![](/images/versions/core.png) - the basic version that meet almost 80% people's needs. 100% free and [open source](https://github.com/devlikeapro/whatsapp-http-api).
- Plus ![](/images/versions/plus.png) - the version with advanced messages, security, and reliability features. One-time donation, no license expiration, quick updates, read more about [Plus Version →]({{< relref "plus-version" >}})
- Core ![](/images/versions/core.png) - the basic version that meet almost 80% people's needs. 100% free and [open source ->](https://github.com/devlikeapro/whatsapp-http-api).
- Plus ![](/images/versions/plus.png) - the version with advanced messages, security, and reliability features. Donations, no license expiration, quick updates, read more about [Plus Version →]({{< relref "plus-version" >}})
Here's the available features:
### Messages
- Send messages ![](/images/versions/core.png)
- Receive messages ![](/images/versions/core.png)
- Message replies ![](/images/versions/core.png)
- Send location ![](/images/versions/core.png)
- Send buttons ![](/images/versions/core.png)
- Receive location ![](/images/versions/core.png)
- React to messages ![](/images/versions/core.png)
- Send messages
- Receive messages
- Message replies
- Send location
- Send buttons
- Receive location
- React to messages
- Send media (images/documents/files) ![](/images/versions/plus.png)
- Send voice messages ![](/images/versions/plus.png)
- Receive media (images/audio/video/documents) ![](/images/versions/plus.png)
- Receive messages - webhook retries ![](/images/versions/plus.png)
### Sessions
- Multi Device ![](/images/versions/core.png)
- Get the screenshot ![](/images/versions/core.png)
- Session saving (don't have to scan QR on every restart) ![](/images/versions/plus.png)
- Session saving on remote storage (database, redis) ![](/images/versions/plus-soon.png)
- Single WhatsApp account running inside one container ![](/images/versions/core.png)
- Multiple WhatsApp account running inside one container ![](/images/versions/plus.png)
### Security
- API authentication ![](/images/versions/plus.png)
- Swagger panel authentication ![](/images/versions/plus.png)
### Updates
- Bug fixes and updates come as soon as they are implemented. ![](/images/versions/core.png)![](/images/versions/plus.png)
### Groups ![](/images/versions/core.png)
### Groups
- Create a group
- Get invite for group
- Modify group info (subject, description)
@@ -82,8 +53,25 @@ Here's the available features:
- Kick group participants
- Promote/demote group participants
### Contacts ![](/images/versions/core.png)
### Contacts
- Mute/unmute chats
- Block/unblock contacts
- Get contact info
- Get profile pictures
- Get presence (online\offline\typing) status
### Sessions
- Multi Device
- Get the screenshot
- Single WhatsApp account running inside one container
- Multiple WhatsApp account running inside one container ![](/images/versions/plus.png)
- Session saving (don't have to scan QR on every restart) ![](/images/versions/plus.png)
- Session saving on remote storage (database, redis) ![](/images/versions/plus-soon.png)
### Security
- API authentication ![](/images/versions/plus.png)
- Swagger panel authentication ![](/images/versions/plus.png)
### Updates
- Bug fixes and updates come as soon as they are implemented.
@@ -16,6 +16,8 @@ toc: true
On this page you're going to install WAHA, run it, authenticate the client using QR code and send the first text message
to WhatsApp!
![](swagger.png)
## Requirements
Only thing that you must have - installed docker. Please follow the original
@@ -34,7 +36,7 @@ Also Docker makes installation and update processes so simple, just one command!
We will guide you through the necessary steps to successfully send your first text message using WhatsApp HTTP API.
If you find it easier to follow a video tutorial rather than reading through text, we've got you covered!
**If you find it easier to follow a video** tutorial, we've got you covered!
A 3-minute video tutorial is available to help you navigate WhatsApp HTTP API smoothly.
<div class="d-flex justify-content-center my-4">
@@ -181,7 +183,6 @@ curl -d "{\"chatId\": \"${PHONE}@c.us\", \"text\": \"Hello from WhatsApp HTTP AP
```
## What is next?
- Right now you've sent simple text message, but you can send more! [Send messages →]({{< relref "/docs/how-to/send-messages" >}})
- Read how to receive and response to messages [Receive messages →]({{< relref "/docs/how-to/receive-messages" >}})
- Make sure your [API is properly protected ->]({{< relref "/docs/how-to/security" >}})
+6 -13
View File
@@ -4,29 +4,22 @@
<div class="col-lg-12 text-center">
<h1 class="mt-0">{{ .Title }}</h1>
</div>
<div class="col-lg-9 col-xl-8 text-center">
<div class="col-lg-9 col-xl-9 text-center">
<div class="mb-4">
<img src="/images/logo.png" title="WhatsApp HTTP API" alt="logo" width="100"/>
</div>
<p class="lead">{{ .Params.lead | safeHTML }}</p>
<code>
docker run -it -p 3000:3000/tcp devlikeapro/whatsapp-http-api
</code>
<br/>
<br/>
<a class="btn btn-primary btn-lg px-4 mb-2"
href="/docs/{{ if .Site.Params.options.docsVersioning }}{{ .Site.Params.docsVersion }}/{{ end }}overview/introduction/"
role="button">Get Started</a>
<p class="meta"><a href="https://github.com/devlikeapro/whatsapp-http-api">Source code in GitHub.</a></p>
</div>
</div>
<div>
<div class="d-flex justify-content-center my-4">
<iframe
width="560" height="315"
src="https://www.youtube.com/embed/RFerMyAUPRg"
title="YouTube video player"
frameborder="0"
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
allowfullscreen
></iframe>
</div>
</div>
</section>
{{ end }}
+1 -1
View File
@@ -3,7 +3,7 @@
<html lang="en">
<head>
<meta charset="UTF-8">
<title>devlikeapro/whatsapp-http-api</title>
<title>WAHA - WhatsApp HTTP API</title>
<link href="./swagger-ui.css" rel="stylesheet" type="text/css">
<link href="./favicon-32x32.png" rel="icon" sizes="32x32" type="image/png"/>
<link href="./favicon-16x16.png" rel="icon" sizes="16x16" type="image/png"/>
+1 -1
View File
@@ -1 +1 @@
{"openapi":"3.0.0","paths":{"/api/sessions/start":{"post":{"operationId":"SessionsController_start","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SessionStartRequest"}}}},"responses":{"201":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SessionDTO"}}}}},"tags":["sessions"],"security":[{"api_key":[]}]}},"/api/sessions/stop":{"post":{"operationId":"SessionsController_stop","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SessionStopRequest"}}}},"responses":{"201":{"description":""}},"tags":["sessions"],"security":[{"api_key":[]}]}},"/api/sessions":{"get":{"operationId":"SessionsController_list","parameters":[],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/SessionDTO"}}}}}},"tags":["sessions"],"security":[{"api_key":[]}]}},"/api/checkNumberStatus":{"get":{"operationId":"ChattingController_checkNumberStatus","summary":"Check number status","parameters":[{"name":"phone","required":true,"in":"query","schema":{"type":"string"}},{"name":"session","required":true,"in":"query","schema":{"default":"default","type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/WANumberExistResult"}}}}},"tags":["chatting"],"security":[{"api_key":[]}]}},"/api/sendContactVcard":{"post":{"operationId":"ChattingController_sendContactVcard","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/MessageContactVcardRequest"}}}},"responses":{"201":{"description":"","content":{"application/json":{"schema":{"type":"object"}}}}},"tags":["chatting"],"security":[{"api_key":[]}]}},"/api/sendText":{"get":{"operationId":"ChattingController_sendTextGet","summary":"Send a text message","parameters":[{"name":"phone","required":true,"in":"query","schema":{"type":"string"}},{"name":"text","required":true,"in":"query","schema":{"type":"string"}},{"name":"session","required":true,"in":"query","schema":{"default":"default","type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"object"}}}}},"tags":["chatting"],"security":[{"api_key":[]}]},"post":{"operationId":"ChattingController_sendText","summary":"Send a text message","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/MessageTextRequest"}}}},"responses":{"201":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/WAMessage"}}}}},"tags":["chatting"],"security":[{"api_key":[]}]}},"/api/sendTextButtons":{"post":{"operationId":"ChattingController_sendTextButtons","summary":"Send a text message with buttons","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/MessageTextButtonsRequest"}}}},"responses":{"201":{"description":"","content":{"application/json":{"schema":{"type":"object"}}}}},"tags":["chatting"],"security":[{"api_key":[]}]}},"/api/sendLocation":{"post":{"operationId":"ChattingController_sendLocation","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/MessageLocationRequest"}}}},"responses":{"201":{"description":"","content":{"application/json":{"schema":{"type":"object"}}}}},"tags":["chatting"],"security":[{"api_key":[]}]}},"/api/sendLinkPreview":{"post":{"operationId":"ChattingController_sendLinkPreview","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/MessageLinkPreviewRequest"}}}},"responses":{"201":{"description":"","content":{"application/json":{"schema":{"type":"object"}}}}},"tags":["chatting"],"security":[{"api_key":[]}]}},"/api/sendImage":{"post":{"operationId":"ChattingController_sendImage","summary":"Send an image. Either from an URL or base64 data - look at the request schemas for details.","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/MessageImageRequest"}}}},"responses":{"201":{"description":"","content":{"application/json":{"schema":{"type":"object"}}}}},"tags":["chatting"],"security":[{"api_key":[]}]}},"/api/sendFile":{"post":{"operationId":"ChattingController_sendFile","summary":"Send a file. Either from an URL or base64 data - look at the request schemas for details.","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/MessageFileRequest"}}}},"responses":{"201":{"description":"","content":{"application/json":{"schema":{"type":"object"}}}}},"tags":["chatting"],"security":[{"api_key":[]}]}},"/api/sendVoice":{"post":{"operationId":"ChattingController_sendVoice","summary":"Send an voice message. Either from an URL or basLine truncated
{"openapi":"3.0.0","paths":{"/api/sessions/start":{"post":{"operationId":"SessionsController_start","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SessionStartRequest"}}}},"responses":{"201":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SessionDTO"}}}}},"tags":["sessions"],"security":[{"api_key":[]}]}},"/api/sessions/stop":{"post":{"operationId":"SessionsController_stop","summary":"Stop session","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SessionStopRequest"}}}},"responses":{"201":{"description":""}},"tags":["sessions"],"security":[{"api_key":[]}]}},"/api/sessions/logout":{"post":{"operationId":"SessionsController_clean","summary":"Logout from session.","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SessionLogoutRequest"}}}},"responses":{"201":{"description":""}},"tags":["sessions"],"security":[{"api_key":[]}]}},"/api/sessions":{"get":{"operationId":"SessionsController_list","parameters":[{"name":"all","required":false,"in":"query","example":false,"description":"Return all sessions, including those that are in the STOPPED state.","schema":{"type":"boolean"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/SessionDTO"}}}}}},"tags":["sessions"],"security":[{"api_key":[]}]}},"/api/checkNumberStatus":{"get":{"operationId":"ChattingController_checkNumberStatus","summary":"Check number status","parameters":[{"name":"phone","required":true,"in":"query","schema":{"type":"string"}},{"name":"session","required":true,"in":"query","schema":{"default":"default","type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/WANumberExistResult"}}}}},"tags":["chatting"],"security":[{"api_key":[]}]}},"/api/sendContactVcard":{"post":{"operationId":"ChattingController_sendContactVcard","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/MessageContactVcardRequest"}}}},"responses":{"201":{"description":"","content":{"application/json":{"schema":{"type":"object"}}}}},"tags":["chatting"],"security":[{"api_key":[]}]}},"/api/sendText":{"get":{"operationId":"ChattingController_sendTextGet","summary":"Send a text message","parameters":[{"name":"phone","required":true,"in":"query","schema":{"type":"string"}},{"name":"text","required":true,"in":"query","schema":{"type":"string"}},{"name":"session","required":true,"in":"query","schema":{"default":"default","type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"object"}}}}},"tags":["chatting"],"security":[{"api_key":[]}]},"post":{"operationId":"ChattingController_sendText","summary":"Send a text message","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/MessageTextRequest"}}}},"responses":{"201":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/WAMessage"}}}}},"tags":["chatting"],"security":[{"api_key":[]}]}},"/api/sendTextButtons":{"post":{"operationId":"ChattingController_sendTextButtons","summary":"Send a text message with buttons","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/MessageTextButtonsRequest"}}}},"responses":{"201":{"description":"","content":{"application/json":{"schema":{"type":"object"}}}}},"tags":["chatting"],"security":[{"api_key":[]}]}},"/api/sendLocation":{"post":{"operationId":"ChattingController_sendLocation","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/MessageLocationRequest"}}}},"responses":{"201":{"description":"","content":{"application/json":{"schema":{"type":"object"}}}}},"tags":["chatting"],"security":[{"api_key":[]}]}},"/api/sendLinkPreview":{"post":{"operationId":"ChattingController_sendLinkPreview","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/MessageLinkPreviewRequest"}}}},"responses":{"201":{"description":"","content":{"application/json":{"schema":{"type":"object"}}}}},"tags":["chatting"],"security":[{"api_key":[]}]}},"/api/sendImage":{"post":{"operationId":"ChattingController_sendImage","summary":"Send an image. Either from an URL or base64 data - look at the request schemas for details.","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/MessageImageRequest"}}}},"responses":{"201":{"description":"","content":{"application/json":{"schema":{"type":"object"}}}}},"tags":["chatting"],"security":[{"api_key":[]}]}},"/api/sendFile":{"post":{"operationId":"ChattingControllLine truncated
+7 -4
View File
@@ -22,7 +22,7 @@
"test:e2e": "jest --config ./test/jest-e2e.json"
},
"dependencies": {
"@adiwajshing/baileys": "file:../WhiskeySockets-Baileys",
"@adiwajshing/baileys": "github:WhiskeySockets/Baileys",
"@adiwajshing/keyed-db": "^0.2.4",
"@nestjs/common": "^9.0.9",
"@nestjs/config": "^0.5.0",
@@ -37,6 +37,7 @@
"del": "^6.0.0",
"express-basic-auth": "^1.2.1",
"file-type": "16.5.4",
"https-proxy-agent": "^7.0.0",
"link-preview-js": "^3.0.4",
"lodash": "^4.17.21",
"mime-types": "^2.1.27",
@@ -50,7 +51,7 @@
"rimraf": "^3.0.2",
"rxjs": "^7.1.0",
"swagger-ui-express": "^4.1.4",
"venom-bot": "^5.0.1",
"venom-bot": "5.0.1",
"whatsapp-web.js": "https://github.com/pedroslopez/whatsapp-web.js"
},
"devDependencies": {
@@ -66,6 +67,7 @@
"eslint": "7.7.0",
"eslint-config-prettier": "^6.10.0",
"eslint-plugin-import": "^2.20.1",
"eslint-plugin-simple-import-sort": "^10.0.0",
"jest": "26.4.2",
"prettier": "^1.19.1",
"supertest": "^4.0.2",
@@ -73,7 +75,7 @@
"ts-loader": "^6.2.1",
"ts-node": "^10.9.1",
"tsconfig-paths": "^4.1.0",
"typescript": "4.7.4"
"typescript": "^4.8.4"
},
"jest": {
"moduleFileExtensions": [
@@ -88,5 +90,6 @@
},
"coverageDirectory": "../coverage",
"testEnvironment": "node"
}
},
"packageManager": "yarn@3.6.0"
}
+56
View File
@@ -0,0 +1,56 @@
import { Controller, Delete, Get, Param, Post, Query } from '@nestjs/common';
import { ApiOperation, ApiSecurity, ApiTags } from '@nestjs/swagger';
import { SessionManager } from '../core/abc/manager.abc';
import { WhatsappSession } from '../core/abc/session.abc';
import { SessionApiParam, SessionParam } from './helpers';
import { GetChatMessagesQuery } from '../structures/chats.dto';
import { parseBool } from '../helpers';
@ApiSecurity('api_key')
@Controller('api/:session/chats')
@ApiTags('chats')
class ChatsController {
constructor(private manager: SessionManager) {}
@Get('')
@SessionApiParam
@ApiOperation({ summary: 'Get all chats' })
getAllChats(@SessionParam session: WhatsappSession) {
return session.getChats();
}
@Delete(':chatId')
@SessionApiParam
@ApiOperation({ summary: 'Deletes the chat' })
deleteChat(
@SessionParam session: WhatsappSession,
@Param('chatId') chatId: string,
) {
return session.deleteChat(chatId);
}
@Get(':chatId/messages')
@SessionApiParam
@ApiOperation({ summary: 'Gets messages in the chat' })
getChatMessages(
@Query() query: GetChatMessagesQuery,
@SessionParam session: WhatsappSession,
@Param('chatId') chatId: string,
) {
const downloadMedia = parseBool(query.downloadMedia);
return session.getChatMessages(chatId, query.limit, downloadMedia);
}
@Delete(':chatId/messages')
@SessionApiParam
@ApiOperation({ summary: 'Clears all messages from the chat' })
clearMessages(
@SessionParam session: WhatsappSession,
@Param('chatId') chatId: string,
) {
return session.clearMessages(chatId);
}
}
export { ChatsController };
+2 -1
View File
@@ -1,5 +1,7 @@
import { Body, Controller, Get, Post, Put, Query } from '@nestjs/common';
import { ApiOperation, ApiSecurity, ApiTags } from '@nestjs/swagger';
import { SessionManager } from '../core/abc/manager.abc';
import {
ChatRequest,
CheckNumberStatusQuery,
@@ -18,7 +20,6 @@ import {
SendSeenRequest,
} from '../structures/chatting.dto';
import { WAMessage, WANumberExistResult } from '../structures/responses.dto';
import { SessionManager } from '../core/abc/manager.abc';
@ApiSecurity('api_key')
@Controller('api')
+2 -1
View File
@@ -1,9 +1,10 @@
import { Body, Controller, Get, Post, Query } from '@nestjs/common';
import { ApiOperation, ApiSecurity, ApiTags } from '@nestjs/swagger';
import { SessionManager } from '../core/abc/manager.abc';
import { ContactQuery, ContactRequest } from '../structures/contacts.dto';
import { SessionQuery } from '../structures/base.dto';
import { CheckNumberStatusQuery } from '../structures/chatting.dto';
import { ContactQuery, ContactRequest } from '../structures/contacts.dto';
import { WANumberExistResult } from '../structures/responses.dto';
@ApiSecurity('api_key')
+1
View File
@@ -6,6 +6,7 @@ import {
HttpStatus,
} from '@nestjs/common';
import { Request, Response } from 'express';
import { VERSION } from '../version';
/**
+2 -1
View File
@@ -8,14 +8,15 @@ import {
Put,
} from '@nestjs/common';
import { ApiOperation, ApiSecurity, ApiTags } from '@nestjs/swagger';
import { SessionManager } from '../core/abc/manager.abc';
import { WhatsappSession } from '../core/abc/session.abc';
import {
CreateGroupRequest,
DescriptionRequest,
ParticipantsRequest,
SubjectRequest,
} from '../structures/groups.dto';
import { WhatsappSession } from '../core/abc/session.abc';
import { SessionApiParam, SessionParam } from './helpers';
@ApiSecurity('api_key')
+2 -1
View File
@@ -1,5 +1,6 @@
import { ApiParam } from '@nestjs/swagger';
import { Injectable, Param, PipeTransform } from '@nestjs/common';
import { ApiParam } from '@nestjs/swagger';
import { SessionManager } from '../core/abc/manager.abc';
import { WhatsappSession } from '../core/abc/session.abc';
+89
View File
@@ -0,0 +1,89 @@
import {
BadRequestException,
Body,
Controller,
Get,
Param,
Post,
} from '@nestjs/common';
import { ApiOperation, ApiSecurity, ApiTags } from '@nestjs/swagger';
import { SessionManager } from '../core/abc/manager.abc';
import { WhatsappSession } from '../core/abc/session.abc';
import {
WAHAChatPresences,
WAHASessionPresence,
} from '../structures/presence.dto';
import { SessionApiParam, SessionParam } from './helpers';
import { WAHAPresenceStatus } from '../structures/enums.dto';
@ApiSecurity('api_key')
@Controller('api/:session/presence')
@ApiTags('presence')
export class PresenceController {
constructor(private manager: SessionManager) {}
@Post('')
@SessionApiParam
@ApiOperation({ summary: 'Set session presence' })
setPresence(
@SessionParam session: WhatsappSession,
@Body() request: WAHASessionPresence,
) {
// Validate request
const presencesWithoutChatId = [
WAHAPresenceStatus.ONLINE,
WAHAPresenceStatus.OFFLINE,
];
const requiresNoChatId = presencesWithoutChatId.includes(request.presence);
const requiresChatId = !requiresNoChatId;
if (requiresNoChatId && request.chatId) {
const msg = {
detail: `'${request.presence}' presence works on the global scope and doesn't require 'chatId' field.`,
};
throw new BadRequestException(msg);
} else if (requiresChatId && !request.chatId) {
const msg = {
detail: `'${request.presence}' presence requires 'chatId' field.`,
};
throw new BadRequestException(msg);
}
return session.setPresence(request.presence, request.chatId);
}
@Get('')
@SessionApiParam
@ApiOperation({ summary: 'Get all subscribed presence information.' })
getPresenceAll(
@SessionParam session: WhatsappSession,
): Promise<WAHAChatPresences[]> {
return session.getPresences();
}
@Get(':chatId')
@SessionApiParam
@ApiOperation({
summary:
"Get the presence for the chat id. If it hasn't been subscribed - it also subscribes to it.",
})
getPresence(
@SessionParam session: WhatsappSession,
@Param('chatId') chatId: string,
): Promise<WAHAChatPresences> {
return session.getPresence(chatId);
}
@Post(':chatId/subscribe')
@SessionApiParam
@ApiOperation({
summary: 'Subscribe to presence events for the chat.',
})
subscribe(
@SessionParam session: WhatsappSession,
@Param('chatId') chatId: string,
): Promise<void> {
return session.subscribePresence(chatId);
}
}
+2 -1
View File
@@ -1,7 +1,8 @@
import { Controller, Get, Query, Res } from '@nestjs/common';
import { ApiSecurity, ApiTags } from '@nestjs/swagger';
import { Readable } from 'stream';
import { Response } from 'express';
import { Readable } from 'stream';
import { SessionManager } from '../core/abc/manager.abc';
import { SessionQuery } from '../structures/base.dto';
+14 -14
View File
@@ -1,15 +1,16 @@
import { Body, Controller, Get, Post, Query } from '@nestjs/common';
import { UnprocessableEntityException } from '@nestjs/common/exceptions/unprocessable-entity.exception';
import { ApiOperation, ApiSecurity, ApiTags } from '@nestjs/swagger';
import { SessionManager } from '../core/abc/manager.abc';
import { parseBool } from '../helpers';
import {
SessionLogoutRequest,
ListSessionsQuery,
SessionDTO,
SessionLogoutRequest,
SessionStartRequest,
SessionStopRequest,
ListSessionsQuery,
} from '../structures/sessions.dto';
import { SessionManager } from '../core/abc/manager.abc';
import { UnprocessableEntityException } from '@nestjs/common/exceptions/unprocessable-entity.exception';
import { parseBool } from '../helpers';
@ApiSecurity('api_key')
@Controller('api/sessions')
@@ -18,8 +19,13 @@ export class SessionsController {
constructor(private manager: SessionManager) {}
@Post('/start/')
start(@Body() request: SessionStartRequest): SessionDTO {
return this.manager.start(request);
async start(@Body() request: SessionStartRequest): Promise<SessionDTO> {
const result = await this.manager.start(request);
await this.manager.sessionStorage.configRepository.save(
request.name,
request.config,
);
return result;
}
@Post('/stop/')
@@ -35,17 +41,11 @@ export class SessionsController {
@Post('/logout/')
@ApiOperation({ summary: 'Logout from session.' })
clean(@Body() request: SessionLogoutRequest): Promise<void> {
const session = this.manager.getSession(request.name, false);
if (session) {
throw new UnprocessableEntityException(
`Can not clean running session, please stop it first. You can set 'clean' field to 'True' in stop request so it stops and cleans at the same time.`,
);
}
return this.manager.logout(request);
}
@Get('/')
list(@Query() query: ListSessionsQuery): SessionDTO[] {
async list(@Query() query: ListSessionsQuery): Promise<SessionDTO[]> {
const all = parseBool(query.all);
return this.manager.getSessions(all);
}
+1
View File
@@ -1,5 +1,6 @@
import { Controller, Get } from '@nestjs/common';
import { ApiSecurity, ApiTags } from '@nestjs/swagger';
import { VERSION } from '../version';
@ApiSecurity('api_key')
+43 -7
View File
@@ -1,7 +1,9 @@
import { Injectable } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { WhatsappEngine } from './structures/enums.dto';
import { parseBool } from './helpers';
import { WAHAEngine } from './structures/enums.dto';
import { WebhookConfig } from './structures/webhooks.dto';
@Injectable()
export class WhatsappConfigService {
@@ -54,19 +56,53 @@ export class WhatsappConfigService {
return parseBool(value);
}
getWebhookUrl(): string | undefined {
get proxyServer(): string[] | string | undefined {
const single = this.configService.get('WHATSAPP_PROXY_SERVER', undefined);
const multipleValues = this.configService.get(
'WHATSAPP_PROXY_SERVER_LIST',
undefined,
);
const multiple = multipleValues ? multipleValues.split(',') : undefined;
return single ? single : multiple;
}
get proxyServerIndexPrefix(): string | undefined {
return this.configService.get(
'WHATSAPP_PROXY_SERVER_INDEX_PREFIX',
undefined,
);
}
get proxyServerUsername(): string | undefined {
return this.configService.get('WHATSAPP_PROXY_SERVER_USERNAME', undefined);
}
get proxyServerPassword(): string | undefined {
return this.configService.get('WHATSAPP_PROXY_SERVER_PASSWORD', undefined);
}
getWebhookConfig(): WebhookConfig | undefined {
const url = this.getWebhookUrl();
const events = this.getWebhookEvents();
if (!url || events.length === 0) {
return undefined;
}
return { url: url, events: events };
}
private getWebhookUrl(): string | undefined {
return this.get('WHATSAPP_HOOK_URL');
}
getWebhookEvents(): string[] {
private getWebhookEvents(): string[] {
const value = this.get('WHATSAPP_HOOK_EVENTS', '');
return value ? value.split(',') : [];
}
getDefaultEngineName(): WhatsappEngine {
const value = this.get('WHATSAPP_DEFAULT_ENGINE', WhatsappEngine.WEBJS);
if (value in WhatsappEngine) {
return WhatsappEngine[value];
getDefaultEngineName(): WAHAEngine {
const value = this.get('WHATSAPP_DEFAULT_ENGINE', WAHAEngine.WEBJS);
if (value in WAHAEngine) {
return WAHAEngine[value];
}
console.log(
`Unknown WhatsApp default engine, using WEBJS. WHATSAPP_DEFAULT_ENGINE=${value}`,
+9 -6
View File
@@ -1,17 +1,20 @@
import { OnApplicationShutdown } from '@nestjs/common';
import { WAHAEngine } from '../../structures/enums.dto';
import {
SessionLogoutRequest,
SessionDTO,
SessionLogoutRequest,
SessionStartRequest,
SessionStopRequest,
} from '../../structures/sessions.dto';
import { WhatsappSession } from './session.abc';
import { WhatsappEngine } from '../../structures/enums.dto';
import { MediaStorage } from './storage.abc';
import { LocalSessionStorage, MediaStorage } from './storage.abc';
import { WebhookConductor } from './webhooks.abc';
export abstract class SessionManager implements OnApplicationShutdown {
protected abstract getEngine(engine: WhatsappEngine): typeof WhatsappSession;
public sessionStorage: LocalSessionStorage;
protected abstract getEngine(engine: WAHAEngine): typeof WhatsappSession;
protected abstract get EngineClass(): typeof WhatsappSession;
@@ -24,7 +27,7 @@ export abstract class SessionManager implements OnApplicationShutdown {
//
// API Methods
//
abstract start(request: SessionStartRequest): SessionDTO;
abstract start(request: SessionStartRequest): Promise<SessionDTO>;
abstract stop(request: SessionStopRequest): Promise<void>;
@@ -32,5 +35,5 @@ export abstract class SessionManager implements OnApplicationShutdown {
abstract getSession(name: string, error?: boolean): WhatsappSession;
abstract getSessions(all: boolean): SessionDTO[];
abstract getSessions(all: boolean): Promise<SessionDTO[]>;
}
+77 -17
View File
@@ -1,9 +1,8 @@
import {
WAEvents,
WhatsappEngine,
WhatsappStatus,
} from '../../structures/enums.dto';
import { ConsoleLogger } from '@nestjs/common';
import { EventEmitter } from 'events';
import * as fs from 'fs';
import { MessageId } from 'whatsapp-web.js';
import {
ChatRequest,
CheckNumberStatusQuery,
@@ -19,19 +18,24 @@ import {
MessageTextRequest,
MessageVoiceRequest,
} from '../../structures/chatting.dto';
import { LocalSessionStorage, MediaStorage } from './storage.abc';
import { MessageId } from 'whatsapp-web.js';
import { ContactQuery, ContactRequest } from '../../structures/contacts.dto';
import { NotImplementedByEngineError } from '../exceptions';
import {
WAHAEngine,
WAHAEvents,
WAHAPresenceStatus,
WAHASessionStatus,
} from '../../structures/enums.dto';
import {
CreateGroupRequest,
ParticipantsRequest,
} from '../../structures/groups.dto';
import * as fs from 'fs';
import { WAHAChatPresences } from '../../structures/presence.dto';
import { ProxyConfig, SessionConfig } from '../../structures/sessions.dto';
import { NotImplementedByEngineError } from '../exceptions';
import { LocalSessionStorage, MediaStorage } from './storage.abc';
const CHROME_PATH = '/usr/bin/google-chrome-stable';
const CHROMIUM_PATH = '/usr/bin/chromium';
import { EventEmitter } from 'events';
export function getBrowserExecutablePath() {
if (fs.existsSync(CHROME_PATH)) {
@@ -39,6 +43,7 @@ export function getBrowserExecutablePath() {
}
return CHROMIUM_PATH;
}
export function ensureSuffix(phone) {
const suffix = '@c.us';
if (phone.includes('@')) {
@@ -51,29 +56,43 @@ export enum WAHAInternalEvent {
engine_start = 'engine.start',
}
export interface WhatsAppSessionConfig {
export interface SessionParams {
name: string;
storage: MediaStorage;
log: ConsoleLogger;
sessionStorage: LocalSessionStorage;
proxyConfig?: ProxyConfig;
sessionConfig: SessionConfig;
}
export abstract class WhatsappSession {
public status: WhatsappStatus;
public status: WAHASessionStatus;
public events: EventEmitter;
public engine: WhatsappEngine;
public engine: WAHAEngine;
public name: string;
protected storage: MediaStorage;
protected log: ConsoleLogger;
protected sessionStorage: LocalSessionStorage;
protected proxyConfig?: ProxyConfig;
public sessionConfig: SessionConfig;
public constructor({ name, log, sessionStorage }: WhatsAppSessionConfig) {
public constructor({
name,
log,
sessionStorage,
proxyConfig,
storage,
sessionConfig,
}: SessionParams) {
this.name = name;
this.status = WhatsappStatus.STARTING;
this.proxyConfig = proxyConfig;
this.status = WAHASessionStatus.STARTING;
this.log = log;
this.sessionStorage = sessionStorage;
this.events = new EventEmitter();
this.storage = storage;
this.sessionConfig = sessionConfig;
}
getBrowserExecutablePath() {
@@ -117,7 +136,7 @@ export abstract class WhatsappSession {
abstract stop(): void;
/** Subscribe the handler to specific hook */
abstract subscribe(hook: WAEvents | string, handler: (message) => void);
abstract subscribe(hook: WAHAEvents | string, handler: (message) => void);
/**
* START - Methods for API
@@ -150,10 +169,35 @@ export abstract class WhatsappSession {
abstract stopTyping(chat: ChatRequest);
abstract getMessages(query: GetMessageQuery);
getMessages(query: GetMessageQuery) {
throw new NotImplementedByEngineError();
}
abstract setReaction(request: MessageReactionRequest);
/**
* Chats methods
*/
public getChats() {
throw new NotImplementedByEngineError();
}
public deleteChat(chatId) {
throw new NotImplementedByEngineError();
}
public getChatMessages(
chatId: string,
limit: number,
downloadMedia: boolean,
) {
throw new NotImplementedByEngineError();
}
public clearMessages(chatId) {
throw new NotImplementedByEngineError();
}
/**
* Contacts methods
*/
@@ -240,6 +284,22 @@ export abstract class WhatsappSession {
throw new NotImplementedByEngineError();
}
public setPresence(presence: WAHAPresenceStatus, chatId?: string) {
throw new NotImplementedByEngineError();
}
public getPresences(): Promise<WAHAChatPresences[]> {
throw new NotImplementedByEngineError();
}
public getPresence(id: string): Promise<WAHAChatPresences> {
throw new NotImplementedByEngineError();
}
public subscribePresence(id: string): Promise<void> {
throw new NotImplementedByEngineError();
}
/**
* END - Methods for API
*/
+25 -8
View File
@@ -1,4 +1,6 @@
export abstract class MediaStorage {
import { SessionConfig } from '../../structures/sessions.dto';
abstract class MediaStorage {
abstract save(
messageId: string,
mimetype: string,
@@ -6,18 +8,33 @@ export abstract class MediaStorage {
): Promise<string>;
}
export abstract class LocalSessionStorage {
public sessionsFolder: string;
abstract class SessionConfigRepository {
abstract save(sessionName: string, config: SessionConfig): Promise<void>;
abstract get(sessionName: string): Promise<SessionConfig>;
}
abstract class SessionStorage {
protected constructor(public engine: string) {
this.engine = engine;
}
abstract init();
abstract init(sessionName?: string): Promise<void>;
abstract clean(sessionName: string): Promise<void>;
abstract getAll(): Promise<string[]>;
//
// Repositories
//
abstract get configRepository(): SessionConfigRepository;
}
abstract class LocalSessionStorage extends SessionStorage {
public sessionsFolder: string;
abstract getFolderPath(sessionName: string): string;
abstract clean(sessionName: string);
abstract getAll(): string[];
}
export { LocalSessionStorage, MediaStorage, SessionConfigRepository };
+16 -1
View File
@@ -1,5 +1,20 @@
import { ConsoleLogger } from '@nestjs/common';
import { WebhookConfig } from '../../structures/webhooks.dto';
import { WhatsappSession } from './session.abc';
export abstract class WebhookSender {
protected url: string;
constructor(
protected log: ConsoleLogger,
protected webhookConfig: WebhookConfig,
) {
this.url = webhookConfig.url;
}
abstract send(json);
}
export abstract class WebhookConductor {
abstract configure(session: WhatsappSession);
abstract configure(session: WhatsappSession, webhooks: WebhookConfig[]);
}
+10 -5
View File
@@ -1,16 +1,19 @@
import { ConsoleLogger, Module } from '@nestjs/common';
import { ScreenshotController } from '../api/screenshot.controller';
import { ConfigModule } from '@nestjs/config';
import { WhatsappConfigService } from '../config.service';
import { PassportModule } from '@nestjs/passport';
import { ServeStaticModule } from '@nestjs/serve-static';
import { SessionsController } from '../api/sessions.controller';
import { ChattingController } from '../api/chatting.controller';
import { ContactsController } from '../api/contacts.controller';
import { GroupsController } from '../api/groups.controller';
import { PresenceController } from '../api/presence.controller';
import { ScreenshotController } from '../api/screenshot.controller';
import { SessionsController } from '../api/sessions.controller';
import { VersionController } from '../api/version.controller';
import { PassportModule } from '@nestjs/passport';
import { WhatsappConfigService } from '../config.service';
import { SessionManager } from './abc/manager.abc';
import { SessionManagerCore } from './manager.core';
import { GroupsController } from '../api/groups.controller';
import { ChatsController } from '../api/chats.controller';
export const IMPORTS = [
ConfigModule.forRoot({
@@ -34,8 +37,10 @@ export const IMPORTS = [
export const CONTROLLERS = [
SessionsController,
ChattingController,
ChatsController,
ContactsController,
GroupsController,
PresenceController,
ScreenshotController,
VersionController,
];
+73
View File
@@ -0,0 +1,73 @@
import { Agent } from 'https';
import { HttpsProxyAgent } from 'https-proxy-agent';
import { URL } from 'url';
import { WhatsappConfigService } from '../config.service';
import { ProxyConfig } from '../structures/sessions.dto';
import { WhatsappSession } from './abc/session.abc';
export function getProxyConfig(
config: WhatsappConfigService,
sessions: Record<string, WhatsappSession>,
sessionName?: string,
): ProxyConfig | undefined {
// Single proxy server configuration
if (typeof config.proxyServer === 'string') {
return {
server: config.proxyServer,
username: config.proxyServerUsername,
password: config.proxyServerPassword,
};
}
// Multiple proxy server configuration
if (Array.isArray(config.proxyServer)) {
const prefix = config.proxyServerIndexPrefix;
let indexToUse: number = undefined;
if (prefix && sessionName.includes(prefix)) {
// match numbers at the end of the string
const matches = sessionName.match(/\d+$/);
indexToUse = matches ? parseInt(matches[0], 10) : undefined;
}
let idx = 0;
idx = Object.keys(sessions).length % config.proxyServer.length;
const index = indexToUse ? indexToUse % config.proxyServer.length : idx;
return {
server: config.proxyServer[index],
username: config.proxyServerUsername,
password: config.proxyServerPassword,
};
}
// No proxy server configuration
return undefined;
}
function getAuthenticatedUrl(
url: string,
username: string,
password: string,
): string {
const hasSchema = url.startsWith('http://') || url.startsWith('https://');
if (!hasSchema) {
url = `http://${url}`;
}
const parsedUrl = new URL(url);
parsedUrl.protocol = parsedUrl.protocol || 'http';
parsedUrl.username = username;
parsedUrl.password = password;
return parsedUrl.toString();
}
/**
* Return https Agent proxy for the config
* @param proxyConfig
*/
export function createAgentProxy(proxyConfig: ProxyConfig): Agent | undefined {
const url = getAuthenticatedUrl(
proxyConfig.server,
proxyConfig.username,
proxyConfig.password,
);
return new HttpsProxyAgent(url);
}
+83 -36
View File
@@ -4,27 +4,30 @@ import {
NotFoundException,
UnprocessableEntityException,
} from '@nestjs/common';
import { SessionManager } from './abc/manager.abc';
import { WhatsappConfigService } from '../config.service';
import { WAHAEngine, WAHASessionStatus } from '../structures/enums.dto';
import {
WAHAInternalEvent,
WhatsappSession,
WhatsAppSessionConfig,
} from './abc/session.abc';
import { WhatsappEngine, WhatsappStatus } from '../structures/enums.dto';
import {
SessionLogoutRequest,
ProxyConfig,
SessionDTO,
SessionLogoutRequest,
SessionStartRequest,
SessionStopRequest,
} from '../structures/sessions.dto';
import { WhatsappConfigService } from '../config.service';
import { WebhookConfig } from '../structures/webhooks.dto';
import { SessionManager } from './abc/manager.abc';
import {
SessionParams,
WAHAInternalEvent,
WhatsappSession,
} from './abc/session.abc';
import { DOCS_URL } from './exceptions';
import { getProxyConfig } from './helpers.proxy';
import { WhatsappSessionNoWebCore } from './session.noweb.core';
import { WhatsappSessionVenomCore } from './session.venom.core';
import { WhatsappSessionWebJSCore } from './session.webjs.core';
import { DOCS_URL } from './exceptions';
import { WebhookConductorCore } from './webhooks.core';
import { MediaStorageCore, SessionStorageCore } from './storage.core';
import { WhatsappSessionNoWebCore } from './session.noweb.core';
import { LocalSessionStorage } from './abc/storage.abc';
import { WebhookConductorCore } from './webhooks.core';
export class OnlyDefaultSessionIsAllowed extends UnprocessableEntityException {
constructor() {
@@ -44,7 +47,6 @@ export class SessionManagerCore extends SessionManager {
// @ts-ignore
protected WebhookConductorClass = WebhookConductorCore;
protected readonly EngineClass: typeof WhatsappSession;
protected sessionStorage: LocalSessionStorage;
constructor(
private config: WhatsappConfigService,
@@ -57,7 +59,6 @@ export class SessionManagerCore extends SessionManager {
const engineName = this.config.getDefaultEngineName();
this.EngineClass = this.getEngine(engineName);
this.sessionStorage = new SessionStorageCore(engineName.toLowerCase());
this.sessionStorage.init();
this.startPredefinedSessions();
}
@@ -69,12 +70,12 @@ export class SessionManagerCore extends SessionManager {
});
}
protected getEngine(engine: WhatsappEngine): typeof WhatsappSession {
if (engine === WhatsappEngine.WEBJS) {
protected getEngine(engine: WAHAEngine): typeof WhatsappSession {
if (engine === WAHAEngine.WEBJS) {
return WhatsappSessionWebJSCore;
} else if (engine === WhatsappEngine.VENOM) {
} else if (engine === WAHAEngine.VENOM) {
return WhatsappSessionVenomCore;
} else if (engine === WhatsappEngine.NOWEB) {
} else if (engine === WAHAEngine.NOWEB) {
return WhatsappSessionNoWebCore;
} else {
throw new NotFoundException(`Unknown whatsapp engine '${engine}'.`);
@@ -97,7 +98,7 @@ export class SessionManagerCore extends SessionManager {
//
// API Methods
//
start(request: SessionStartRequest): SessionDTO {
async start(request: SessionStartRequest): Promise<SessionDTO> {
this.onlyDefault(request.name);
const name = request.name;
@@ -105,27 +106,60 @@ export class SessionManagerCore extends SessionManager {
const log = new ConsoleLogger(`WhatsappSession - ${name}`);
const storage = new this.MediaStorageClass();
const webhookLog = new ConsoleLogger(`Webhook - ${name}`);
const webhook = new this.WebhookConductorClass(
webhookLog,
this.config.getWebhookUrl(),
this.config.getWebhookEvents(),
);
const sessionConfig: WhatsAppSessionConfig = {
const webhook = new this.WebhookConductorClass(webhookLog);
const proxyConfig = this.getProxyConfig(request);
const sessionConfig: SessionParams = {
name,
storage,
log,
sessionStorage: this.sessionStorage,
proxyConfig: proxyConfig,
sessionConfig: request.config,
};
await this.sessionStorage.init(name);
// @ts-ignore
const session = new this.EngineClass(sessionConfig);
this.session = session;
// configure webhooks
const webhooks = this.getWebhooks(request);
session.events.on(WAHAInternalEvent.engine_start, () =>
webhook.configure(session),
webhook.configure(session, webhooks),
);
session.start();
return { name: session.name, status: session.status };
// start session
await session.start();
return {
name: session.name,
status: session.status,
config: session.sessionConfig,
};
}
/**
* Combine per session and global webhooks
*/
private getWebhooks(request: SessionStartRequest) {
let webhooks: WebhookConfig[] = [];
if (request.config?.webhooks) {
webhooks = webhooks.concat(request.config.webhooks);
}
const globalWebhookConfig = this.config.getWebhookConfig();
webhooks.push(globalWebhookConfig);
return webhooks;
}
/**
* Get either session's or global proxy if defined
*/
protected getProxyConfig(
request: SessionStartRequest,
): ProxyConfig | undefined {
if (request.config?.proxy) {
return request.config.proxy;
}
const sessions = { [request.name]: this.session };
return getProxyConfig(this.config, sessions, request.name);
}
async stop(request: SessionStopRequest): Promise<void> {
@@ -140,7 +174,7 @@ export class SessionManagerCore extends SessionManager {
}
async logout(request: SessionLogoutRequest) {
this.sessionStorage.clean(request.name);
await this.sessionStorage.clean(request.name);
}
getSession(name: string, error = true): WhatsappSession {
@@ -157,12 +191,25 @@ export class SessionManagerCore extends SessionManager {
return session;
}
getSessions(all: boolean): SessionDTO[] {
async getSessions(all: boolean): Promise<SessionDTO[]> {
if (!this.session) {
return all
? [{ name: this.DEFAULT, status: WhatsappStatus.STOPPED }]
: [];
if (!all) {
return [];
}
return [
{
name: this.DEFAULT,
status: WAHASessionStatus.STOPPED,
config: undefined,
},
];
}
return [{ name: this.session.name, status: this.session.status }];
return [
{
name: this.session.name,
status: this.session.status,
config: this.session.sessionConfig,
},
];
}
}
+162 -56
View File
@@ -1,22 +1,18 @@
import {
SECOND,
WAEvents,
WhatsappEngine,
WhatsappStatus,
} from '../structures/enums.dto';
import makeWASocket, {
DisconnectReason,
isJidGroup,
makeInMemoryStore,
PresenceData,
useMultiFileAuthState,
} from '@adiwajshing/baileys';
import { WABrowserDescription } from '@adiwajshing/baileys';
import { UnprocessableEntityException } from '@nestjs/common';
import * as fs from 'fs/promises';
import { Agent } from 'https';
import * as lodash from 'lodash';
import { Message } from 'whatsapp-web.js';
import {
ensureSuffix,
WAHAInternalEvent,
WhatsappSession,
} from './abc/session.abc';
import { WAMessage, WANumberExistResult } from '../structures/responses.dto';
import { flipObject } from '../helpers';
import {
Button,
ChatRequest,
@@ -34,22 +30,34 @@ import {
MessageVoiceRequest,
SendSeenRequest,
} from '../structures/chatting.dto';
import {
AvailableInPlusVersion,
NotImplementedByEngineError,
} from './exceptions';
import { ContactQuery, ContactRequest } from '../structures/contacts.dto';
import {
SECOND,
WAHAEngine,
WAHAEvents,
WAHAPresenceStatus,
WAHASessionStatus,
} from '../structures/enums.dto';
import {
CreateGroupRequest,
ParticipantsRequest,
} from '../structures/groups.dto';
import {
WAHAChatPresences,
WAHAPresenceData,
} from '../structures/presence.dto';
import { WAMessage, WANumberExistResult } from '../structures/responses.dto';
import {
ensureSuffix,
WAHAInternalEvent,
WhatsappSession,
} from './abc/session.abc';
import {
AvailableInPlusVersion,
NotImplementedByEngineError,
} from './exceptions';
import { createAgentProxy } from './helpers.proxy';
import { QR } from './QR';
import { MediaStorage } from './abc/storage.abc';
import { ConsoleLogger, UnprocessableEntityException } from '@nestjs/common';
import { Message } from 'whatsapp-web.js';
import * as fs from 'fs';
import * as path from 'path';
import * as os from 'os';
// eslint-disable-next-line @typescript-eslint/no-var-requires
const QRCode = require('qrcode');
@@ -61,43 +69,64 @@ export const BaileysEvents = {
CREDS_UPDATE: 'creds.update',
MESSAGES_UPSERT: 'messages.upsert',
GROUPS_UPSERT: 'groups.upsert',
PRESENCE_UPDATE: 'presence.update',
};
const PresenceStatuses = {
unavailable: WAHAPresenceStatus.OFFLINE,
available: WAHAPresenceStatus.ONLINE,
composing: WAHAPresenceStatus.TYPING,
recording: WAHAPresenceStatus.RECORDING,
paused: WAHAPresenceStatus.PAUSED,
};
const ToEnginePresenceStatus = flipObject(PresenceStatuses);
export class WhatsappSessionNoWebCore extends WhatsappSession {
engine = WhatsappEngine.NOWEB;
engine = WAHAEngine.NOWEB;
sock: any;
store: any;
private qr: QR;
authFolder: string;
public constructor(config) {
super(config);
this.qr = new QR();
this.authFolder = this.getAuthFolder();
}
start() {
return this.buildClient();
}
protected getAuthFolder() {
const folder = this.sessionStorage.getFolderPath(this.name);
return fs.mkdtempSync(folder);
getSocketConfig(agent, state) {
return {
agent: agent,
fetchAgent: agent,
auth: state,
printQRInTerminal: true,
browser: ['Linux', 'Chrome', '111.0.5563.64'] as WABrowserDescription,
logger: logger,
mobile: false,
};
}
async makeSocket() {
const { state, saveCreds } = await useMultiFileAuthState(this.authFolder);
const sock: any = makeWASocket({
auth: state,
printQRInTerminal: true,
browser: ['Linux', 'Chrome', '111.0.5563.64'],
logger: logger,
});
const authFolder = this.sessionStorage.getFolderPath(this.name);
await fs.mkdir(authFolder, { recursive: true });
const { state, saveCreds } = await useMultiFileAuthState(authFolder);
const agent = this.makeAgent();
const socketConfig = this.getSocketConfig(agent, state);
const sock: any = makeWASocket(socketConfig);
sock.ev.on(BaileysEvents.CREDS_UPDATE, saveCreds);
return sock;
}
protected makeAgent(): Agent {
if (!this.proxyConfig) {
return undefined;
}
return createAgentProxy(this.proxyConfig);
}
connectStore() {
if (!this.store) {
this.store = makeInMemoryStore({});
@@ -105,6 +134,12 @@ export class WhatsappSessionNoWebCore extends WhatsappSession {
this.store.bind(this.sock.ev);
}
resubscribeToKnownPresences() {
for (const jid in this.store.presences) {
this.sock.presenceSubscribe(jid);
}
}
async buildClient() {
this.sock = await this.makeSocket();
this.connectStore();
@@ -112,7 +147,8 @@ export class WhatsappSessionNoWebCore extends WhatsappSession {
const { connection, lastDisconnect, qr } = update;
if (connection === 'connecting') {
this.qr.save('');
this.status = WhatsappStatus.WORKING;
this.status = WAHASessionStatus.WORKING;
this.resubscribeToKnownPresences();
return;
} else if (connection === 'close') {
const shouldReconnect =
@@ -125,7 +161,7 @@ export class WhatsappSessionNoWebCore extends WhatsappSession {
shouldReconnect,
);
this.qr.save('');
this.status = WhatsappStatus.FAILED;
this.status = WAHASessionStatus.FAILED;
// reconnect if not logged out
if (shouldReconnect) {
setTimeout(() => this.buildClient(), 2 * SECOND);
@@ -134,7 +170,7 @@ export class WhatsappSessionNoWebCore extends WhatsappSession {
// Save QR
if (qr) {
this.status = WhatsappStatus.SCAN_QR_CODE;
this.status = WAHASessionStatus.SCAN_QR_CODE;
QRCode.toDataURL(qr).then((url) => {
this.qr.save(url);
});
@@ -144,8 +180,9 @@ export class WhatsappSessionNoWebCore extends WhatsappSession {
}
stop() {
this.sock.ev.removeAllListeners();
this.sock.ws.removeAllListeners();
this.log.log('socket connection terminated');
this.sock.ws.close();
return;
}
@@ -153,13 +190,13 @@ export class WhatsappSessionNoWebCore extends WhatsappSession {
* START - Methods for API
*/
async getScreenshot(): Promise<Buffer | string> {
if (this.status === WhatsappStatus.STARTING) {
if (this.status === WAHASessionStatus.STARTING) {
throw new UnprocessableEntityException(
`The session is starting, please try again after few seconds`,
);
} else if (this.status === WhatsappStatus.SCAN_QR_CODE) {
} else if (this.status === WAHASessionStatus.SCAN_QR_CODE) {
return Promise.resolve(this.qr.get());
} else if (this.status === WhatsappStatus.WORKING) {
} else if (this.status === WAHASessionStatus.WORKING) {
throw new UnprocessableEntityException(
`Can not get screenshot for non chrome based engine.`,
);
@@ -181,7 +218,11 @@ export class WhatsappSessionNoWebCore extends WhatsappSession {
sendText(request: MessageTextRequest) {
const chatId = this.ensureSuffix(request.chatId);
return this.sock.sendMessage(chatId, { text: request.text });
const message = {
text: request.text,
mentions: request.mentions?.map(toJID),
};
return this.sock.sendMessage(chatId, message);
}
sendTextButtons(request: MessageTextButtonsRequest) {
@@ -208,12 +249,17 @@ export class WhatsappSessionNoWebCore extends WhatsappSession {
async reply(request: MessageReplyRequest) {
const { id } = parseMessageId(request.reply_to);
const message = await this.store.loadMessage(toJID(request.chatId), id);
return await this.sock.sendMessage(
request.chatId,
{ text: request.text },
{ quoted: message },
const quotedMessage = await this.store.loadMessage(
toJID(request.chatId),
id,
);
const message = {
text: request.text,
mentions: request.mentions?.map(toJID),
};
return await this.sock.sendMessage(request.chatId, message, {
quoted: quotedMessage,
});
}
sendImage(request: MessageImageRequest) {
@@ -261,10 +307,6 @@ export class WhatsappSessionNoWebCore extends WhatsappSession {
return this.sock.sendPresenceUpdate('paused', request.chatId);
}
async getMessages(query: GetMessageQuery) {
throw new NotImplementedByEngineError();
}
async setReaction(request: MessageReactionRequest) {
const key = parseMessageId(request.messageId);
const reactionMessage = {
@@ -370,23 +412,63 @@ export class WhatsappSessionNoWebCore extends WhatsappSession {
return this.sock.groupParticipantsUpdate(id, participants, 'demote');
}
public async setPresence(presence: WAHAPresenceStatus, chatId?: string) {
const enginePresence = ToEnginePresenceStatus[presence];
if (!enginePresence) {
throw new NotImplementedByEngineError(
`NOWEB engine doesn't support '${presence}' presence.`,
);
}
await this.sock.sendPresenceUpdate(enginePresence, chatId);
}
public async getPresences(): Promise<WAHAChatPresences[]> {
const result: WAHAChatPresences[] = [];
for (const remoteJid in this.store.presences) {
const storedPresences = this.store.presences[remoteJid];
result.push(this.toWahaPresences(remoteJid, storedPresences));
}
return result;
}
public async getPresence(chatId: string): Promise<WAHAChatPresences> {
await this.subscribePresence(chatId);
const remoteJid = toJID(chatId);
const storedPresences = this.store.presences[remoteJid];
return this.toWahaPresences(remoteJid, storedPresences || []);
}
public async subscribePresence(chatId: string): Promise<void> {
const remoteJid = toJID(chatId);
if (this.store.presences[remoteJid]) {
return;
}
// Have no info - subscribe to listen for events
await this.sock.presenceSubscribe(remoteJid);
return;
}
/**
* END - Methods for API
*/
subscribe(event, handler) {
if (event === WAEvents.MESSAGE) {
if (event === WAHAEvents.MESSAGE) {
return this.sock.ev.on(BaileysEvents.MESSAGES_UPSERT, ({ messages }) =>
this.handleIncomingMessages(messages, handler, false),
);
} else if (event === WAEvents.MESSAGE_ANY) {
} else if (event === WAHAEvents.MESSAGE_ANY) {
return this.sock.ev.on(BaileysEvents.MESSAGES_UPSERT, ({ messages }) =>
this.handleIncomingMessages(messages, handler, true),
);
} else if (event === WAEvents.STATE_CHANGE) {
} else if (event === WAHAEvents.STATE_CHANGE) {
return this.sock.ev.on(BaileysEvents.CONNECTION_UPDATE, handler);
} else if (event === WAEvents.GROUP_JOIN) {
} else if (event === WAHAEvents.GROUP_JOIN) {
return this.sock.ev.on(BaileysEvents.GROUPS_UPSERT, handler);
} else if (event === WAHAEvents.PRESENCE_UPDATE) {
return this.sock.ev.on(BaileysEvents.PRESENCE_UPDATE, (data) =>
handler(this.toWahaPresences(data.id, data.presences)),
);
} else {
throw new NotImplementedByEngineError(
`Engine does not support webhook event: ${event}`,
@@ -447,6 +529,30 @@ export class WhatsappSessionNoWebCore extends WhatsappSession {
});
}
private toWahaPresences(
remoteJid,
storedPresences: PresenceData[],
): WAHAChatPresences {
const presences: WAHAPresenceData[] = [];
for (const participant in storedPresences) {
const data: PresenceData = storedPresences[participant];
const lastKnownPresence = lodash.get(
PresenceStatuses,
data.lastKnownPresence,
data.lastKnownPresence,
);
const presence: WAHAPresenceData = {
participant: toCusFormat(participant),
// @ts-ignore
lastKnownPresence: lastKnownPresence,
lastSeen: data.lastSeen || null,
};
presences.push(presence);
}
const chatId = toCusFormat(remoteJid);
return { id: chatId, presences: presences };
}
protected async downloadMedia(message: Message) {
if (!message.hasMedia) {
return message;
+45 -25
View File
@@ -1,4 +1,6 @@
import { WAHAInternalEvent, WhatsappSession } from './abc/session.abc';
import { UnprocessableEntityException } from '@nestjs/common/exceptions/unprocessable-entity.exception';
import { create, CreateConfig, Message, Whatsapp } from 'venom-bot';
import {
Button,
ChatRequest,
@@ -14,19 +16,18 @@ import {
MessageTextButtonsRequest,
MessageTextRequest,
} from '../structures/chatting.dto';
import { WAMessage, WANumberExistResult } from '../structures/responses.dto';
import { create, Message, Whatsapp } from 'venom-bot';
import {
WAEvents,
WhatsappEngine,
WhatsappStatus,
WAHAEngine,
WAHAEvents,
WAHASessionStatus,
} from '../structures/enums.dto';
import { WAMessage, WANumberExistResult } from '../structures/responses.dto';
import { WAHAInternalEvent, WhatsappSession } from './abc/session.abc';
import { NotImplementedByEngineError } from './exceptions';
import { UnprocessableEntityException } from '@nestjs/common/exceptions/unprocessable-entity.exception';
import { QR } from './QR';
export class WhatsappSessionVenomCore extends WhatsappSession {
engine = WhatsappEngine.NOWEB;
engine = WAHAEngine.VENOM;
whatsapp: Whatsapp;
private qr: QR;
@@ -37,7 +38,7 @@ export class WhatsappSessionVenomCore extends WhatsappSession {
}
protected buildClient() {
return create(this.name, this.getCatchQR(), undefined, {
const venomOptions: CreateConfig = {
headless: true,
devtools: false,
debug: false,
@@ -45,13 +46,28 @@ export class WhatsappSessionVenomCore extends WhatsappSession {
browserArgs: this.getBrowserArgsForPuppeteer(),
autoClose: 60000,
puppeteerOptions: {},
});
};
this.addProxyConfig(venomOptions);
return create(this.name, this.getCatchQR(), undefined, venomOptions);
}
protected addProxyConfig(venomOptions: CreateConfig) {
if (this.proxyConfig?.server !== undefined) {
venomOptions.addProxy = [this.proxyConfig?.server];
}
if (
this.proxyConfig?.username !== undefined &&
this.proxyConfig?.password !== undefined
) {
venomOptions.userProxy = this.proxyConfig?.username;
venomOptions.userPass = this.proxyConfig?.password;
}
}
protected getCatchQR() {
return (base64Qrimg, asciiQR, attempts, urlCode) => {
this.qr.save(base64Qrimg);
this.status = WhatsappStatus.SCAN_QR_CODE;
this.status = WAHASessionStatus.SCAN_QR_CODE;
this.log.debug('Number of attempts to read the qrcode: ', attempts);
this.log.log('Terminal qrcode:');
// Log QR image in console without this.log to make it pretty
@@ -63,13 +79,13 @@ export class WhatsappSessionVenomCore extends WhatsappSession {
try {
this.whatsapp = await this.buildClient();
} catch (error) {
this.status = WhatsappStatus.FAILED;
this.status = WAHASessionStatus.FAILED;
this.log.error(error);
this.qr.save('');
return;
}
this.status = WhatsappStatus.WORKING;
this.status = WAHASessionStatus.WORKING;
this.events.emit(WAHAInternalEvent.engine_start);
return this;
}
@@ -78,20 +94,20 @@ export class WhatsappSessionVenomCore extends WhatsappSession {
return this.whatsapp.close();
}
subscribe(event: WAEvents | string, handler: (message) => void) {
if (event === WAEvents.MESSAGE) {
subscribe(event: WAHAEvents | string, handler: (message) => void) {
if (event === WAHAEvents.MESSAGE) {
return this.whatsapp.onMessage((message: Message) =>
this.processIncomingMessage(message).then(handler),
);
} else if (event === WAEvents.MESSAGE_ANY) {
} else if (event === WAHAEvents.MESSAGE_ANY) {
return this.whatsapp.onAnyMessage((message: Message) =>
this.processIncomingMessage(message).then(handler),
);
} else if (event === WAEvents.STATE_CHANGE) {
} else if (event === WAHAEvents.STATE_CHANGE) {
return this.whatsapp.onStateChange(handler);
} else if (event === WAEvents.MESSAGE_ACK) {
} else if (event === WAHAEvents.MESSAGE_ACK) {
return this.whatsapp.onAck(handler);
} else if (event === WAEvents.GROUP_JOIN) {
} else if (event === WAHAEvents.GROUP_JOIN) {
return this.whatsapp.onAddedToGroup(handler);
} else {
throw new NotImplementedByEngineError(
@@ -104,13 +120,13 @@ export class WhatsappSessionVenomCore extends WhatsappSession {
* START - Methods for API
*/
getScreenshot(): Promise<Buffer | string> {
if (this.status === WhatsappStatus.STARTING) {
if (this.status === WAHASessionStatus.STARTING) {
throw new UnprocessableEntityException(
`The session is starting, please try again after few seconds`,
);
} else if (this.status === WhatsappStatus.SCAN_QR_CODE) {
} else if (this.status === WAHASessionStatus.SCAN_QR_CODE) {
return Promise.resolve(this.qr.get());
} else if (this.status === WhatsappStatus.WORKING) {
} else if (this.status === WAHASessionStatus.WORKING) {
return this.whatsapp.page.screenshot();
} else {
throw new UnprocessableEntityException(`Unknown status - ${this.status}`);
@@ -212,16 +228,16 @@ export class WhatsappSessionVenomCore extends WhatsappSession {
// return this.whatsapp.stopTyping(chat.chatId);
}
async getMessages(query: GetMessageQuery) {
async getChatMessages(chatId: string, limit: number) {
const messages = await this.whatsapp.getAllMessagesInChat(
query.chatId,
chatId,
true,
false,
);
// Go over messages, download media, and convert to right format.
const result = [];
for (const [count, message] of messages.entries()) {
if (count > query.limit) {
if (count > limit) {
// Have enough in the list, stop processing
break;
}
@@ -230,6 +246,10 @@ export class WhatsappSessionVenomCore extends WhatsappSession {
return result;
}
async getMessages(query: GetMessageQuery) {
return this.getChatMessages(query.chatId, query.limit);
}
setReaction(request: MessageReactionRequest) {
throw new NotImplementedByEngineError();
}
+125 -38
View File
@@ -3,6 +3,7 @@ import {
Buttons,
Chat,
Client,
ClientOptions,
Contact,
Events,
GroupChat,
@@ -10,13 +11,7 @@ import {
Message,
} from 'whatsapp-web.js';
import { Message as MessageInstance } from 'whatsapp-web.js/src/structures';
import {
WAEvents,
WhatsappEngine,
WhatsappStatus,
} from '../structures/enums.dto';
import { WAHAInternalEvent, WhatsappSession } from './abc/session.abc';
import { WAMessage, WANumberExistResult } from '../structures/responses.dto';
import {
Button,
ChatRequest,
@@ -34,39 +29,66 @@ import {
MessageVoiceRequest,
SendSeenRequest,
} from '../structures/chatting.dto';
import {
AvailableInPlusVersion,
NotImplementedByEngineError,
} from './exceptions';
import { ContactQuery, ContactRequest } from '../structures/contacts.dto';
import {
WAHAEngine,
WAHAEvents,
WAHAPresenceStatus,
WAHASessionStatus,
} from '../structures/enums.dto';
import {
CreateGroupRequest,
ParticipantsRequest,
} from '../structures/groups.dto';
import { WAMessage, WANumberExistResult } from '../structures/responses.dto';
import { WAHAInternalEvent, WhatsappSession } from './abc/session.abc';
import {
AvailableInPlusVersion,
NotImplementedByEngineError,
} from './exceptions';
// eslint-disable-next-line @typescript-eslint/no-var-requires
const qrcode = require('qrcode-terminal');
export class WhatsappSessionWebJSCore extends WhatsappSession {
engine = WhatsappEngine.NOWEB;
engine = WAHAEngine.WEBJS;
whatsapp: Client;
protected buildClient() {
return new Client({
const clientOptions: ClientOptions = {
puppeteer: {
headless: true,
executablePath: this.getBrowserExecutablePath(),
args: this.getBrowserArgsForPuppeteer(),
},
});
};
this.addProxyConfig(clientOptions);
return new Client(clientOptions);
}
protected addProxyConfig(clientOptions: ClientOptions) {
if (this.proxyConfig?.server !== undefined) {
// push the proxy server to the args
clientOptions.puppeteer.args.push(
`--proxy-server=${this.proxyConfig?.server}`,
);
// Authenticate
if (this.proxyConfig?.username && this.proxyConfig?.password) {
clientOptions.proxyAuthentication = {
username: this.proxyConfig?.username,
password: this.proxyConfig?.password,
};
}
}
}
async start() {
this.whatsapp = this.buildClient();
this.whatsapp.initialize().catch((error) => {
this.status = WhatsappStatus.FAILED;
this.status = WAHASessionStatus.FAILED;
this.log.error(error);
return;
});
@@ -74,11 +96,11 @@ export class WhatsappSessionWebJSCore extends WhatsappSession {
// Connect events
this.whatsapp.on(Events.QR_RECEIVED, (qr) => {
qrcode.generate(qr, { small: true });
this.status = WhatsappStatus.SCAN_QR_CODE;
this.status = WAHASessionStatus.SCAN_QR_CODE;
});
this.whatsapp.on(Events.AUTHENTICATED, () => {
this.status = WhatsappStatus.WORKING;
this.status = WAHASessionStatus.WORKING;
this.log.log(`Session '${this.name}' has been authenticated!`);
});
this.events.emit(WAHAInternalEvent.engine_start);
@@ -93,7 +115,7 @@ export class WhatsappSessionWebJSCore extends WhatsappSession {
* START - Methods for API
*/
async getScreenshot(): Promise<Buffer | string> {
if (this.status === WhatsappStatus.FAILED) {
if (this.status === WAHASessionStatus.FAILED) {
throw new UnprocessableEntityException(
`The session under FAILED status. Please try to restart it.`,
);
@@ -111,9 +133,14 @@ 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 Contact[],
};
return this.whatsapp.sendMessage(
this.ensureSuffix(request.chatId),
request.text,
options,
);
}
@@ -138,6 +165,8 @@ 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 Contact[],
};
return this.whatsapp.sendMessage(request.chatId, request.text, options);
}
@@ -183,18 +212,7 @@ export class WhatsappSessionWebJSCore extends WhatsappSession {
}
async getMessages(query: GetMessageQuery) {
const chat: Chat = await this.whatsapp.getChatById(
this.ensureSuffix(query.chatId),
);
const messages = await chat.fetchMessages({
limit: query.limit,
});
// Go over messages, download media, and convert to right format.
const result = [];
for (const message of messages) {
result.push(await this.processIncomingMessage(message));
}
return result;
return this.getChatMessages(query.chatId, query.limit, query.downloadMedia);
}
async setReaction(request: MessageReactionRequest) {
@@ -207,6 +225,39 @@ export class WhatsappSessionWebJSCore extends WhatsappSession {
return message.react(request.reaction);
}
/**
* Chats methods
*/
getChats() {
return this.whatsapp.getChats();
}
async getChatMessages(chatId: string, limit: number, downloadMedia: boolean) {
const chat: Chat = await this.whatsapp.getChatById(
this.ensureSuffix(chatId),
);
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);
}
return result;
}
async deleteChat(chatId) {
const chat = await this.whatsapp.getChatById(chatId);
return chat.delete();
}
async clearMessages(chatId) {
const chat = await this.whatsapp.getChatById(chatId);
return chat.clearMessages();
}
/**
* Contacts methods
*/
@@ -338,29 +389,57 @@ export class WhatsappSessionWebJSCore extends WhatsappSession {
return groupChat.demoteParticipants(participantIds);
}
public async setPresence(presence: WAHAPresenceStatus, chatId?: string) {
let chat: Chat;
switch (presence) {
case WAHAPresenceStatus.ONLINE:
await this.whatsapp.sendPresenceAvailable();
break;
case WAHAPresenceStatus.OFFLINE:
await this.whatsapp.sendPresenceUnavailable();
break;
case WAHAPresenceStatus.TYPING:
chat = await this.whatsapp.getChatById(chatId);
await chat.sendStateTyping();
break;
case WAHAPresenceStatus.RECORDING:
chat = await this.whatsapp.getChatById(chatId);
await chat.sendStateRecording();
break;
case WAHAPresenceStatus.PAUSED:
chat = await this.whatsapp.getChatById(chatId);
await chat.clearState();
break;
default:
throw new NotImplementedByEngineError(
`WEBJS engine doesn't support '${presence}' presence.`,
);
}
}
/**
* END - Methods for API
*/
subscribe(event, handler) {
if (event === WAEvents.MESSAGE) {
if (event === WAHAEvents.MESSAGE) {
this.whatsapp.on(Events.MESSAGE_RECEIVED, (message) =>
this.processIncomingMessage(message).then(handler),
);
} else if (event === WAEvents.MESSAGE_ANY) {
} else if (event === WAHAEvents.MESSAGE_ANY) {
this.whatsapp.on(Events.MESSAGE_CREATE, (message) =>
this.processIncomingMessage(message).then(handler),
);
} else if (event === WAEvents.STATE_CHANGE) {
} else if (event === WAHAEvents.STATE_CHANGE) {
this.whatsapp.on(Events.STATE_CHANGED, handler);
} else if (event === WAEvents.MESSAGE_ACK) {
} else if (event === WAHAEvents.MESSAGE_ACK) {
// We do not download media here
this.whatsapp.on(Events.MESSAGE_ACK, (message) =>
this.toWAMessage(message).then(handler),
);
} else if (event === WAEvents.GROUP_JOIN) {
} else if (event === WAHAEvents.GROUP_JOIN) {
this.whatsapp.on(Events.GROUP_JOIN, handler);
} else if (event === WAEvents.GROUP_LEAVE) {
} else if (event === WAHAEvents.GROUP_LEAVE) {
this.whatsapp.on(Events.GROUP_LEAVE, handler);
} else {
throw new NotImplementedByEngineError(
@@ -369,8 +448,16 @@ export class WhatsappSessionWebJSCore extends WhatsappSession {
}
}
private processIncomingMessage(message: Message) {
return this.downloadMedia(message).then(this.toWAMessage);
private async processIncomingMessage(message: Message, downloadMedia = true) {
if (downloadMedia) {
try {
message = await this.downloadMedia(message);
} catch (e) {
this.log.error('Failed when tried to download media for a message');
this.log.error(e, e.stack);
}
}
return await this.toWAMessage(message);
}
protected toWAMessage(message: Message): Promise<WAMessage> {
+63 -13
View File
@@ -1,8 +1,15 @@
import { DOCS_URL } from './exceptions';
import { LocalSessionStorage, MediaStorage } from './abc/storage.abc';
import * as path from 'path';
import * as fs from 'fs';
import * as crypto from 'crypto';
import * as fs from 'fs/promises';
import * as os from 'os';
import * as path from 'path';
import { SessionConfig } from '../structures/sessions.dto';
import {
LocalSessionStorage,
MediaStorage,
SessionConfigRepository,
} from './abc/storage.abc';
import { DOCS_URL } from './exceptions';
export class MediaStorageCore implements MediaStorage {
async save(
@@ -16,35 +23,78 @@ export class MediaStorageCore implements MediaStorage {
}
}
class LocalSessionConfigRepository extends SessionConfigRepository {
FILENAME = '.waha.session.config.json';
constructor(private storage: LocalSessionStorage) {
super();
}
async get(sessionName: string): Promise<SessionConfig> {
const filepath = this.getFilePath(sessionName);
// Check file exists
try {
await fs.access(filepath, fs.constants.F_OK);
} catch (error) {
return undefined;
}
const content = await fs.readFile(filepath, 'utf-8');
return JSON.parse(content);
}
async save(sessionName: string, config: SessionConfig) {
const filepath = this.getFilePath(sessionName);
const content = JSON.stringify(config || null);
await fs.writeFile(filepath, content);
}
private getFilePath(sessionName): string {
const folder = this.storage.getFolderPath(sessionName);
return path.join(folder, this.FILENAME);
}
}
export class SessionStorageCore extends LocalSessionStorage {
public sessionsFolder: string;
constructor(engine: string) {
super(engine);
this.sessionsFolder = path.join(os.tmpdir(), 'waha-');
this.sessionsFolder = path.join(os.tmpdir(), 'waha-sessions');
}
get engineFolder() {
return path.join(this.sessionsFolder, this.engine);
}
init() {
fs.mkdirSync(this.engineFolder, { recursive: true });
async init(sessionName?: string) {
const folder = sessionName
? this.getFolderPath(sessionName)
: this.engineFolder;
await fs.mkdir(folder, { recursive: true });
}
getFolderPath(name: string): string {
return path.join(this.engineFolder, name);
const suffix = crypto.createHash('md5').update(name).digest('hex');
return path.join(this.engineFolder, `${name}-${suffix}`);
}
clean(sessionName: string) {
async clean(sessionName: string) {
const folder = this.getFolderPath(sessionName);
fs.rmSync(folder, { recursive: true });
await fs.rm(folder, { recursive: true });
}
getAll(): string[] {
return fs
.readdirSync(this.engineFolder, { withFileTypes: true })
async getAll(): Promise<string[]> {
await this.init();
const content = await fs.readdir(this.engineFolder, {
withFileTypes: true,
});
return content
.filter((dirent) => dirent.isDirectory())
.map((dirent) => dirent.name);
}
get configRepository(): LocalSessionConfigRepository {
return new LocalSessionConfigRepository(this);
}
}
+10 -5
View File
@@ -1,7 +1,8 @@
import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger';
import { INestApplication } from '@nestjs/common';
import { VERSION } from '../version';
import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger';
import { WhatsappConfigService } from '../config.service';
import { VERSION } from '../version';
export class SwaggerModuleCore {
configure(app: INestApplication) {
@@ -10,12 +11,13 @@ export class SwaggerModuleCore {
builder
.setTitle('WAHA - WhatsApp HTTP API')
.setDescription('WhatsApp HTTP API that you can configure in a click!')
.setDescription('WhatsApp HTTP API that you can run in a click!')
.setExternalDoc('Documentation', 'https://waha.devlike.pro/')
.setVersion(VERSION.version)
.addTag('sessions', 'Control WhatsApp sessions')
.addTag('screenshot', 'Get screenshot of WhatsApp and show QR code')
.addTag('chatting', 'Chat methods')
.addTag('chatting', 'Chatting methods')
.addTag('chats', `Chats methods`)
.addTag(
'contacts',
`Contacts methods.<br>
@@ -23,6 +25,7 @@ export class SwaggerModuleCore {
'E.g: \`12312312310\` OR \`12312312310@c.us\`<br>`,
)
.addTag('groups', `Groups methods.<br>`)
.addTag('presence', `Presence information`)
.addTag('other', 'Other methods')
.addApiKey({
type: 'apiKey',
@@ -57,7 +60,9 @@ export class SwaggerModuleCore {
const options = builder.build();
const document = SwaggerModule.createDocument(app, options);
SwaggerModule.setup('', app, document);
SwaggerModule.setup('', app, document, {
customSiteTitle: 'WAHA - WhatsApp HTTP API',
});
}
protected setUpAuth(app: INestApplication) {
+57 -33
View File
@@ -1,24 +1,40 @@
import { WAHAInternalEvent, WhatsappSession } from './abc/session.abc';
import { ConsoleLogger } from '@nestjs/common';
import { WAEvents } from '../structures/enums.dto';
import { WAHAEvents } from '../structures/enums.dto';
import { WAWebhook } from '../structures/responses.dto';
import { WebhookConfig } from '../structures/webhooks.dto';
import { WhatsappSession } from './abc/session.abc';
import { WebhookConductor, WebhookSender } from './abc/webhooks.abc';
import { NotImplementedByEngineError } from './exceptions';
import { WebhookConductor } from './abc/webhooks.abc';
import request = require('request');
export class WebhookSenderCore extends WebhookSender {
send(json) {
this.log.log(`Sending POST to ${this.url}...`);
this.log.debug(`POST DATA: ${JSON.stringify(json)}`);
request.post(this.url, { json: json }, (error, res, body) => {
if (error) {
this.log.error(error);
return;
}
this.log.log(`POST request was sent with status code: ${res.statusCode}`);
this.log.verbose(`Response: ${JSON.stringify(body)}`);
});
}
}
export class WebhookConductorCore implements WebhookConductor {
constructor(
protected log: ConsoleLogger,
private readonly url,
private readonly events: WAEvents[] | string[],
) {
this.url = url;
this.events = this.getSuitableEvents(events);
constructor(protected log: ConsoleLogger) {
this.log = log;
}
private getSuitableEvents(events: WAEvents[] | string[]) {
const allEvents = Object.values(WAEvents);
protected buildSender(webhookConfig: WebhookConfig): WebhookSender {
return new WebhookSenderCore(this.log, webhookConfig);
}
private getSuitableEvents(events: WAHAEvents[] | string[]): WAHAEvents[] {
const allEvents = Object.values(WAHAEvents);
// Enable all events if * in the events
// @ts-ignore
@@ -39,12 +55,28 @@ export class WebhookConductorCore implements WebhookConductor {
return rightEvents;
}
public configure(session: WhatsappSession) {
this.log.log('Configuring webhooks...');
for (const event of this.events) {
public configure(session: WhatsappSession, webhooks: WebhookConfig[]) {
for (const webhookConfig of webhooks) {
this.configureSingleWebhook(session, webhookConfig);
}
}
private configureSingleWebhook(
session: WhatsappSession,
webhook: WebhookConfig,
) {
if (!webhook || !webhook.url || webhook.events.length === 0) {
return;
}
const url = webhook.url;
this.log.log(`Configuring webhooks for ${url}...`);
const events = this.getSuitableEvents(webhook.events);
const sender = this.buildSender(webhook);
for (const event of events) {
try {
session.subscribe(event, (data: any) =>
this.callWebhook(event, session, data, this.url),
this.callWebhook(event, session, data, sender),
);
} catch (error) {
if (error instanceof NotImplementedByEngineError) {
@@ -53,31 +85,23 @@ export class WebhookConductorCore implements WebhookConductor {
throw error;
}
}
this.log.log(`Event '${event}' is enabled for url: ${this.url}`);
this.log.log(`Event '${event}' is enabled for url: ${url}`);
}
this.log.log('Webhooks were configured.');
this.log.log(`Webhooks were configured for ${url}.`);
}
public callWebhook(event, session: WhatsappSession, data: any, url) {
public callWebhook(
event,
session: WhatsappSession,
data: any,
sender: WebhookSender,
) {
const json: WAWebhook = {
event: event,
session: session.name,
payload: data,
engine: session.engine,
};
this.log.log(`Sending POST to ${url}...`);
this.log.debug(`POST DATA: ${JSON.stringify(json)}`);
this.post(json, url);
}
protected post(json, url) {
request.post(url, { json: json }, (error, res, body) => {
if (error) {
this.log.error(error);
return;
}
this.log.log(`POST request was sent with status code: ${res.statusCode}`);
this.log.verbose(`Response: ${JSON.stringify(body)}`);
});
sender.send(json);
}
}
+9 -1
View File
@@ -1,4 +1,4 @@
export function parseBool(value: any): boolean {
function parseBool(value: any): boolean {
if (!value) {
return false;
}
@@ -18,3 +18,11 @@ export function parseBool(value: any): boolean {
'Error: parseBool got unexpected value - use "true" or "false" values',
);
}
function flipObject(object) {
return Object.fromEntries(
Object.entries(object).map(([key, value]) => [value, key]),
);
}
export { flipObject, parseBool };
+4 -3
View File
@@ -1,10 +1,11 @@
import { NestFactory } from '@nestjs/core';
import { WhatsappConfigService } from './config.service';
import { json, urlencoded } from 'express';
import { AllExceptionsFilter } from './api/exception.filter';
import { getWAHAVersion, VERSION, WAHAVersion } from './version';
import { WhatsappConfigService } from './config.service';
import { AppModuleCore } from './core/app.module.core';
import { SwaggerModuleCore } from './core/swagger.module.core';
import { json, urlencoded } from 'express';
import { getWAHAVersion, WAHAVersion } from './version';
async function loadModules(): Promise<
[typeof AppModuleCore, typeof SwaggerModuleCore]
+21
View File
@@ -0,0 +1,21 @@
import { ApiProperty } from '@nestjs/swagger';
import { SessionBaseRequest, SessionQuery } from './base.dto';
/**
* Queries
*/
export class GetChatMessagesQuery extends SessionQuery {
limit: number = 100;
@ApiProperty({
example: true,
required: false,
description: 'Download media for messages',
})
downloadMedia: boolean = true;
}
/**
* Requests
*/
+18 -2
View File
@@ -1,5 +1,6 @@
import { IsNotEmpty, IsString, IsOptional, IsNumber } from 'class-validator';
import { ApiExtraModels, ApiProperty, getSchemaPath } from '@nestjs/swagger';
import { IsNumber, IsString } from 'class-validator';
import { SessionBaseRequest, SessionQuery } from './base.dto';
/**
@@ -27,8 +28,16 @@ export class ChatQuery extends SessionQuery {
export class GetMessageQuery extends ChatQuery {
@IsNumber()
limit: number;
@ApiProperty({
example: true,
required: false,
description: 'Download media for messages',
})
downloadMedia: true;
}
export class GetPresenceQuery extends ChatQuery {}
/**
* Requests
*/
@@ -71,6 +80,13 @@ export class MessageContactVcardRequest extends ChatRequest {
export class MessageTextRequest extends ChatRequest {
text = 'Hi there!';
@ApiProperty({
description:
'Mention contact in the message. ' +
"The message MUST contain '@123456789' text in order to work with 'mentions' field.",
example: ['123456789@c.us'],
})
mentions?: string[];
}
export class Button {
@@ -84,7 +100,7 @@ export class MessageTextButtonsRequest extends ChatRequest {
buttons?: Array<Button>;
}
export class MessageReplyRequest extends ChatRequest {
export class MessageReplyRequest extends MessageTextRequest {
text = 'Reply text';
@ApiProperty({
example: 'false_11111111111@c.us_AAAAAAAAAAAAAAAAAAAA',
+1
View File
@@ -1,4 +1,5 @@
import { IsString } from 'class-validator';
import { SessionBaseRequest, SessionQuery } from './base.dto';
/**
+12 -3
View File
@@ -1,15 +1,16 @@
export const SECOND = 1000;
export enum WAEvents {
export enum WAHAEvents {
MESSAGE = 'message',
MESSAGE_ANY = 'message.any',
MESSAGE_ACK = 'message.ack',
STATE_CHANGE = 'state.change',
GROUP_JOIN = 'group.join',
GROUP_LEAVE = 'group.leave',
PRESENCE_UPDATE = 'presence.update',
}
export enum WhatsappStatus {
export enum WAHASessionStatus {
STOPPED = 'STOPPED',
STARTING = 'STARTING',
SCAN_QR_CODE = 'SCAN_QR_CODE',
@@ -17,8 +18,16 @@ export enum WhatsappStatus {
FAILED = 'FAILED',
}
export enum WhatsappEngine {
export enum WAHAEngine {
VENOM = 'VENOM',
WEBJS = 'WEBJS',
NOWEB = 'NOWEB',
}
export enum WAHAPresenceStatus {
OFFLINE = 'offline',
ONLINE = 'online',
TYPING = 'typing',
RECORDING = 'recording',
PAUSED = 'paused',
}
+34
View File
@@ -0,0 +1,34 @@
import { ApiProperty } from '@nestjs/swagger';
import { WAHAPresenceStatus } from './enums.dto';
export class WAHAPresenceData {
@ApiProperty({
description: 'Chat ID - participant or contact id',
example: '11111111111@c.us',
})
participant: string;
lastKnownPresence: WAHAPresenceStatus;
@ApiProperty({
example: 1686568773,
})
lastSeen?: number;
}
export class WAHAChatPresences {
@ApiProperty({
description: 'Chat ID - either group id or contact id',
example: '11111111111@c.us',
})
id: string;
presences: WAHAPresenceData[];
}
export class WAHASessionPresence {
presence: WAHAPresenceStatus;
@ApiProperty({
description: 'Chat ID - either group id or contact id',
example: '11111111111@c.us',
})
chatId: string;
}
+4 -3
View File
@@ -1,5 +1,6 @@
import { ApiProperty } from '@nestjs/swagger';
import { WAEvents, WhatsappEngine } from './enums.dto';
import { WAHAEngine, WAHAEvents } from './enums.dto';
export class WANumberExistResult {
numberExists: boolean;
@@ -140,9 +141,9 @@ export class WAMessage {
}
export class WAWebhook {
event: WAEvents;
event: WAHAEvents;
session: string;
engine: WhatsappEngine;
engine: WAHAEngine;
// eslint-disable-next-line @typescript-eslint/ban-types
payload: WAMessage | WAGroupNotification | object;
}
+30 -2
View File
@@ -1,6 +1,8 @@
import { WhatsappStatus } from './enums.dto';
import { ApiProperty } from '@nestjs/swagger';
import { WAHASessionStatus } from './enums.dto';
import { WebhookConfig } from './webhooks.dto';
/**
* Queries
*/
@@ -13,11 +15,36 @@ export class ListSessionsQuery {
})
all: boolean;
}
/**
* Requests
*/
export class ProxyConfig {
@ApiProperty({
example: 'localhost:3128',
})
server: string;
@ApiProperty({
example: null,
})
username?: string;
@ApiProperty({
example: null,
})
password?: string;
}
export class SessionConfig {
webhooks?: WebhookConfig[];
@ApiProperty({
example: null,
})
proxy?: ProxyConfig;
}
export class SessionStartRequest {
name = 'default';
config?: SessionConfig;
}
export class SessionStopRequest {
@@ -36,5 +63,6 @@ export class SessionLogoutRequest {
export class SessionDTO {
name = 'default';
status: WhatsappStatus;
status: WAHASessionStatus;
config: SessionConfig;
}
+60
View File
@@ -0,0 +1,60 @@
import { ApiProperty } from '@nestjs/swagger';
export class RetriesConfiguration {
@ApiProperty({
example: 2,
})
delaySeconds: number;
@ApiProperty({
example: 15,
})
attempts: number;
}
export class CustomHeader {
@ApiProperty({
example: 'X-My-Custom-Header',
})
name: string;
@ApiProperty({
example: 'Value',
})
value: string;
}
export class HmacConfiguration {
@ApiProperty({
example: 'your-secret-key',
})
key: string;
}
export class WebhookConfig {
@ApiProperty({
example: 'https://httpbin.org/post',
required: true,
})
url: string;
@ApiProperty({
example: ['message'],
required: true,
})
events: string[];
@ApiProperty({
example: null,
})
hmac?: HmacConfiguration;
@ApiProperty({
example: null,
})
retries?: RetriesConfiguration;
@ApiProperty({
example: null,
})
customHeaders?: CustomHeader[];
}
+1 -3
View File
@@ -3,8 +3,6 @@ import { getBrowserExecutablePath } from './core/abc/session.abc';
// eslint-disable-next-line @typescript-eslint/no-var-requires
const fs = require('fs');
// eslint-disable-next-line @typescript-eslint/no-var-requires
const path = require('path');
export enum WAHAVersion {
PLUS = 'PLUS',
@@ -28,7 +26,7 @@ export function getWAHAVersion(): WAHAVersion {
}
export const VERSION = {
version: '2023.6.1',
version: '2023.7.16',
tier: getWAHAVersion(),
browser: getBrowserExecutablePath(),
};
+2 -1
View File
@@ -10,6 +10,7 @@
"sourceMap": true,
"outDir": "./dist",
"baseUrl": "./",
"incremental": true
"incremental": true,
"resolveJsonModule": true,
}
}
+13420 -9309
View File
File diff suppressed because it is too large. Load diff