Compare commits

..
52 Commits
Author SHA1 Message Date
allburov 41b29cbf60 [core] Update swagger
Release / amd64 - chrome - chrome (push) Waiting to run
Release / amd64 - chromium - latest (push) Waiting to run
Release / linux/arm64 - chromium - arm (push) Waiting to run
Release / amd64 - none - noweb (push) Waiting to run
Release / linux/arm64 - none - noweb-arm (push) Waiting to run
2023-12-01 09:34:25 +07:00
allburov 44463c199a [core] Try to fix swagger for redoc 2023-12-01 09:33:56 +07:00
allburov 9071adcdcc [core] Update NOWEB engine 2023-12-01 09:32:11 +07:00
allburov 21cd92ca13 [core] Update WEBJS engine 2023-12-01 09:30:02 +07:00
allburov d8a3e8dc43 [core] Bump version to 2023.12.1 2023-12-01 09:25:23 +07:00
allburov 3f5707952c [core] Update swagger 2023-12-01 09:25:06 +07:00
allburov 38a9590be0 [core] Add screenshot and QR to changelog
Fix #177
2023-11-30 18:01:51 +07:00
allburov a7ce117b87 [core] VENOM supports /qr 2023-11-30 18:01:51 +07:00
allburov c3b24a243c [core] document QR code formats 2023-11-30 18:01:51 +07:00
allburov cb2c11df43 [core] screenshot base64 2023-11-30 18:01:50 +07:00
allburov 98f88dcf68 [core] screenshot doc 2023-11-30 18:01:50 +07:00
allburov 9fb12e0e23 [core] Add base64 to screenshot 2023-11-30 18:01:49 +07:00
allburov 0ca106f16b [core] Fix swagger 2023-11-30 18:01:49 +07:00
allburov 33210c6374 [core] Get raw QR value 2023-11-30 18:01:49 +07:00
allburov 045ab67f5d [core] Add BufferResponseInterceptor 2023-11-30 18:01:49 +07:00
allburov 19259c93ea [core] Return json or binary based on Accept header 2023-11-30 18:01:49 +07:00
allburov 9b6784a018 [core] Use "default" session by default 2023-11-30 18:01:49 +07:00
allburov 2fe3706e9c [core] Add base64 file 2023-11-30 18:01:49 +07:00
allburov a4e310a7f8 [core] Get screenshot return only buffer 2023-11-30 18:01:49 +07:00
allburov 802da9f3b2 [core] Use StreamableFile 2023-11-30 18:01:49 +07:00
allburov 0692979b19 [core] Add swagger to config page 2023-11-25 20:43:05 +07:00
allburov fb8b146bc4 [core] Add hide swagger doc 2023-11-25 20:33:35 +07:00
allburov fdbaa43ec9 [core] Add WHATSAPP_SWAGGER_ENABLED environment variable 2023-11-25 20:33:35 +07:00
allburov 4a807530c1 [core] Remove unused method 2023-11-25 20:33:35 +07:00
allburov 84784faa1e [core] eslint no warnings 2023-11-25 20:03:34 +07:00
allburov bd40978dee [core] Apply prettier to other files too 2023-11-25 19:56:32 +07:00
allburov d4d94ffe0e [core] Add empty line linter 2023-11-25 19:44:16 +07:00
allburov d984382a33 [core] Run linter in pre-commit hook 2023-11-25 19:41:41 +07:00
allburov 02d05812d2 [core] Update prettier 2023-11-25 19:09:25 +07:00
allburov 1dbf568bac [core] Add more props 2023-11-25 19:09:25 +07:00
allburov 6bc3aed348 [core] print width 2023-11-25 19:03:58 +07:00
allburov c1e8e0f09c [core] tab 2023-11-25 19:03:58 +07:00
allburov 8f8bcfc474 [core] Don't use tabs 2023-11-25 19:03:58 +07:00
allburov b386df982d [core] bracketSpacing 2023-11-25 19:03:58 +07:00
allburov fa846ddf9b [core] arrowParens always 2023-11-25 19:03:58 +07:00
allburov 16685ed8ea [core] Add endOfLine 2023-11-25 19:03:53 +07:00
allburov 4addccbd55 [core] Update node version in nvm 2023-11-25 19:03:53 +07:00
allburov e9a20c4326 [core] Run eslint 2023-11-25 19:03:53 +07:00
Alex f073aeef13 Update issue templates 2023-11-25 18:43:22 +07:00
allburov eb08739eea [core] Send 404 if file does not exist
fix #134
2023-11-25 18:16:51 +07:00
allburov aff3b01e39 [core] Hide mentions 2023-11-25 18:03:49 +07:00
allburov 2d64b26e86 [core] Hide mentions from send message 2023-11-25 17:37:38 +07:00
allburov 7b7bd02503 [core] Media improvements
- Disable downloading files
- Add 'media' field
2023-11-25 17:25:19 +07:00
allburov 7904516c97 [core] Changelog for filename 2023-11-24 17:47:29 +07:00
allburov 6e0bafdd89 [core] Add doc about new filename field 2023-11-24 17:47:29 +07:00
allburov 2b1e605e5f [core] Add filename property - NOWEB 2023-11-24 17:47:29 +07:00
allburov f454310a09 [core] Add filename property - WEBJS 2023-11-24 17:47:29 +07:00
allburov b6579b5b96 [core] Add filename property 2023-11-24 17:47:29 +07:00
allburov 4ef06d7b92 [docs] Add doc for GET /info-admin-only 2023-11-20 16:25:21 +07:00
allburov e0c594a71b [core] Add GET /info-admin-only 2023-11-20 16:23:23 +07:00
allburov a48d386d8b [core] Update WEBJS 2023-11-20 16:23:21 +07:00
allburov d592858a90 [core] Add name to job 2023-11-05 20:19:42 +07:00
62 changed files with 979 additions and 303 deletions

No files matched your search

+25 -22
View File
@@ -1,33 +1,36 @@
module.exports = {
parser: "@typescript-eslint/parser",
parser: '@typescript-eslint/parser',
parserOptions: {
project: "tsconfig.json",
sourceType: "module"
project: 'tsconfig.json',
sourceType: 'module',
},
plugins: [
"@typescript-eslint/eslint-plugin",
"simple-import-sort"
],
plugins: ['@typescript-eslint/eslint-plugin', 'simple-import-sort'],
extends: [
"plugin:@typescript-eslint/eslint-recommended",
"plugin:@typescript-eslint/recommended",
"prettier",
"prettier/@typescript-eslint"
'plugin:@typescript-eslint/eslint-recommended',
'plugin:@typescript-eslint/recommended',
'prettier',
'prettier/@typescript-eslint',
],
root: true,
env: {
node: true,
jest: 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",
}
'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',
'@typescript-eslint/no-unused-vars': 'off',
'lines-between-class-members': [
'error',
'always',
{ exceptAfterSingleLine: true },
],
},
};
+61
View File
@@ -0,0 +1,61 @@
---
name: Bug report
about: Create a report to help us improve
title: '[BUG][WEBJS] - '
labels: bug
assignees: ''
---
**Describe the bug**
> A clear and concise description of what the bug is. Feel free to remove
> sections that you don't feel to make the text shorter!
**Version**
```json
{
"version": "2023.11.1",
"engine": "NOWEB",
"tier": "PLUS",
"browser": "/usr/bin/google-chrome-stable"
}
```
> You can get the WAHA version by calling `GET /api/version` Try to update to
> [the latest version](https://github.com/devlikeapro/whatsapp-http-api/releases)
> before creating an issue!
**To Reproduce** Steps to reproduce the behavior:
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error
**Expected behavior**
> A clear and concise description of what you expected to happen.
**Engine** I'm using **WEBJS** engine.
> If you set `WHATSAPP_DEFAULT_ENGINE` explicitly to
> [one of the engine](https://waha.devlike.pro/docs/how-to/engines/) - please
> provide that information here and in the issue title.
**Requests - Responses**
> Please attach all sent requests, responses that you sent.
**Docker Logs**
> Collect and attach related docker logs if you have any
> https://waha.devlike.pro/docs/how-to/deploy/#viewing-the-logs
**Screenshots**
> If applicable, add screenshots to help explain your problem.
**Additional context**
> Add any other context about the problem here.
+24
View File
@@ -0,0 +1,24 @@
---
name: Feature request
about: Suggest an idea for this project
title: '[Feature Request]'
labels: ''
assignees: ''
---
**Is your feature request related to a problem? Please describe.** I'm always
frustrated when [...]
> A clear and concise description of what the problem is. Ex.
**Describe the solution you'd like**
> A clear and concise description of what you want to happen.
**Describe alternatives you've considered**
> A clear and concise description of any alternative solutions or features
> you've considered.
**Additional context** Add any other context or screenshots about the feature
request here.
+26 -25
View File
@@ -3,42 +3,43 @@ name: Release
on:
push:
tags:
- "*"
- '*'
jobs:
docker-build:
runs-on: ${{ matrix.runner }}
name: ${{ matrix.platform }} - ${{ matrix.browser }} - ${{ matrix.tag }}
strategy:
matrix:
include:
# Chromium
- runner: "buildjet-4vcpu-ubuntu-2204"
tag: "latest"
platform: "amd64"
browser: "chromium"
- runner: "buildjet-4vcpu-ubuntu-2204-arm"
tag: "arm"
platform: "linux/arm64"
browser: "chromium"
- runner: 'buildjet-4vcpu-ubuntu-2204'
tag: 'latest'
platform: 'amd64'
browser: 'chromium'
- runner: 'buildjet-4vcpu-ubuntu-2204-arm'
tag: 'arm'
platform: 'linux/arm64'
browser: 'chromium'
# Chrome
- runner: "buildjet-4vcpu-ubuntu-2204"
tag: "chrome"
platform: "amd64"
browser: "chrome"
- runner: 'buildjet-4vcpu-ubuntu-2204'
tag: 'chrome'
platform: 'amd64'
browser: 'chrome'
# Chrome is not supported on arm64
# - runner: "buildjet-4vcpu-ubuntu-2204-arm"
# tag: "chrome-arm"
# platform: "linux/arm64"
# browser: "chrome"
# - runner: "buildjet-4vcpu-ubuntu-2204-arm"
# tag: "chrome-arm"
# platform: "linux/arm64"
# browser: "chrome"
# No browser
- runner: "buildjet-4vcpu-ubuntu-2204"
tag: "noweb"
platform: "amd64"
browser: "none"
- runner: "buildjet-4vcpu-ubuntu-2204-arm"
tag: "noweb-arm"
platform: "linux/arm64"
browser: "none"
- runner: 'buildjet-4vcpu-ubuntu-2204'
tag: 'noweb'
platform: 'amd64'
browser: 'none'
- runner: 'buildjet-4vcpu-ubuntu-2204-arm'
tag: 'noweb-arm'
platform: 'linux/arm64'
browser: 'none'
steps:
- name: Checkout
+1 -1
View File
@@ -1 +1 @@
v18.12.0
v20.5.0
+16 -4
View File
@@ -1,13 +1,25 @@
repos:
- repo: https://github.com/pre-commit/mirrors-prettier
rev: 'v2.7.1'
- repo: local
hooks:
- id: prettier
types_or: [ javascript, ts ]
- id: lint
name: lint
language: system
entry: bash -c '$HOME/.nvm/nvm-exec npm run lint'
pass_filenames: false
files: |
(?x)^(
src/.*|
)$
- repo: https://github.com/pre-commit/mirrors-prettier
rev: 'v3.1.0'
hooks:
- id: prettier
exclude: |
(?x)^(
docs/.*|
README.md|
)$
- repo: https://github.com/Lucas-C/pre-commit-hooks-nodejs
rev: v1.1.2
hooks:
+11 -2
View File
@@ -1,4 +1,13 @@
{
"singleQuote": true,
"trailingComma": "all"
}
"trailingComma": "all",
"endOfLine": "auto",
"arrowParens": "always",
"bracketSpacing": true,
"useTabs": false,
"tabWidth": 2,
"semi": true,
"quoteProps": "as-needed",
"proseWrap": "always",
"printWidth": 80
}
+3 -1
View File
@@ -1,2 +1,4 @@
# Changelog
You can find changelog on [the changelog page](https://waha.devlike.pro/docs/help/changelog/)
You can find changelog on
[the changelog page](https://waha.devlike.pro/docs/help/changelog/)
+3 -3
View File
@@ -4,7 +4,7 @@ services:
image: devlikeapro/whatsapp-http-api
container_name: whatsapp-http-api
ports:
- "3000:3000/tcp"
- '3000:3000/tcp'
environment:
# Environment variables from https://waha.devlike.pro/docs/how-to/config/
- WHATSAPP_HOOK_URL=https://httpbin.org/post
@@ -16,9 +16,9 @@ services:
image: devlikeapro/whatsapp-http-api-plus
container_name: whatsapp-http-api-plus
ports:
- "3000:3000/tcp"
- '3000:3000/tcp'
volumes:
- "./.sessions:/app/.sessions"
- './.sessions:/app/.sessions'
environment:
# Environment variables from https://waha.devlike.pro/docs/how-to/config/
- WHATSAPP_RESTART_ALL_SESSIONS=True
@@ -42,6 +42,11 @@ The following environment variables can be used to configure the WAHA.
### 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.
- `WHATSAPP_SWAGGER_ENABLED=false` - disables Swagger documentation. Enabled by default. Available in **WAHA Plus** only.
- `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. Available in **WAHA Plus** only.
Read more about security settings for Swagger and API on [**Security page** ->]({{< relref "/docs/how-to/security" >}}).
### Proxy
#### Global proxy configuration
@@ -72,14 +77,34 @@ Keep in mind that session's proxy configuration takes precedence over proxy conf
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_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`). See usage below.
- `WHATSAPP_DOWNLOAD_MEDIA=true` - this variable can be used to **completely** disable downloading media files. By default, all files are downloaded.
Set this variable to `WHATSAPP_DOWNLOAD_MEDIA=false` to disable downloading media files.
- Under the hood, it sets `WHATSAPP_FILES_MIMETYPES=mimetype/ignore-all-media` to ignore all media files.
- `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`.
💡 Even if WAHA doesn't process the message media because of `WHATSAPP_FILES_MIMETYPES` or `WHATSAPP_DOWNLOAD_MEDIA`
you'll get a webhook event with `hasMedia: True` field, but with no `media.url`.
```json
{
"event": "message",
"session": "default",
"payload": {
"hasMedia": true,
"media": {
"url": null,
"mimetype": "video/mp4",
"filename": null
}
}
}
```
## Examples
#### Debug Mode
@@ -138,6 +163,13 @@ comma-separated list of mimetypes:
WHATSAPP_FILES_MIMETYPES=audio,image/png,image/gif
```
#### Disable Downloading Media Files
To disable downloading media files, set the `WHATSAPP_DOWNLOAD_MEDIA` environment variable to `false`:
```
WHATSAPP_DOWNLOAD_MEDIA=false
```
#### Setting the File Lifetime
To set the time (in seconds) after which files will be removed to free up space, set the `WHATSAPP_FILES_LIFETIME`
@@ -107,7 +107,7 @@ please [create an issue](https://github.com/devlikeapro/whatsapp-http-api/issues
| `GET /api/sessions/` | ✔️ | ✔️ | ✔️ |
| `GET /api/sessions/{session}/me` | ✔️ | ✔️ | ➖ |
| **Authentication** | | | |
| `POST /api/{session}/auth/qr` | ✔️ | ✔️ | ➖ |
| `POST /api/{session}/auth/qr` | ✔️ | ✔️ | ✔️ |
| `POST /api/{session}/auth/request-code` | ➖ | ✔️ | ➖ |
| `POST /api/{session}/auth/authorize-code` | ➖ | ✔️ | ➖ |
| **Screenshot** | | | |
@@ -160,6 +160,7 @@ please [create an issue](https://github.com/devlikeapro/whatsapp-http-api/issues
| `GET /api/{session}/groups` | ✔️ | ✔️ | ➖ |
| `GET /api/{session}/groups/{id}` | ✔️ | ✔️ | ➖ |
| `DELETE /api/{session}/groups/{id}` | ✔️ | ➖ | ➖ |
| `GET /api/{session}/groups/{id}/settings/security/info-admin-only` | ✔️ | ➖ | ➖ |
| `PUT /api/{session}/groups/{id}/settings/security/info-admin-only` | ✔️ | ➖ | ➖ |
| `POST /api/{session}/groups/{id}/leave` | ✔️ | ✔️ | ➖ |
| `PUT /api/{session}/groups/{id}/description` | ✔️ | ✔️ | ➖ |
@@ -90,7 +90,12 @@ Request:
### Settings
#### Security for group info
Updates the group settings to only allow admins to edit group info (title, description, photo).
`GET /api/{session}/groups/{groupId}/settings/security/info-admin-only`
```json
{
"adminsOnly": true
}
```
`PUT /api/{session}/groups/{groupId}/settings/security/info-admin-only`
The request doesn't require any request's body and
@@ -154,10 +154,10 @@ Receive events when server or recipient gets the message, read or played it.
## Files ![](/images/versions/plus.png)
When people send you files - images, voice messages, and documents - WAHA saves it in the file storage.
In your application you must download it and use it as you want to. You can find the URL in `mediaUrl` field
When people send you media - images, voice messages, and documents - WAHA saves it in the file storage.
In your application you must download it and use it as you want to. You can find the URL in `media.url` field.
For example, you can get the webhook like this with `mediaUrl` value (we've skipped other fields):
For example, you can get the webhook like this with `media` value (we've skipped other fields):
```json
{
@@ -167,12 +167,35 @@ For example, you can get the webhook like this with `mediaUrl` value (we've skip
"id": "true_11111111111@c.us_AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA",
"timestamp": 1667561485,
"from": "11111111111@c.us",
"mediaUrl": "http://localhost:3000/api/files/true_11111111111@c.us_AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA.jpg"
"hasMedia": true,
"media": {
"url": "http://localhost:3000/api/files/true_11111111111@c.us_AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA.jpg",
"mimetype": "image/jpeg",
"filename": null
}
}
}
```
Then you can use the link to download the file `http://localhost:3000/api/files/true_11111111111@c.us_AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA.jpg`.
For documents (file attachments) there's also `filename` field with the original file name.
```json
{
"event": "message",
"session": "default",
"payload": {
"id": "true_11111111111@c.us_AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA",
"timestamp": 1667561485,
"from": "11111111111@c.us",
"media": {
"url": "http://localhost:3000/api/files/true_11111111111@c.us_AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA.pdf",
"filename": "some-file.pdf",
"mimetype": "application/pdf"
}
}
}
```
To configure the url you can use environment variables `WHATSAPP_API_HOSTNAME` and `WHATSAPP_API_PORT`.
By default, WAHA download all files that the account receive.
@@ -16,6 +16,7 @@ Either protect the API with [Api Key](https://www.fortinet.com/resources/cybergl
firewalls.
## Swagger Security ![](/images/versions/plus.png)
### Username and password
If you want to hide the project Swagger panel under the password - run the following command to hide under `admin/admin`
login and password.
@@ -29,6 +30,12 @@ Open http://localhost:3000/ and enter `admin / admin` in the inputs:
{{< alert icon="👉" text="Protecting Swagger under the password does not protect your API from other request! Use both techniques to protect your API and Swagger!" />}}
### Disable Swagger
You also can hide swagger completely by setting `WHATSAPP_SWAGGER_ENABLED=false` environment variable.
{{< alert icon="👉" text="Disabling Swagger does not protect the API, please use API security as well" />}}
## API security ![](/images/versions/plus.png)
You can protect the API by requiring Api Key in a request's headers.
@@ -128,6 +128,32 @@ In order to log out the session - call `POST /api/sessions/logout`
"logout": true
}
```
### Get screenshot
Get screenshot of the session's screen.
```bash
GET /api/screenshot?session=default
```
#### Get screenshot in Base64
You can get screenshot in base64 format by adding `Accept: application/json` header to the request.
```bash
GET /api/screenshot?session=default
Accept: application/json
```
```json
{
"mimetype": "image/png",
"data": "base64-encoded-data"
}
```
You can change it in Swagger by clicking on **Media Type** dropdown and selecting **application/json**:
![](/images/swagger-media-type.png)
### Get me
@@ -160,6 +186,55 @@ GET /api/{session}/auth/qr
```
You'll get QR image that you can scan and get authenticated
#### QR formats
You can get QR in different formats:
1. **binary image** - `GET /api/{session}/auth/qr`
2. **base64 image** - `GET /api/{session}/auth/qr` and set `Accept: application/json` header
3. **raw** - `GET /api/{session}/auth/qr?format=raw`
Here's detailed information about each format:
1. **binary image**, binary image - **default** format, you'll get image in response
```bash
# Get image - binary
GET /api/{session}/auth/qr
# OR
GET /api/{session}/auth/qr?format=image
# OR specify Accept header as well
GET /api/{session}/auth/qr?format=image
Accept: image/png
```
2. **base64 image** - you'll get image in base64 format in response if you set `Accept: application/json` header.
```bash
GET /api/{session}/auth/qr?format=image
Accept: application/json
```
```json
{
"mimetype": "image/png",
"data": "base64-encoded-data"
}
```
You can change it in Swagger by clicking on **Media Type** dropdown and selecting **application/json**:
![](/images/swagger-media-type.png)
3. **raw** - you'll get raw data in response, you can use it to generate QR code on your side
```bash
GET /api/{session}/auth/qr?format=raw
```
```json
{
"value": "value-that-you-need-to-use-to-generate-qr-code"
}
```
### Get pairing code
See the list of engines [**that support the features ->**]({{< relref "/docs/how-to/engines#features" >}}).
@@ -21,7 +21,36 @@ You even can **subscribe to get new updates** there!
## 2023.11
November 2023
- Add different formats for QR code:
1. **binary image** - `GET /api/{session}/auth/qr`
2. **base64 image** - `GET /api/{session}/auth/qr` and set `Accept: application/json` header
3. **raw** - `GET /api/{session}/auth/qr?format=raw`
- Add different formats for the screenshot:
1. **binary image** - `GET /api/{session}/screenshot`
2. **base64 image** - `GET /api/{session}/screenshot` and set `Accept: application/json` header
- Add `WHATSAPP_SWAGGER_ENABLED=false` so you can hide Swagger documentation (available only in Plus version). [#185](https://github.com/devlikeapro/whatsapp-http-api/issues/185)
- Add dedicated `media` field in webhook payload - now you can know which media is attached to the message.
```json
{
"event": "message",
"session": "default",
"payload": {
"hasMedia": true,
"media": {
"url": "http://localhost:3000/api/files/true_11111111111@c.us_AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA.pdf",
"mimetype": "application/pdf",
"filename": "file.pdf"
}
}
}
```
- Add `WHATSAPP_DOWNLOAD_MEDIA` environment variable to control media download. Available only in Plus version.
- `WHATSAPP_DOWNLOAD_MEDIA=True` - download media
- `WHATSAPP_DOWNLOAD_MEDIA=False` - do not download media at all.
- Add `filename` field for document (file attachments) - the original filename of the document.
- Add `/api/sendVideo` endpoint to **NOWEB** and **WEBJS** engines. Available only in Plus version.
- Add security settings for edit group info `GET /api/{session}/groups/{groupId}/settings/security/info-admin-only` - updates the group settings to only allow admins to edit group info (title, description, photo).
## 2023.10
October 2023
Binary file not shown.

After

Width:  |  Height:  |  Size: 49 KiB

+1 -1
View File
@@ -1 +1 @@
{"openapi":"3.1.0","paths":{"/api/{session}/auth/qr":{"get":{"operationId":"AuthController_getQR","summary":"Get QR code for pairing WhatsApp Web.","parameters":[{"name":"session","required":true,"in":"path","description":"WhatsApp session name","schema":{"type":"string"}}],"responses":{"200":{"description":""}},"tags":["auth"],"security":[{"api_key":[]}]}},"/api/{session}/auth/request-code":{"post":{"operationId":"AuthController_requestCode","summary":"Request authentication code.","parameters":[{"name":"session","required":true,"in":"path","description":"WhatsApp session name","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RequestCodeRequest"}}}},"responses":{"201":{"description":""}},"tags":["auth"],"security":[{"api_key":[]}]}},"/api/{session}/auth/authorize-code":{"post":{"operationId":"AuthController_authorizeCode","summary":"Send OTP authentication code.","parameters":[{"name":"session","required":true,"in":"path","description":"WhatsApp session name","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/OTPRequest"}}}},"responses":{"201":{"description":""}},"tags":["auth"],"security":[{"api_key":[]}]}},"/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/SessionInfo"}}}}}},"tags":["sessions"],"security":[{"api_key":[]}]}},"/api/sessions/{session}/me":{"get":{"operationId":"SessionController_getMe","summary":"Get information about the authenticated account","parameters":[{"name":"session","required":true,"in":"path","description":"WhatsApp session name","schema":{"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/MeInfo"}}}}},"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":[]}]}},"/apLine truncated
{"openapi":"3.1.0","paths":{"/api/{session}/auth/qr":{"get":{"operationId":"AuthController_getQR","summary":"Get QR code for pairing WhatsApp Web.","parameters":[{"name":"session","required":true,"in":"path","schema":{"default":"default"},"description":"WhatsApp session name"},{"name":"format","required":true,"in":"query","schema":{"default":"image","enum":["image","raw"],"type":"string"}}],"responses":{"200":{"content":{"image/png":{"schema":{"type":"string","format":"binary"}},"application/json":{"schema":{"oneOf":[{"$ref":"#/components/schemas/Base64File"},{"$ref":"#/components/schemas/QRCodeValue"}]}}},"description":""}},"tags":["auth"],"security":[{"api_key":[]}]}},"/api/{session}/auth/request-code":{"post":{"operationId":"AuthController_requestCode","summary":"Request authentication code.","parameters":[{"name":"session","required":true,"in":"path","schema":{"default":"default"},"description":"WhatsApp session name"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RequestCodeRequest"}}}},"responses":{"201":{"description":""}},"tags":["auth"],"security":[{"api_key":[]}]}},"/api/{session}/auth/authorize-code":{"post":{"operationId":"AuthController_authorizeCode","summary":"Send OTP authentication code.","parameters":[{"name":"session","required":true,"in":"path","schema":{"default":"default"},"description":"WhatsApp session name"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/OTPRequest"}}}},"responses":{"201":{"description":""}},"tags":["auth"],"security":[{"api_key":[]}]}},"/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/SessionInfo"}}}}}},"tags":["sessions"],"security":[{"api_key":[]}]}},"/api/sessions/{session}/me":{"get":{"operationId":"SessionController_getMe","summary":"Get information about the authenticated account","parameters":[{"name":"session","required":true,"in":"path","schema":{"default":"default"},"description":"WhatsApp session name"}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/MeInfo"}}}}},"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":""}},"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,Line truncated
+30 -20
View File
@@ -9,10 +9,11 @@ We assume that you have installed software:
### Download and start image
First of all, you must run WhatsApp HTTP API locally (which under the hood it runs real WhatsApp Web instance and expose
HTTP API for interaction).
First of all, you must run WhatsApp HTTP API locally (which under the hood it
runs real WhatsApp Web instance and expose HTTP API for interaction).
Here are the steps from [Quick Start](https://waha.devlike.pro/docs/overview/quick-start/):
Here are the steps from
[Quick Start](https://waha.devlike.pro/docs/overview/quick-start/):
Download and start WhatsApp HTTP API docker container
@@ -28,9 +29,11 @@ docker run -it --rm --network=host -e WHATSAPP_HOOK_URL=http://localhost:5000/bo
#### Download image - ARM
If you're using ARM (like Apple Silicon, Apple M1, etc.) - use following commands to download the image
If you're using ARM (like Apple Silicon, Apple M1, etc.) - use following
commands to download the image
![](/images/versions/core.png) For Core version the command is
```bash
# Download the image
docker pull devlikeapro/whatsapp-http-api:arm
@@ -47,12 +50,16 @@ docker run -it --rm --network=host -e WHATSAPP_HOOK_URL=http://localhost:5000/bo
2. Open Swagger API in the browser http://localhost:3000/
3. Start session and scan QR code in swagger
1. Find `POST /api/session/start` request press **Try it out** and **Execute** it with `default` session name
2. Find `GET /api/screenshot` and execute it - it must show QR code
3. Scan QR code on your mobile WhatsApp application (that installed on your phone)
4. Execute `GET /api/screenshot` once again - it must show the screenshot from WhatsApp Web.
4. Send test text message - find `POST /api/sendText` and execute it with payload (change `12132132130` in the `chatId`
to phone number that is registered in WhatsApp).
1. Find `POST /api/session/start` request press **Try it out** and
**Execute** it with `default` session name
2. Find `GET /api/screenshot` and execute it - it must show QR code
3. Scan QR code on your mobile WhatsApp application (that installed on your
phone)
4. Execute `GET /api/screenshot` once again - it must show the screenshot
from WhatsApp Web.
4. Send test text message - find `POST /api/sendText` and execute it with
payload (change `12132132130` in the `chatId` to phone number that is
registered in WhatsApp).
```json
{
@@ -66,9 +73,9 @@ If you see **Hi there!** message then you're ready to run bots!
## WhatsApp Echo Bot
The WhatsApp Echo Bot is a sample flask webhook server application that echoes back to you whatever you send it. It can
serve
as a basic reference for how to set up webhooks and reply to incoming messages.
The WhatsApp Echo Bot is a sample flask webhook server application that echoes
back to you whatever you send it. It can serve as a basic reference for how to
set up webhooks and reply to incoming messages.
```bash
# Clone the git repository with example
@@ -81,16 +88,19 @@ python -mpip install -r requirements.txt
FLASK_APP=whatsapp_echo_bot.py flask run
```
Open http://127.0.0.1:5000/bot - if you see **WhatsApp Echo Bot is ready!** then the bot is ready to receive message!
Open http://127.0.0.1:5000/bot - if you see **WhatsApp Echo Bot is ready!** then
the bot is ready to receive message!
Send message to the WhatsApp (that you used to scan QR code) and it'll echo text back to you!
Send message to the WhatsApp (that you used to scan QR code) and it'll echo text
back to you!
## WhatsApp Download Files Bot
The WhatsApp Download Image Bot downloads all files people send to your WhatsApp and log the path for the file.
The WhatsApp Download Image Bot downloads all files people send to your WhatsApp
and log the path for the file.
**The bot works only with WAHA Plus version** available with donations.
Visit [read more about difference between Core and Plus versions](https://waha.devlike.pro/docs/how-to/plus-version/).
**The bot works only with WAHA Plus version** available with donations. Visit
[read more about difference between Core and Plus versions](https://waha.devlike.pro/docs/how-to/plus-version/).
Download **WAHA Plus** version:
@@ -114,5 +124,5 @@ python -mpip install -r requirements.txt
FLASK_APP=whatsapp_download_files_bot.py flask run
```
Open http://127.0.0.1:5000/bot - if you see **WhatsApp Download Files Bot!** then the bot is ready to receive message
with files!
Open http://127.0.0.1:5000/bot - if you see **WhatsApp Download Files Bot!**
then the bot is ready to receive message with files!
+1 -3
View File
@@ -2,8 +2,6 @@
"collection": "@nestjs/schematics",
"sourceRoot": "src",
"compilerOptions": {
"plugins": [
"@nestjs/swagger"
]
"plugins": ["@nestjs/swagger"]
}
}
+1 -1
View File
@@ -14,7 +14,7 @@
"start:debug": "nest start --debug --watch",
"start:prod": "node --unhandled-rejections=none dist/main",
"start:prod-exit": "node dist/main",
"lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix",
"lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix --max-warnings=0",
"test": "jest",
"test:watch": "jest --watch",
"test:cov": "jest --coverage",
+54
View File
@@ -0,0 +1,54 @@
import {
CallHandler,
ExecutionContext,
Injectable,
NestInterceptor,
StreamableFile,
} from '@nestjs/common';
import { Request, Response } from 'express';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';
import { Base64File } from '../structures/files.dto';
@Injectable()
/**
* Interceptor to convert Buffer response to Base64File or StreamableFile
* based on Accept header.
* It should go together with @ApiFileAcceptHeader in controller method
* So we have proper Swagger documentation
*/
export class BufferResponseInterceptor<T>
implements NestInterceptor<Buffer, Base64File | StreamableFile>
{
intercept(
context: ExecutionContext,
next: CallHandler,
): Observable<Base64File | StreamableFile> {
const ctx = context.switchToHttp();
const request = ctx.getRequest<Request>();
const response = ctx.getResponse<Response>();
return next.handle().pipe(map(processBuffer));
function processBuffer(buffer: Buffer) {
// Check buffer is Buffer
if (!Buffer.isBuffer(buffer)) {
return buffer;
}
const accept = request.headers['accept'];
if (accept == 'application/json') {
return {
mimetype: 'image/png',
data: buffer.toString('base64'),
};
}
const file = new StreamableFile(buffer);
response.set({
'Content-Type': 'image/png',
'Content-Length': buffer.length,
});
return file;
}
}
}
+31 -19
View File
@@ -1,14 +1,27 @@
import { Body, Controller, Get, Post, Res } from '@nestjs/common';
import {
Body,
Controller,
Get,
Post,
Query,
UseInterceptors,
} 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 { WhatsappSession } from '../core/abc/session.abc';
import { OTPRequest, RequestCodeRequest } from '../structures/auth.dto';
import { SessionApiParam, SessionParam } from './helpers';
import {
OTPRequest,
QRCodeFormat,
QRCodeQuery,
QRCodeValue,
RequestCodeRequest,
} from '../structures/auth.dto';
import { WAHASessionStatus } from '../structures/enums.dto';
import { UnprocessableEntityException } from '@nestjs/common/exceptions/unprocessable-entity.exception';
import { Readable } from 'stream';
import { Response } from 'express';
import { Base64File } from '../structures/files.dto';
import { BufferResponseInterceptor } from './BufferResponseInterceptor';
import { ApiFileAcceptHeader, SessionApiParam, SessionParam } from './helpers';
@ApiSecurity('api_key')
@Controller('api/:session/auth')
@@ -17,26 +30,25 @@ class AuthController {
constructor(private manager: SessionManager) {}
@Get('qr')
@SessionApiParam
@ApiOperation({
summary: 'Get QR code for pairing WhatsApp Web.',
})
async getQR(@Res() res: Response, @SessionParam session: WhatsappSession) {
@SessionApiParam
@ApiFileAcceptHeader(Base64File, QRCodeValue)
@UseInterceptors(new BufferResponseInterceptor())
async getQR(
@SessionParam session: WhatsappSession,
@Query() query: QRCodeQuery,
): Promise<Buffer | QRCodeValue> {
if (session.status != WAHASessionStatus.SCAN_QR_CODE) {
const err = `Can get QR code only in SCAN_QR_CODE status. The current status is '${session.status}'`;
throw new UnprocessableEntityException(err);
}
const buffer = await session.getQR();
const stream = new Readable();
stream.push(buffer);
stream.push(null);
res.set({
'Content-Type': 'image/png',
'Content-Length': buffer.length,
});
stream.pipe(res);
const qr = session.getQR();
if (query.format == QRCodeFormat.RAW) {
return { value: qr.raw };
}
return qr.get();
}
@Post('request-code')
+2 -2
View File
@@ -3,9 +3,9 @@ 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';
import { GetChatMessagesQuery } from '../structures/chats.dto';
import { SessionApiParam, SessionParam } from './helpers';
@ApiSecurity('api_key')
@Controller('api/:session/chats')
+1
View File
@@ -67,6 +67,7 @@ export class ContactsController {
const whatsapp = this.manager.getSession(request.session);
return whatsapp.blockContact(request);
}
@Post('/unblock')
@ApiOperation({ summary: 'Unblock contact' })
unblock(@Body() request: ContactRequest) {
+27 -1
View File
@@ -23,16 +23,42 @@ export function serializeError(err: unknown) {
@Catch()
export class AllExceptionsFilter implements ExceptionFilter {
catch(exception: HttpException | Error, host: ArgumentsHost): void {
catch(exception: any | Error, host: ArgumentsHost): void {
const ctx = host.switchToHttp();
const response = ctx.getResponse<Response>();
const request = ctx.getRequest<Request>();
/**
* If file not found - we get weird 500 error
* So we convert that exception manually to 404 and send appropriate JSON response
* @issue https://github.com/devlikeapro/whatsapp-http-api/issues/134
* @solution https://github.com/nestjs/serve-static/issues/139#issuecomment-612429557
*/
if (exception.code === 'ENOENT') {
response.status(HttpStatus.NOT_FOUND).json({
error: {
code: 404,
key: 'FILE_NOT_FOUND',
message: 'File not found',
details: 'File not found or no longer available',
},
});
response.send();
return;
}
/**
* If it's HttpException - pass it as is
*/
if (exception instanceof HttpException) {
response.status(exception.getStatus()).json(exception.getResponse());
return;
}
/**
* If it's not HttpException - pass it as 500 error
* And send JSON response with error details
*/
const httpStatus = HttpStatus.INTERNAL_SERVER_ERROR;
response.status(httpStatus).json({
statusCode: httpStatus,
+14
View File
@@ -15,6 +15,7 @@ import {
CreateGroupRequest,
DescriptionRequest,
ParticipantsRequest,
SettingsSecurityChangeInfo,
SubjectRequest,
} from '../structures/groups.dto';
import { SessionApiParam, SessionParam } from './helpers';
@@ -62,6 +63,19 @@ export class GroupsController {
return session.setInfoAdminsOnly(id, true);
}
@Get(':id/settings/security/info-admin-only')
@SessionApiParam
@ApiOperation({
summary:
'Gets the group settings to only allow admins to edit group info (title, description, photo).',
})
getInfoAdminOnly(
@SessionParam session: WhatsappSession,
@Param('id') id: string,
): Promise<SettingsSecurityChangeInfo> {
return session.getInfoAdminsOnly(id);
}
@Delete(':id')
@SessionApiParam
@ApiOperation({ summary: 'Delete the group.' })
+47 -2
View File
@@ -1,8 +1,19 @@
import { Injectable, Param, PipeTransform } from '@nestjs/common';
import { ApiParam } from '@nestjs/swagger';
import {
applyDecorators,
Injectable,
Param,
PipeTransform,
} from '@nestjs/common';
import {
ApiExtraModels,
ApiParam,
ApiResponse,
getSchemaPath,
} from '@nestjs/swagger';
import { SessionManager } from '../core/abc/manager.abc';
import { WhatsappSession } from '../core/abc/session.abc';
import { Base64File } from '../structures/files.dto';
/**
* Get session name and return Whatsapp session back (if exists)
@@ -25,6 +36,9 @@ export const SessionApiParam = ApiParam({
name: 'session',
required: true,
type: 'string',
schema: {
default: 'default',
},
description: 'WhatsApp session name',
});
@@ -33,3 +47,34 @@ export const SessionApiParam = ApiParam({
@SessionParam session: WhatsappSession,
*/
export const SessionParam = Param('session', SessionPipe);
function getRefSchemaPaths(models) {
return models.map((model) => {
return { $ref: getSchemaPath(model) };
});
}
export function ApiFileAcceptHeader(...models) {
models = models.length ? models : [Base64File];
return applyDecorators(
// Add extra models, otherwise it'll give a error
// $ref not found
ApiExtraModels(...models),
ApiResponse({
status: 200,
content: {
'image/png': {
schema: {
type: 'string',
format: 'binary',
},
},
'application/json': {
schema: {
oneOf: getRefSchemaPaths(models),
},
},
},
}),
);
}
+1 -1
View File
@@ -10,12 +10,12 @@ import { ApiOperation, ApiSecurity, ApiTags } from '@nestjs/swagger';
import { SessionManager } from '../core/abc/manager.abc';
import { WhatsappSession } from '../core/abc/session.abc';
import { WAHAPresenceStatus } from '../structures/enums.dto';
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')
+17 -13
View File
@@ -1,10 +1,18 @@
import { Controller, Get, Query, Res } from '@nestjs/common';
import {
Controller,
Get,
Query,
Res,
StreamableFile,
UseInterceptors,
} from '@nestjs/common';
import { ApiSecurity, ApiTags } from '@nestjs/swagger';
import { Response } from 'express';
import { Readable } from 'stream';
import { SessionManager } from '../core/abc/manager.abc';
import { SessionQuery } from '../structures/base.dto';
import { BufferResponseInterceptor } from './BufferResponseInterceptor';
import { ApiFileAcceptHeader } from './helpers';
@ApiSecurity('api_key')
@Controller('api')
@@ -13,17 +21,13 @@ export class ScreenshotController {
constructor(private manager: SessionManager) {}
@Get('/screenshot')
async screenshot(@Res() res: Response, @Query() sessionQuery: SessionQuery) {
@UseInterceptors(new BufferResponseInterceptor())
@ApiFileAcceptHeader()
async screenshot(
@Res({ passthrough: true }) res: Response,
@Query() sessionQuery: SessionQuery,
) {
const whatsappService = this.manager.getSession(sessionQuery.session);
const buffer = await whatsappService.getScreenshot();
const stream = new Readable();
stream.push(buffer);
stream.push(null);
res.set({
'Content-Type': 'image/png',
'Content-Length': buffer.length,
});
stream.pipe(res);
return await whatsappService.getScreenshot();
}
}
+3 -6
View File
@@ -1,5 +1,4 @@
import { Body, Controller, Get, Post, Query } from '@nestjs/common';
import { UnprocessableEntityException } from '@nestjs/common/exceptions/unprocessable-entity.exception';
import {
ApiExtraModels,
ApiOperation,
@@ -8,6 +7,7 @@ import {
} from '@nestjs/swagger';
import { SessionManager } from '../core/abc/manager.abc';
import { WhatsappSession } from '../core/abc/session.abc';
import { parseBool } from '../helpers';
import {
ListSessionsQuery,
@@ -19,9 +19,6 @@ import {
SessionStopRequest,
} from '../structures/sessions.dto';
import { SessionApiParam, SessionParam } from './helpers';
import { WhatsappSession } from '../core/abc/session.abc';
import { WAHAChatPresences } from '../structures/presence.dto';
import { WAHAWebhook } from '../structures/webhooks.dto';
@ApiSecurity('api_key')
@Controller('api/sessions')
@@ -34,7 +31,7 @@ class SessionsController {
const result = await this.manager.start(request);
await this.manager.sessionStorage.configRepository.save(
request.name,
request.config,
request.config || null,
);
return result;
}
@@ -71,7 +68,7 @@ class SessionController {
@Get('me')
@SessionApiParam
@ApiOperation({ summary: 'Get information about the authenticated account' })
getMe(@SessionParam session: WhatsappSession): Promise<MeInfo> {
getMe(@SessionParam session: WhatsappSession): Promise<MeInfo | null> {
return session.getSessionMeInfo();
}
}
+1 -1
View File
@@ -11,13 +11,13 @@ 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 {
ImageStatus,
TextStatus,
VideoStatus,
VoiceStatus,
} from '../structures/status.dto';
import { SessionApiParam, SessionParam } from './helpers';
@ApiSecurity('api_key')
@Controller('api/:session/status')
+16 -3
View File
@@ -36,9 +36,17 @@ export class WhatsappConfigService {
return this.configService.get<number>('WHATSAPP_FILES_LIFETIME', 180);
}
get mimetypes(): string[] | null {
get mimetypes(): string[] {
if (!this.shouldDownloadMedia) {
return ['mimetype/ignore-all-media'];
}
const types = this.configService.get('WHATSAPP_FILES_MIMETYPES', '');
return types ? types.split(',') : null;
return types ? types.split(',') : [];
}
get shouldDownloadMedia(): boolean {
const value = this.configService.get('WHATSAPP_DOWNLOAD_MEDIA', 'true');
return parseBool(value);
}
get startSessions(): string[] {
@@ -111,7 +119,7 @@ export class WhatsappConfigService {
return WAHAEngine.WEBJS;
}
get(name: string, defaultValue = undefined): any {
get(name: string, defaultValue: any = undefined): any {
return this.configService.get(name, defaultValue);
}
@@ -119,6 +127,11 @@ export class WhatsappConfigService {
return this.configService.get('WHATSAPP_API_KEY', '');
}
getSwaggerEnabled(): boolean {
const value = this.configService.get('WHATSAPP_SWAGGER_ENABLED', 'true');
return parseBool(value);
}
getSwaggerUsernamePassword(): [string, string] | undefined {
const user = this.configService.get('WHATSAPP_SWAGGER_USERNAME', undefined);
const password = this.configService.get(
+3 -1
View File
@@ -1,8 +1,10 @@
export class QR {
private base64: string;
public raw?: string;
save(base64) {
save(base64: string, raw?: string) {
this.base64 = base64.replace(/^data:image\/png;base64,/, '');
this.raw = raw;
}
get(): Buffer {
+2 -4
View File
@@ -9,7 +9,7 @@ import {
SessionStopRequest,
} from '../../structures/sessions.dto';
import { WhatsappSession } from './session.abc';
import { LocalSessionStorage, MediaStorage } from './storage.abc';
import { LocalSessionStorage } from './storage.abc';
import { WebhookConductor } from './webhooks.abc';
export abstract class SessionManager implements OnApplicationShutdown {
@@ -21,8 +21,6 @@ export abstract class SessionManager implements OnApplicationShutdown {
protected abstract get WebhookConductorClass(): typeof WebhookConductor;
protected abstract get MediaStorageClass(): typeof MediaStorage;
abstract onApplicationShutdown(signal?: string);
//
@@ -34,7 +32,7 @@ export abstract class SessionManager implements OnApplicationShutdown {
abstract logout(request: SessionLogoutRequest): Promise<void>;
abstract getSession(name: string, error?: boolean): WhatsappSession;
abstract getSession(name: string): WhatsappSession;
abstract getSessions(all: boolean): Promise<SessionInfo[]>;
}
+28
View File
@@ -0,0 +1,28 @@
abstract class MediaStorage {
abstract save(
messageId: string,
mimetype: string,
buffer: Buffer,
): Promise<string>;
}
interface IEngineMediaProcessor<Message> {
hasMedia(message: Message): boolean;
getFilename(message: Message): string | null;
getMimetype(message: Message): string;
getMessageId(message: Message): string;
getMediaBuffer(message: Message): Promise<Buffer | null>;
}
interface MediaManager {
processMedia<Message>(
processor: IEngineMediaProcessor<Message>,
message: Message,
): Promise<Message>;
}
export { IEngineMediaProcessor, MediaManager, MediaStorage };
+18 -11
View File
@@ -3,6 +3,7 @@ import { EventEmitter } from 'events';
import * as fs from 'fs';
import { MessageId } from 'whatsapp-web.js';
import { OTPRequest, RequestCodeRequest } from '../../structures/auth.dto';
import {
ChatRequest,
CheckNumberStatusQuery,
@@ -29,6 +30,7 @@ import {
import {
CreateGroupRequest,
ParticipantsRequest,
SettingsSecurityChangeInfo,
} from '../../structures/groups.dto';
import { WAHAChatPresences } from '../../structures/presence.dto';
import {
@@ -36,9 +38,6 @@ import {
ProxyConfig,
SessionConfig,
} from '../../structures/sessions.dto';
import { NotImplementedByEngineError } from '../exceptions';
import { LocalSessionStorage, MediaStorage } from './storage.abc';
import { OTPRequest, RequestCodeRequest } from '../../structures/auth.dto';
import {
ImageStatus,
TextStatus,
@@ -46,6 +45,10 @@ import {
VoiceStatus,
} from '../../structures/status.dto';
import { WASessionStatusBody } from '../../structures/webhooks.dto';
import { NotImplementedByEngineError } from '../exceptions';
import { QR } from '../QR';
import { MediaManager } from './media.abc';
import { LocalSessionStorage } from './storage.abc';
const CHROME_PATH = '/usr/bin/google-chrome-stable';
const CHROMIUM_PATH = '/usr/bin/chromium';
@@ -72,11 +75,11 @@ export enum WAHAInternalEvent {
export interface SessionParams {
name: string;
storage: MediaStorage;
mediaManager: MediaManager;
log: ConsoleLogger;
sessionStorage: LocalSessionStorage;
proxyConfig?: ProxyConfig;
sessionConfig: SessionConfig;
sessionConfig?: SessionConfig;
}
export abstract class WhatsappSession {
@@ -84,11 +87,11 @@ export abstract class WhatsappSession {
public engine: WAHAEngine;
public name: string;
protected storage: MediaStorage;
protected mediaManager: MediaManager;
protected log: ConsoleLogger;
protected sessionStorage: LocalSessionStorage;
protected proxyConfig?: ProxyConfig;
public sessionConfig: SessionConfig;
public sessionConfig?: SessionConfig;
private _status: WAHASessionStatus;
@@ -97,7 +100,7 @@ export abstract class WhatsappSession {
log,
sessionStorage,
proxyConfig,
storage,
mediaManager,
sessionConfig,
}: SessionParams) {
this.events = new EventEmitter();
@@ -105,7 +108,7 @@ export abstract class WhatsappSession {
this.proxyConfig = proxyConfig;
this.log = log;
this.sessionStorage = sessionStorage;
this.storage = storage;
this.mediaManager = mediaManager;
this.sessionConfig = sessionConfig;
}
@@ -192,7 +195,7 @@ export abstract class WhatsappSession {
* Auth methods
*/
public getQR(): Promise<Buffer> {
public getQR(): QR {
throw new NotImplementedByEngineError();
}
@@ -204,7 +207,7 @@ export abstract class WhatsappSession {
throw new NotImplementedByEngineError();
}
abstract getScreenshot(): Promise<Buffer | string>;
abstract getScreenshot(): Promise<Buffer>;
public getSessionMeInfo(): Promise<MeInfo | null> {
throw new NotImplementedByEngineError();
@@ -320,6 +323,10 @@ export abstract class WhatsappSession {
throw new NotImplementedByEngineError();
}
public getInfoAdminsOnly(id): Promise<SettingsSecurityChangeInfo> {
throw new NotImplementedByEngineError();
}
public setInfoAdminsOnly(id, value) {
throw new NotImplementedByEngineError();
}
+6 -11
View File
@@ -1,17 +1,12 @@
import { SessionConfig } from '../../structures/sessions.dto';
abstract class MediaStorage {
abstract save(
messageId: string,
mimetype: string,
buffer: Buffer,
): Promise<string>;
}
abstract class SessionConfigRepository {
abstract save(sessionName: string, config: SessionConfig): Promise<void>;
abstract save(
sessionName: string,
config: SessionConfig | null,
): Promise<void>;
abstract get(sessionName: string): Promise<SessionConfig>;
abstract get(sessionName: string): Promise<SessionConfig | null>;
}
abstract class SessionStorage {
@@ -37,4 +32,4 @@ abstract class LocalSessionStorage extends SessionStorage {
abstract getFolderPath(sessionName: string): string;
}
export { LocalSessionStorage, MediaStorage, SessionConfigRepository };
export { LocalSessionStorage, SessionConfigRepository };
+3 -3
View File
@@ -3,6 +3,8 @@ import { ConfigModule } from '@nestjs/config';
import { PassportModule } from '@nestjs/passport';
import { ServeStaticModule } from '@nestjs/serve-static';
import { AuthController } from '../api/auth.controller';
import { ChatsController } from '../api/chats.controller';
import { ChattingController } from '../api/chatting.controller';
import { ContactsController } from '../api/contacts.controller';
import { GroupsController } from '../api/groups.controller';
@@ -12,13 +14,11 @@ import {
SessionController,
SessionsController,
} from '../api/sessions.controller';
import { StatusController } from '../api/status.controller';
import { VersionController } from '../api/version.controller';
import { WhatsappConfigService } from '../config.service';
import { SessionManager } from './abc/manager.abc';
import { SessionManagerCore } from './manager.core';
import { AuthController } from '../api/auth.controller';
import { ChatsController } from '../api/chats.controller';
import { StatusController } from '../api/status.controller';
export const IMPORTS = [
ConfigModule.forRoot({
+4 -4
View File
@@ -9,7 +9,7 @@ import { WhatsappSession } from './abc/session.abc';
export function getProxyConfig(
config: WhatsappConfigService,
sessions: Record<string, WhatsappSession>,
sessionName?: string,
sessionName: string,
): ProxyConfig | undefined {
// Single proxy server configuration
if (typeof config.proxyServer === 'string') {
@@ -23,7 +23,7 @@ export function getProxyConfig(
// Multiple proxy server configuration
if (Array.isArray(config.proxyServer)) {
const prefix = config.proxyServerIndexPrefix;
let indexToUse: number = undefined;
let indexToUse: number | undefined = undefined;
if (prefix && sessionName.includes(prefix)) {
// match numbers at the end of the string
const matches = sessionName.match(/\d+$/);
@@ -66,8 +66,8 @@ function getAuthenticatedUrl(
export function createAgentProxy(proxyConfig: ProxyConfig): Agent | undefined {
const url = getAuthenticatedUrl(
proxyConfig.server,
proxyConfig.username,
proxyConfig.password,
proxyConfig.username || '',
proxyConfig.password || '',
);
return new HttpsProxyAgent(url);
}
+19 -19
View File
@@ -18,17 +18,14 @@ import {
} from '../structures/sessions.dto';
import { WebhookConfig } from '../structures/webhooks.config.dto';
import { SessionManager } from './abc/manager.abc';
import {
SessionParams,
WAHAInternalEvent,
WhatsappSession,
} from './abc/session.abc';
import { SessionParams, WhatsappSession } from './abc/session.abc';
import { DOCS_URL } from './exceptions';
import { getProxyConfig } from './helpers.proxy';
import { CoreMediaManager, MediaStorageCore } from './media.core';
import { WhatsappSessionNoWebCore } from './session.noweb.core';
import { WhatsappSessionVenomCore } from './session.venom.core';
import { WhatsappSessionWebJSCore } from './session.webjs.core';
import { MediaStorageCore, SessionStorageCore } from './storage.core';
import { SessionStorageCore } from './storage.core';
import { WebhookConductorCore } from './webhooks.core';
export class OnlyDefaultSessionIsAllowed extends UnprocessableEntityException {
@@ -45,11 +42,9 @@ export function buildLogger(name) {
@Injectable()
export class SessionManagerCore extends SessionManager {
private session: WhatsappSession;
private session?: WhatsappSession;
DEFAULT = 'default';
// @ts-ignore
protected MediaStorageClass = MediaStorageCore;
// @ts-ignore
protected WebhookConductorClass = WebhookConductorCore;
protected readonly EngineClass: typeof WhatsappSession;
@@ -110,13 +105,16 @@ export class SessionManagerCore extends SessionManager {
const name = request.name;
this.log.log(`'${name}' - starting session...`);
const log = buildLogger(`WhatsappSession - ${name}`);
const storage = new this.MediaStorageClass();
const mediaManager = new CoreMediaManager(
new MediaStorageCore(),
this.config.mimetypes,
);
const webhookLog = buildLogger(`Webhook - ${name}`);
const webhook = new this.WebhookConductorClass(webhookLog);
const proxyConfig = this.getProxyConfig(request);
const sessionConfig: SessionParams = {
name,
storage,
mediaManager,
log,
sessionStorage: this.sessionStorage,
proxyConfig: proxyConfig,
@@ -149,7 +147,9 @@ export class SessionManagerCore extends SessionManager {
webhooks = webhooks.concat(request.config.webhooks);
}
const globalWebhookConfig = this.config.getWebhookConfig();
webhooks.push(globalWebhookConfig);
if (globalWebhookConfig) {
webhooks.push(globalWebhookConfig);
}
return webhooks;
}
@@ -162,6 +162,9 @@ export class SessionManagerCore extends SessionManager {
if (request.config?.proxy) {
return request.config.proxy;
}
if (!this.session) {
return undefined;
}
const sessions = { [request.name]: this.session };
return getProxyConfig(this.config, sessions, request.name);
}
@@ -181,16 +184,13 @@ export class SessionManagerCore extends SessionManager {
await this.sessionStorage.clean(request.name);
}
getSession(name: string, error = true): WhatsappSession {
getSession(name: string): WhatsappSession {
this.onlyDefault(name);
const session = this.session;
if (!session) {
if (error) {
throw new NotFoundException(
`We didn't find a session with name '${name}'. Please start it first by using POST /sessions/start request`,
);
}
return;
throw new NotFoundException(
`We didn't find a session with name '${name}'. Please start it first by using POST /sessions/start request`,
);
}
return session;
}
+47
View File
@@ -0,0 +1,47 @@
import { WAMedia } from '../structures/responses.dto';
import {
IEngineMediaProcessor,
MediaManager,
MediaStorage,
} from './abc/media.abc';
import { DOCS_URL } from './exceptions';
export class MediaStorageCore implements MediaStorage {
async save(
messageId: string,
mimetype: string,
buffer: Buffer,
): Promise<string> {
return Promise.resolve(
`Media attachment's available only in WAHA Plus version. ${DOCS_URL}`,
);
}
}
export class CoreMediaManager implements MediaManager {
constructor(
private storage: MediaStorage,
private mimetypes: string[],
) {}
async processMedia<Message>(
processor: IEngineMediaProcessor<Message>,
message: Message,
): Promise<Message> {
if (!processor.hasMedia(message)) {
return message;
}
const mimetype = '';
const filename = processor.getFilename(message);
const url = await this.storage.save('', mimetype, Buffer.from(''));
const media: WAMedia = {
mimetype: mimetype,
filename: filename,
url: url,
};
// @ts-ignore
message.media = media;
return message;
}
}
+44 -18
View File
@@ -13,11 +13,11 @@ import makeWASocket, {
WAMessageKey,
} from '@adiwajshing/baileys';
import { UnprocessableEntityException } from '@nestjs/common';
import * as Buffer from 'buffer';
import * as fs from 'fs/promises';
import { Agent } from 'https';
import * as lodash from 'lodash';
import { PairingCodeResponse } from 'src/structures/auth.dto';
import { Message } from 'whatsapp-web.js';
import { flipObject, splitAt } from '../helpers';
import {
@@ -55,12 +55,14 @@ import {
WAHAPresenceData,
} from '../structures/presence.dto';
import { WAMessage } from '../structures/responses.dto';
import { MeInfo } from '../structures/sessions.dto';
import { BROADCAST_ID, TextStatus } from '../structures/status.dto';
import {
PollVote,
PollVotePayload,
WAMessageAckBody,
} from '../structures/webhooks.dto';
import { IEngineMediaProcessor } from './abc/media.abc';
import {
ensureSuffix,
WAHAInternalEvent,
@@ -72,7 +74,6 @@ import {
} from './exceptions';
import { createAgentProxy } from './helpers.proxy';
import { QR } from './QR';
import { MeInfo } from '../structures/sessions.dto';
// eslint-disable-next-line @typescript-eslint/no-var-requires
const QRCode = require('qrcode');
@@ -179,7 +180,7 @@ export class WhatsappSessionNoWebCore extends WhatsappSession {
if (!this.store) {
return proto.Message.fromObject({});
}
const msg = await this.store.loadMessage(key.remoteJid!, key.id!);
const msg = await this.store.loadMessage(key.remoteJid, key.id);
return msg?.message || undefined;
}
@@ -221,7 +222,7 @@ export class WhatsappSessionNoWebCore extends WhatsappSession {
if (qr) {
this.status = WAHASessionStatus.SCAN_QR_CODE;
QRCode.toDataURL(qr).then((url) => {
this.qr.save(url);
this.qr.save(url, qr);
});
}
});
@@ -255,8 +256,8 @@ export class WhatsappSessionNoWebCore extends WhatsappSession {
/**
* Auth methods
*/
public async getQR(): Promise<Buffer> {
return Promise.resolve(this.qr.get());
public getQR(): QR {
return this.qr;
}
public async requestCode(
@@ -287,7 +288,7 @@ export class WhatsappSessionNoWebCore extends WhatsappSession {
return { code: codeRepr };
}
async getScreenshot(): Promise<Buffer | string> {
async getScreenshot(): Promise<Buffer> {
if (this.status === WAHASessionStatus.STARTING) {
throw new UnprocessableEntityException(
`The session is starting, please try again after few seconds`,
@@ -659,10 +660,10 @@ export class WhatsappSessionNoWebCore extends WhatsappSession {
body: body,
to: toCusFormat(fromToParticipant.to),
participant: toCusFormat(fromToParticipant.participant),
// @ts-ignore
hasMedia: Boolean(message.mediaUrl),
// @ts-ignore
mediaUrl: message.mediaUrl,
// Media
hasMedia: Boolean(message.media),
media: message.media,
mediaUrl: message.media?.url,
// @ts-ignore
ack: message.ack,
// @ts-ignore
@@ -811,14 +812,39 @@ export class WhatsappSessionNoWebCore extends WhatsappSession {
return { id: chatId, presences: presences };
}
protected async downloadMedia(message: Message) {
if (!message.hasMedia) {
return message;
}
protected downloadMedia(message) {
const processor = new EngineMediaProcessor(this);
return this.mediaManager.processMedia(processor, message);
}
}
// @ts-ignore
message.mediaUrl = await this.storage.save(message.key.id, '', undefined);
return message;
export class EngineMediaProcessor implements IEngineMediaProcessor<any> {
constructor(public session: WhatsappSessionNoWebCore) {}
hasMedia(message: any): boolean {
const messageType = Object.keys(message.message)[0];
const hasMedia =
messageType === 'imageMessage' ||
messageType == 'audioMessage' ||
messageType == 'documentMessage' ||
messageType == 'videoMessage';
return hasMedia;
}
getMessageId(message: any): string {
return '';
}
getMimetype(message: any): string {
return '';
}
getMediaBuffer(message: any): Promise<Buffer | null> {
return Promise.resolve(undefined);
}
getFilename(message: any): string | null {
return message.message?.documentMessage?.fileName || null;
}
}
+47 -12
View File
@@ -1,4 +1,5 @@
import { UnprocessableEntityException } from '@nestjs/common/exceptions/unprocessable-entity.exception';
import * as Buffer from 'buffer';
import { create, CreateConfig, Message, Whatsapp } from 'venom-bot';
import {
@@ -21,6 +22,7 @@ import {
WAHASessionStatus,
} from '../structures/enums.dto';
import { WAMessage } from '../structures/responses.dto';
import { IEngineMediaProcessor } from './abc/media.abc';
import { WAHAInternalEvent, WhatsappSession } from './abc/session.abc';
import { NotImplementedByEngineError } from './exceptions';
import { QR } from './QR';
@@ -65,7 +67,7 @@ export class WhatsappSessionVenomCore extends WhatsappSession {
protected getCatchQR() {
return (base64Qrimg, asciiQR, attempts, urlCode) => {
this.qr.save(base64Qrimg);
this.qr.save(base64Qrimg, urlCode);
this.status = WAHASessionStatus.SCAN_QR_CODE;
this.log.debug('Number of attempts to read the qrcode: ', attempts);
this.log.log('Terminal qrcode:');
@@ -124,7 +126,15 @@ export class WhatsappSessionVenomCore extends WhatsappSession {
/**
* START - Methods for API
*/
getScreenshot(): Promise<Buffer | string> {
/**
* Auth methods
*/
public getQR(): QR {
return this.qr;
}
getScreenshot(): Promise<Buffer> {
if (this.status === WAHASessionStatus.STARTING) {
throw new UnprocessableEntityException(
`The session is starting, please try again after few seconds`,
@@ -246,17 +256,13 @@ export class WhatsappSessionVenomCore extends WhatsappSession {
* END - Methods for API
*/
protected async downloadAndDecryptMedia(message: Message) {
if (!message.isMMS || !message.isMedia) {
return message;
}
// @ts-ignore
message.mediaUrl = await this.storage.save(message.id, '', undefined);
return message;
protected downloadMedia(message: Message) {
const processor = new EngineMediaProcessor(this);
return this.mediaManager.processMedia(processor, message);
}
private processIncomingMessage(message: Message) {
return this.downloadAndDecryptMedia(message).then(this.toWAMessage);
return this.downloadMedia(message).then(this.toWAMessage);
}
protected toWAMessage(message: Message): Promise<WAMessage> {
@@ -268,10 +274,13 @@ export class WhatsappSessionVenomCore extends WhatsappSession {
fromMe: message.fromMe,
to: message.to,
body: message.body,
// Media
// @ts-ignore
hasMedia: Boolean(message.mediaUrl),
hasMedia: Boolean(message.media),
// @ts-ignore
mediaUrl: message.mediaUrl,
media: message.media,
// @ts-ignore
mediaUrl: message.media?.url,
// @ts-ignore
ack: message.ack,
location: undefined,
@@ -280,3 +289,29 @@ export class WhatsappSessionVenomCore extends WhatsappSession {
});
}
}
export class EngineMediaProcessor implements IEngineMediaProcessor<Message> {
constructor(public session: WhatsappSessionVenomCore) {}
hasMedia(message: any): boolean {
if (!message.isMMS || !message.isMedia) {
return message;
}
}
getMessageId(message: any): string {
return '';
}
getMimetype(message: any): string {
return '';
}
getMediaBuffer(message: any): Promise<Buffer | null> {
return Promise.resolve(undefined);
}
getFilename(message: Message): string | null {
return null;
}
}
+52 -17
View File
@@ -37,16 +37,18 @@ import {
import {
CreateGroupRequest,
ParticipantsRequest,
SettingsSecurityChangeInfo,
} from '../structures/groups.dto';
import { WAMessage } from '../structures/responses.dto';
import { MeInfo } from '../structures/sessions.dto';
import { WAMessageRevokedBody } from '../structures/webhooks.dto';
import { IEngineMediaProcessor } from './abc/media.abc';
import { WAHAInternalEvent, WhatsappSession } from './abc/session.abc';
import {
AvailableInPlusVersion,
NotImplementedByEngineError,
} from './exceptions';
import { QR } from './QR';
import { MeInfo } from '../structures/sessions.dto';
import { WAMessageRevokedBody } from '../structures/webhooks.dto';
// eslint-disable-next-line @typescript-eslint/no-var-requires
const QRCode = require('qrcode');
@@ -144,7 +146,7 @@ export class WhatsappSessionWebJSCore extends WhatsappSession {
this.log.debug('QR received');
// Convert to image and save
QRCode.toDataURL(qr).then((url) => {
this.qr.save(url);
this.qr.save(url, qr);
});
// Print in terminal
qrcode.generate(qr, { small: true });
@@ -171,17 +173,20 @@ export class WhatsappSessionWebJSCore extends WhatsappSession {
/**
* Auth methods
*/
public async getQR(): Promise<Buffer> {
return Promise.resolve(this.qr.get());
public getQR(): QR {
return this.qr;
}
async getScreenshot(): Promise<Buffer | string> {
async getScreenshot(): Promise<Buffer> {
if (this.status === WAHASessionStatus.FAILED) {
throw new UnprocessableEntityException(
`The session under FAILED status. Please try to restart it.`,
);
}
return await this.whatsapp.pupPage.screenshot();
const screenshot = await this.whatsapp.pupPage.screenshot({
encoding: 'binary',
});
return screenshot as Buffer;
}
async checkNumberStatus(
@@ -348,6 +353,14 @@ export class WhatsappSessionWebJSCore extends WhatsappSession {
return this.whatsapp.createGroup(request.name, participantIds);
}
public async getInfoAdminsOnly(id): Promise<SettingsSecurityChangeInfo> {
const groupChat = (await this.whatsapp.getChatById(id)) as GroupChat;
return {
// @ts-ignore
adminsOnly: groupChat.groupMetadata.restrict,
};
}
public async setInfoAdminsOnly(id, value) {
const groupChat = (await this.whatsapp.getChatById(id)) as GroupChat;
return groupChat.setInfoAdminsOnly(value);
@@ -532,10 +545,14 @@ export class WhatsappSessionWebJSCore extends WhatsappSession {
fromMe: message.fromMe,
to: message.to,
body: message.body,
// Media
// @ts-ignore
hasMedia: Boolean(message.mediaUrl),
hasMedia: Boolean(message.media),
// @ts-ignore
media: message.media,
// @ts-ignore
mediaUrl: message.media?.url,
// @ts-ignore
mediaUrl: message.mediaUrl,
ack: message.ack,
ackName: WAMessageAck[message.ack] || ACK_UNKNOWN,
location: message.location,
@@ -550,17 +567,35 @@ export class WhatsappSessionWebJSCore extends WhatsappSession {
return contact;
}
protected async downloadMedia(message: Message) {
protected downloadMedia(message: Message) {
const processor = new EngineMediaProcessor();
return this.mediaManager.processMedia(processor, message);
}
}
export class EngineMediaProcessor implements IEngineMediaProcessor<Message> {
hasMedia(message: Message): boolean {
if (!message.hasMedia) {
return message;
return false;
}
// Can't get media for revoked messages
return message.type !== 'revoked';
}
getMessageId(message: Message): string {
return '';
}
getMimetype(message: Message): string {
return '';
}
getMediaBuffer(message: Message): Promise<Buffer | null> {
return Promise.resolve(undefined);
}
getFilename(message: Message): string | null {
// @ts-ignore
message.mediaUrl = await this.storage.save(
message.id._serialized,
'',
undefined,
);
return message;
return message.rawData?.filename || null;
}
}
+4 -18
View File
@@ -6,22 +6,8 @@ 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(
messageId: string,
mimetype: string,
buffer: Buffer,
): Promise<string> {
return Promise.resolve(
`Media attachment's available only in WAHA Plus version. ${DOCS_URL}`,
);
}
}
class LocalSessionConfigRepository extends SessionConfigRepository {
FILENAME = '.waha.session.config.json';
@@ -30,13 +16,13 @@ class LocalSessionConfigRepository extends SessionConfigRepository {
super();
}
async get(sessionName: string): Promise<SessionConfig> {
async get(sessionName: string): Promise<SessionConfig | null> {
const filepath = this.getFilePath(sessionName);
// Check file exists
try {
await fs.access(filepath, fs.constants.F_OK);
} catch (error) {
return undefined;
return null;
}
// Try to load config
@@ -44,13 +30,13 @@ class LocalSessionConfigRepository extends SessionConfigRepository {
try {
content = await fs.readFile(filepath, 'utf-8');
} catch (error) {
return undefined;
return null;
}
return JSON.parse(content);
}
async save(sessionName: string, config: SessionConfig) {
async save(sessionName: string, config: SessionConfig | null) {
// Create folder if not exist
const folder = this.storage.getFolderPath(sessionName);
await fs.mkdir(folder, { recursive: true });
+1 -6
View File
@@ -1,13 +1,12 @@
import { INestApplication } from '@nestjs/common';
import { DocumentBuilder, OpenAPIObject, SwaggerModule } from '@nestjs/swagger';
import { DECORATORS } from '@nestjs/swagger/dist/constants';
import { WhatsappConfigService } from '../config.service';
import { VERSION } from '../version';
import { DECORATORS } from '@nestjs/swagger/dist/constants';
export class SwaggerModuleCore {
configure(app: INestApplication, webhooks: any[]) {
this.setUpAuth(app);
const builder = new DocumentBuilder();
builder
@@ -130,8 +129,4 @@ export class SwaggerModuleCore {
document.webhooks = webhooks;
return document;
}
protected setUpAuth(app: INestApplication) {
return undefined;
}
}
+2 -3
View File
@@ -5,10 +5,9 @@ import { AllExceptionsFilter } from './api/exception.filter';
import { WhatsappConfigService } from './config.service';
import { AppModuleCore } from './core/app.module.core';
import { SwaggerModuleCore } from './core/swagger.module.core';
import { getWAHAVersion, VERSION, WAHAVersion } from './version';
import { WAHA_WEBHOOKS } from './structures/webhooks.dto';
import { LogLevel } from '@nestjs/common';
import { getLogLevels } from './helpers';
import { WAHA_WEBHOOKS } from './structures/webhooks.dto';
import { getWAHAVersion, VERSION, WAHAVersion } from './version';
async function loadModules(): Promise<
[typeof AppModuleCore, typeof SwaggerModuleCore]
+13
View File
@@ -1,11 +1,24 @@
import { ApiProperty } from '@nestjs/swagger';
export enum QRCodeFormat {
IMAGE = 'image',
RAW = 'raw',
}
export class QRCodeQuery {
format: QRCodeFormat = QRCodeFormat.IMAGE;
}
export class QRCodeValue {
value: string;
}
export class RequestCodeRequest {
@ApiProperty({
description: 'Mobile phone number in international format',
example: '12132132130',
})
phoneNumber: string;
@ApiProperty({
description:
'How would you like to receive the one time code for registration? |sms|voice. Leave empty for Web pairing.',
+11 -7
View File
@@ -1,4 +1,9 @@
import { ApiExtraModels, ApiProperty, getSchemaPath } from '@nestjs/swagger';
import {
ApiExtraModels,
ApiHideProperty,
ApiProperty,
getSchemaPath,
} from '@nestjs/swagger';
import { IsNumber, IsString } from 'class-validator';
import { SessionBaseRequest, SessionQuery } from './base.dto';
@@ -23,6 +28,7 @@ export class CheckNumberStatusQuery extends SessionQuery {
export class MessageTextQuery extends SessionQuery {
@IsString()
phone: string;
@IsString()
text: string;
}
@@ -35,6 +41,7 @@ export class ChatQuery extends SessionQuery {
export class GetMessageQuery extends ChatQuery {
@IsNumber()
limit: number;
@ApiProperty({
example: true,
required: false,
@@ -85,12 +92,7 @@ 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'],
})
@ApiHideProperty()
mentions?: string[];
}
@@ -147,6 +149,7 @@ export class MessageVideoRequest extends ChatRequest {
],
})
file: VideoRemoteFile | VideoBinaryFile;
caption: string = 'Just watch at this!';
}
@@ -192,6 +195,7 @@ export class MessageDestination {
example: 'false_11111111111@c.us_AAAAAAAAAAAAAAAAAAAA',
})
id: string;
to: string;
from: string;
fromMe: boolean;
+6 -2
View File
@@ -1,4 +1,8 @@
import { ApiProperty } from '@nestjs/swagger';
export class Base64File {
mimetype: string;
data: string;
}
export class RemoteFile {
@ApiProperty({
@@ -11,7 +15,7 @@ export class RemoteFile {
description: 'Document file name. Value can be null',
example: 'filename.jpg',
})
filename?: string | null;
filename?: string;
@ApiProperty({
example:
@@ -31,7 +35,7 @@ export class BinaryFile {
description: 'Document file name. Optional',
example: 'filename.jpeg',
})
filename?: string | null;
filename?: string;
@ApiProperty({
description: 'Base64-encoded data of the file',
+4
View File
@@ -8,6 +8,10 @@ export class Participant {
id: string;
}
export class SettingsSecurityChangeInfo {
adminsOnly: boolean;
}
/**
* Queries
*/
+2
View File
@@ -8,6 +8,7 @@ export class WAHAPresenceData {
example: '11111111111@c.us',
})
participant: string;
lastKnownPresence: WAHAPresenceStatus;
@ApiProperty({
example: 1686568773,
@@ -20,6 +21,7 @@ export class WAHAChatPresences {
example: '11111111111@c.us',
})
id: string;
presences: WAHAPresenceData[];
}
+2 -3
View File
@@ -1,7 +1,6 @@
import { ApiProperty } from '@nestjs/swagger';
import { WAHAEvents } from './enums.dto';
import { ApiProperty, ApiPropertyOptions } from '@nestjs/swagger';
function ChatIdProperty(options = undefined) {
function ChatIdProperty(options: ApiPropertyOptions | null = null) {
options = options || {};
if (!options.example) {
options.example = '11111111111@c.us';
+27 -2
View File
@@ -4,11 +4,32 @@ import { WAMessageAck } from './enums.dto';
import { ChatIdProperty, MessageIdProperty } from './properties.dto';
export class WALocation {
description?: string | null;
description?: string;
latitude: string;
longitude: string;
}
export class WAMedia {
@ApiProperty({
description: 'The URL for the media in the message if any',
example:
'http://localhost:3000/api/files/false_11111111111@c.us_AAAAAAAAAAAAAAAAAAAA.oga',
})
url: string;
@ApiProperty({
description: 'mimetype for the media in the message if any',
example: 'audio/jpeg',
})
mimetype: string | null;
@ApiProperty({
description: 'The original filename in mediaUrl in the message if any',
example: 'example.pdf',
})
filename: string | null;
}
export class WAMessage {
@MessageIdProperty()
id: string;
@@ -55,8 +76,12 @@ export class WAMessage {
})
hasMedia: boolean;
media: WAMedia | null = null;
@ApiProperty({
description: 'The URL for the media in the message if any',
description:
'Use `media.url` instead! The URL for the media in the message if any',
deprecated: true,
example:
'http://localhost:3000/api/files/false_11111111111@c.us_AAAAAAAAAAAAAAAAAAAA.oga',
})
+6 -3
View File
@@ -1,8 +1,8 @@
import { ApiProperty } from '@nestjs/swagger';
import { WAHASessionStatus } from './enums.dto';
import { WebhookConfig } from './webhooks.config.dto';
import { ChatIdProperty } from './properties.dto';
import { WebhookConfig } from './webhooks.config.dto';
/**
* Queries
@@ -25,10 +25,12 @@ export class ProxyConfig {
example: 'localhost:3128',
})
server: string;
@ApiProperty({
example: null,
})
username?: string;
@ApiProperty({
example: null,
})
@@ -65,15 +67,16 @@ export class SessionLogoutRequest {
export class SessionDTO {
name = 'default';
status: WAHASessionStatus;
config: SessionConfig;
config?: SessionConfig;
}
export class MeInfo {
@ChatIdProperty()
id: string;
pushName: string;
}
export class SessionInfo extends SessionDTO {
me: MeInfo | null;
me?: MeInfo;
}
+4
View File
@@ -1,4 +1,5 @@
import { ApiExtraModels, ApiProperty, getSchemaPath } from '@nestjs/swagger';
import {
BinaryFile,
RemoteFile,
@@ -32,6 +33,7 @@ export class ImageStatus extends StatusRequest {
],
})
file: BinaryFile | RemoteFile;
caption: string;
}
@@ -44,6 +46,7 @@ export class VoiceStatus extends StatusRequest {
],
})
file: VoiceBinaryFile | VoiceRemoteFile;
backgroundColor = '#38b42f';
}
@@ -56,5 +59,6 @@ export class VideoStatus extends StatusRequest {
],
})
file: BinaryFile | RemoteFile;
caption: string;
}
+17 -5
View File
@@ -1,17 +1,17 @@
import { ApiProperty } from '@nestjs/swagger';
import { MessageDestination } from './chatting.dto';
import {
WAHAEngine,
WAHAEvents,
WAHASessionStatus,
WAMessageAck,
} from './enums.dto';
import { WAMessage } from './responses.dto';
import { MessageDestination } from './chatting.dto';
import { ChatIdProperty, MessageIdProperty } from './properties.dto';
import { WAHAChatPresences } from './presence.dto';
import { MeInfo } from './sessions.dto';
import { WAHAEnvironment } from './environment.dto';
import { WAHAChatPresences } from './presence.dto';
import { ChatIdProperty, MessageIdProperty } from './properties.dto';
import { WAMessage } from './responses.dto';
import { MeInfo } from './sessions.dto';
export class WAMessageAckBody {
@MessageIdProperty()
@@ -92,6 +92,7 @@ export class WASessionStatusBody {
example: 'default',
})
name: string;
status: WAHASessionStatus;
}
@@ -122,12 +123,14 @@ class WAHAWebhookSessionStatus extends WAHAWebhook {
description: 'The event is triggered when the session status changes.',
})
event = WAHAEvents.SESSION_STATUS;
payload: WASessionStatusBody;
}
class WAHAWebhookMessage extends WAHAWebhook {
@ApiProperty({ description: 'Incoming message.' })
event = WAHAEvents.MESSAGE;
payload: WAMessage;
}
@@ -136,6 +139,7 @@ class WAHAWebhookMessageAny extends WAHAWebhook {
description: 'Fired on all message creations, including your own.',
})
event = WAHAEvents.MESSAGE_ANY;
payload: WAMessage;
}
@@ -145,6 +149,7 @@ class WAHAWebhookMessageAck extends WAHAWebhook {
'Receive events when server or recipient gets the message, read or played it.',
})
event = WAHAEvents.MESSAGE_ACK;
payload: WAMessageAckBody;
}
@@ -155,6 +160,7 @@ class WAHAWebhookMessageRevoked extends WAHAWebhook {
'revokes a previously sent message.',
})
event = WAHAEvents.MESSAGE_REVOKED;
payload: WAMessageRevokedBody;
}
@@ -163,6 +169,7 @@ class WAHAWebhookStateChange extends WAHAWebhook {
description: 'It’s an internal engine’s state, not session status.',
})
event = WAHAEvents.STATE_CHANGE;
payload: any;
}
@@ -171,6 +178,7 @@ class WAHAWebhookGroupJoin extends WAHAWebhook {
description: 'Some one join a group.',
})
event = WAHAEvents.GROUP_JOIN;
payload: any;
}
@@ -179,6 +187,7 @@ class WAHAWebhookGroupLeave extends WAHAWebhook {
description: 'Some one left a group.',
})
event = WAHAEvents.GROUP_LEAVE;
payload: any;
}
@@ -187,6 +196,7 @@ class WAHAWebhookPresenceUpdate extends WAHAWebhook {
description: 'The most recent presence information for a chat.',
})
event = WAHAEvents.PRESENCE_UPDATE;
payload: WAHAChatPresences;
}
@@ -195,6 +205,7 @@ class WAHAWebhookPollVote extends WAHAWebhook {
description: 'With this event, you receive new votes for the poll sent.',
})
event = WAHAEvents.POLL_VOTE;
payload: PollVotePayload;
}
@@ -205,6 +216,7 @@ class WAHAWebhookPollVoteFailed extends WAHAWebhook {
'Read more about how to handle such events: https://waha.devlike.pro/docs/how-to/polls/#pollvotefailed',
})
event = WAHAEvents.POLL_VOTE_FAILED;
payload: PollVotePayload;
}
+1 -1
View File
@@ -33,7 +33,7 @@ export function getEngineName(): string {
}
export const VERSION: WAHAEnvironment = {
version: '2023.11.1',
version: '2023.12.1',
engine: getEngineName(),
tier: getWAHAVersion(),
browser: getBrowserExecutablePath(),
+1 -6
View File
@@ -1,9 +1,4 @@
{
"extends": "./tsconfig.json",
"exclude": [
"node_modules",
"test",
"dist",
"**/*spec.ts"
]
"exclude": ["node_modules", "test", "dist", "**/*spec.ts"]
}
+1 -1
View File
@@ -12,6 +12,6 @@
"baseUrl": "./",
"incremental": true,
"resolveJsonModule": true,
"skipLibCheck": true,
"skipLibCheck": true
}
}
+6 -6
View File
@@ -13,8 +13,8 @@ __metadata:
linkType: hard
"@adiwajshing/baileys@github:WhiskeySockets/Baileys":
version: 6.4.1
resolution: "@adiwajshing/baileys@https://github.com/WhiskeySockets/Baileys.git#commit=cd8542ff7c072b5e58d2d3de2396983a5304e5e1"
version: 6.5.0
resolution: "@adiwajshing/baileys@https://github.com/WhiskeySockets/Baileys.git#commit=e24947e55ad6e3370de867b258bb33bb9e828c56"
dependencies:
"@adiwajshing/keyed-db": ^0.2.4
"@hapi/boom": ^9.1.3
@@ -44,7 +44,7 @@ __metadata:
optional: true
sharp:
optional: true
checksum: 4778b35869210130b938b5fb1d49a5b7263d7aaff734fe3ea4747fb06231ef9ffcf646ec653054e086c08d2a1b8833d167ebb975e0ec93251658dd9a081bd790
checksum: b15ff56171157df1d4b55e984c09bf67a5537a179be57bf36e2e97cbdeaf70f1d3ee1dfb3e9d95a916b3e44373dc5f35a9bb8481e9c49bbba83b5ec926b6755d
languageName: node
linkType: hard
@@ -13130,8 +13130,8 @@ __metadata:
linkType: soft
"whatsapp-web.js@https://github.com/pedroslopez/whatsapp-web.js":
version: 1.22.2-alpha.3
resolution: "whatsapp-web.js@https://github.com/pedroslopez/whatsapp-web.js.git#commit=f5555f6aa0d34e5c5b2f6fd5d8e1ee5559528c55"
version: 1.23.1-alpha.0
resolution: "whatsapp-web.js@https://github.com/pedroslopez/whatsapp-web.js.git#commit=58fc39f97d5663f7651faff4c60c12f2e91ce63e"
dependencies:
"@pedroslopez/moduleraid": ^5.0.2
archiver: ^5.3.1
@@ -13150,7 +13150,7 @@ __metadata:
optional: true
unzipper:
optional: true
checksum: d4d030cef0955cadfd199684d9176effdac1e4222100ec994c9d91004618b070bd23d01b04060e6fd988e3c1329a8ba0f38d208093a75f78115676eb6f6b4a4b
checksum: ac8b24ccc79f26dfe28c6ae3b39fa18dae6e3ea3b3831358eaf84e78fa78e67e05adfa1a37ddb6684c88de89a4244d9b589bad375d0ce2e3e9d197f281d15de6
languageName: node
linkType: hard