Compare commits

..
20 Commits
Author SHA1 Message Date
allburov 5c666c0dd4 Use buildjet with arm
Release / docker-amd64 (push) Waiting to run
Release / docker-arm (push) Waiting to run
2023-09-04 18:39:57 +07:00
allburov 3ccc0469c0 Use buildjet 2023-09-04 18:39:55 +07:00
allburov 5ab3244d6a adjust CI 2023-09-04 18:39:52 +07:00
allburov 05bc8abbee [core] Bump version to 2023.9.3 2023-09-03 19:30:47 +07:00
allburov 8e9056a0cd [core] Add /api/sessions/{session}/me endpoint
Fix #191
2023-09-03 19:30:16 +07:00
allburov ea75b357a7 [core] Log engine events if DEBUG enabled for NOWEB 2023-09-03 16:33:08 +07:00
allburov 2b607e9b44 [core] Bump version to 2023.8.29 2023-08-29 07:49:13 +03:00
allburov 57d3528a96 [core] Apply code review suggestions 2023-08-29 07:48:51 +03:00
allburov 660a04aa52 [core] Document polls 2023-08-29 07:48:51 +03:00
allburov d0a408001d [core] poll.vote.failed 2023-08-29 07:48:51 +03:00
allburov 48391533a6 [core] poll.vote 2023-08-29 07:48:51 +03:00
allburov f2113adbbc [core] Send poll 2023-08-29 07:48:51 +03:00
allburov abfcbc7397 [core] Update WEBJS
Release / deploy-docker (push) Waiting to run
2023-08-23 16:40:46 +03:00
allburov 96f2982325 [core] Bump to 2023.8.23 2023-08-23 12:15:06 +03:00
allburov 3a521bc06c [core] Update WEBJS engine 2023-08-23 12:14:51 +03:00
allburov 6fbaec0083 [core] Bump to 2023.8.21 2023-08-21 14:25:06 +03:00
allburov cae94c7b5c [core] catch error while handling incoming message NOWEB 2023-08-21 14:24:51 +03:00
allburov bce012f692 [core] Bump version to 2023.8.19
Release / deploy-docker (push) Waiting to run
2023-08-19 12:36:08 +03:00
allburov b23b639a8b [core] Support message.ack for NOWEB, add ackName 2023-08-19 12:35:44 +03:00
allburov 5a628907ed [core] Add pairing auth method for NOWEB, add QR endpoint 2023-08-13 16:08:56 +03:00
42 changed files with 1249 additions and 189 deletions

No files matched your search

+39 -9
View File
@@ -6,18 +6,12 @@ on:
- "*"
jobs:
deploy-docker:
runs-on: ubuntu-latest
docker-amd64:
runs-on: buildjet-4vcpu-ubuntu-2204
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Set up QEMU
uses: docker/setup-qemu-action@v2
with:
platforms: 'amd64'
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
@@ -26,9 +20,10 @@ jobs:
uses: docker/metadata-action@v4
with:
images: ${{ secrets.DOCKER_IMAGE }}
flavor: latest=true
flavor: latest=false
tags: |
type=semver,pattern={{version}}
type=raw,value=latest
- name: Login to image repository
if: github.ref_type == 'tag'
@@ -46,3 +41,38 @@ jobs:
push: ${{ github.ref_type == 'tag' }}
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
docker-arm:
runs-on: buildjet-4vcpu-ubuntu-2204-arm
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
- name: Docker meta
id: meta
uses: docker/metadata-action@v4
with:
images: ${{ secrets.DOCKER_IMAGE }}
flavor: latest=false
tags: |
type=raw,value=arm
- name: Login to image repository
if: github.ref_type == 'tag'
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKER_USER }}
password: ${{ secrets.DOCKER_TOKEN }}
- name: Build and push
uses: docker/build-push-action@v4
with:
context: .
file: Dockerfile
platforms: 'linux/arm64'
push: ${{ github.ref_type == 'tag' }}
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
+1
View File
@@ -1,4 +1,5 @@
.yarn
example.ts
.*sessions
tokens
files
@@ -6,7 +6,7 @@ date: 2020-10-06T08:48:45+00:00
lastmod: 2020-10-06T08:48:45+00:00
draft: false
images: []
weight: 126
weight: 127
---
Chats methods.
Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

@@ -0,0 +1,219 @@
---
title: "Polls"
description: "How to send polls and receive votes"
lead: ""
date: 2020-10-06T08:48:45+00:00
lastmod: 2020-10-06T08:48:45+00:00
draft: false
images: [ ]
weight: 126
---
**Available on NOWEB engine only**.
Waiting for WEBJS engine to support it, and we'll add it ASAP!
![poll-example.jpg](poll-example.jpg)
## Endpoints
### Send poll
Use the endpoint to send a poll!
```bash
POST /api/sendPoll
```
The request body is pretty simple:
```json
{
"session": "default",
"chatId": "123123123@c.us",
"poll": {
"name": "How are you?",
"options": [
"Awesome!",
"Good!",
"Not bad!"
],
"multipleAnswers": false
}
}
```
The response you get back:
```json
{
"id": "true_321321321@c.us_83ACBAAAAAAAAAAAAAAAAAAAA",
"other-fields-here": "value"
}
```
You must save the `id` field from the response in your database so that you can identify the poll for which you receive
a vote (see webhook events below).
## Events
### poll.vote
With this event, you receive new votes for the poll sent.
#### Vote from a user in direct messages.
```json
{
"event": "poll.vote",
"session": "default",
"payload": {
"vote": {
"id": "false_1111111111@c.us_83ACBE602A05C79B234B54415E95EE8A",
"to": "me",
"from": "1111111@c.us",
"fromMe": false,
"selectedOptions": [
"Awesome!"
],
"timestamp": 1692861427
},
"poll": {
"id": "true_1111111111@c.us_BAE5F2EF5C69001E",
"to": "1111111111@c.us",
"from": "me",
"fromMe": true
}
},
"engine": "NOWEB"
}
```
#### Do I receive votes only for my polls?
No, you receive all votes. Keep in mind that you'll get all votes with this event, even from other polls. To identify
that it's your poll, look at the `poll.fromMe` field.
#### How to handle multiple-answer votes
For `multipleAnswers: true`, you receive the `selectedOptions` with all the selected values at a certain moment. So if a
user has chosen 3 options from the poll, you will receive **3** `poll.vote` events:
1. `selectedOptions: ["First"]`
2. `selectedOptions: ["First", "Second"]`
3. `selectedOptions: ["First", "Second", "Third"]`
#### Timestamp
If a user clicks on the poll multiple times, you will receive multiple `poll.vote` events. This is true for
both `multipleAnswers: false` (when a user changes their mind about answers) and `multipleAnswers: true` (when a user
selects two or more options) events.
There is a little chance that you may receive votes in the wrong order (due to the nature of HTTP and Webhooks),
like `1-3-2` instead of `1-2-3`. To determine the right order, look at the `timestamp` field. The event with a
higher `timestamp` value is more recent.
👉 It's important to save the `timestamp` for each vote in your database and compare them as numbers, without converting to
internal datetime. Right now, the `timestamp` shows the timestamp in seconds, but it may be changed to milliseconds in
the future.
#### Vote from a user in a group
```json
{
"event": "poll.vote",
"session": "default",
"payload": {
"vote": {
"id": "false_3333333333333@g.us_1C18A7EAADD2A8D0324755D241C4238A",
"to": "3333333333333@g.us",
"from": "1111111111@c.us",
"fromMe": false,
"selectedOptions": [
"Awesome!"
],
"timestamp": 1692861427
},
"poll": {
"id": "true_3333333333333@g.us_BAE5304BA1ECF704",
"to": "33333333333333@g.us",
"from": "222222222@c.us",
"fromMe": true
}
},
"engine": "NOWEB"
}
```
### poll.vote.failed
There may be cases when WAHA fails to decrypt a vote from the user. In such cases, you will receive
a `poll.vote.failed` event on your webhook.
The payload for `poll.vote.failed` is the same as for `poll.vote`, but with an empty list in `selectedOptions`.
```json
{
"event": "poll.vote.failed",
"session": "default",
"payload": {
"vote": {
"id": "false_11111111111@c.us_2E8C4CDA89EDE3BC0BC7F605364B8451",
"to": "me",
"from": "111111111@c.us",
"fromMe": false,
"selectedOptions": [],
"timestamp": 1692956972
},
"poll": {
"id": "true_1111111111@c.us_BAE595F4E0A2042C",
"to": "111111111@c.us",
"from": "me",
"fromMe": true
}
},
"engine": "NOWEB"
}
```
#### How should I handle poll.vote.failed events?
When you send a poll, save the poll configuration (question and options) in your database with the `id` field from the
response you received from `POST /api/sendPoll`.
Later, when you receive a `poll.vote.failed` event, find the `id` for the poll in the database and repeat the same
question to the user, apologizing for the inconvenience.
For example, you can say:
> Sorry, we don't understand your choice 😞
>
> Please click one more time on the message below 👇
After the user clicks on the poll again, you will receive a `poll.vote` event with their choice.
![handle-poll-vote-failed.jpg](handle-poll-vote-failed.jpg)
#### How to test poll.vote.failed events?
To receive `poll.vote.failed` events, follow these steps:
1. Start a session and authorize it with a QR code.
2. Send a poll to a chat.
3. Stop the session (logout is not required).
4. Start the session again.
5. Vote on the poll.
6. You will receive a `poll.vote.failed` event.
#### Why does the poll.vote.failed event occur and when will it be fixed?
The issue occurs because WAHA does not have a proper storage system, but polls require proper storage in order to
decrypt votes later.
There will be two fixes for this:
1. In the short term, a [local file storage](https://github.com/devlikeapro/whatsapp-http-api/issues/188) will be used
to save poll keys.
2. In the long term, work is being done
on [remote storages](https://github.com/devlikeapro/whatsapp-http-api/issues/41).
Even after these fixes are implemented, it's better to handle `poll.vote.failed` events anyway, so your application is
prepared for such cases!
Binary file not shown.

After

Width:  |  Height:  |  Size: 50 KiB

@@ -129,7 +129,8 @@ You can subscribe to presence information by calling `POST /api/{session}/presen
You can get later presence information for the chat with above `GET` endpoints or by listening to `presence.update`
webhook.
## Webhook
## Events
### presence.update
You can subscribe to `presence.update` webhook event to get the most recent presence information.
@@ -116,6 +116,9 @@ Fired on all message creations, including your own. The payload is the same as f
```
### message.ack
Receive events when server or recipient gets the message, read or played it.
[See details on Webhooks page ->]({{< relref "webhooks#messageack" >}}).
```json
{
@@ -31,7 +31,7 @@ When sending media (images, voice, files) you can either use:
## Endpoints
### Send text ![](/images/versions/core.png)
### Send text
To send text message - use `POST /api/sendText` with example payload.
```json
{
@@ -58,7 +58,12 @@ also mention it in `mentions` in format `2132132130@c.us`
}
```
### Reply on message ![](/images/versions/core.png)
### Send poll
We have a dedicated page [how to send polls and receive votes]({{< relref "/docs/how-to/polls" >}})!
![](poll-example.jpg)
### Reply on message
To reply on a message - use `POST /api/reply` with example payload.
```json
{
@@ -72,7 +77,7 @@ To reply on a message - use `POST /api/reply` with example payload.
#### Reply files ![](/images/versions/plus-soon.png)
WAHA does not support reply with files (images, voice, etc). If you're interested in it - please create an issue in GitHub.
### Add a reaction ![](/images/versions/core.png)
### Add a reaction
Use `PUT /api/reaction` method to set reaction to a message.
{{< alert icon="👉" text="Reaction API uses PUT, not POST request! Please make sure you send right request." />}}
Binary file not shown.

After

Width:  |  Height:  |  Size: 50 KiB

@@ -9,7 +9,7 @@ images: [ ]
weight: 125
---
## Endpoints
## Sessions
### Start
@@ -128,6 +128,60 @@ In order to log out the session - call `POST /api/sessions/logout`
}
```
### Get me
Get information about the associated account for that session (if any).
```bash
GET /api/sessions/{session}/me
```
**Authenticated and working** session's response:
```json
{
"id": "11111111111@c.us",
"pushName": "string"
}
```
**Stopped** or **not authenticated** session returns null:
```json
null
```
## Authentication
### Get QR
The simplest way to authenticate a new session - get QR code and scan it on your device.
```bash
GET /api/{session}/auth/qr
```
You'll get QR image that you can scan and get authenticated
### Get pairing code
{{< alert icon="👉" text="Available in **NOWEB** engine only." />}}
You can [link a session with phone number](https://faq.whatsapp.com/1324084875126592) - make a request to the endpoint.
```bash
POST /api/{session}/auth/request-code
```
Body example:
```json
{
"phoneNumber": "12132132130"
}
```
You'll get code in the response that you can use on your WhatsApp app to connect the session:
```json
{
"code": "ABCD-ABCD"
}
```
## Advanced sessions ![](/images/versions/plus.png)
@@ -190,14 +190,32 @@ Fired on all message creations, including your own. The payload is the same as f
```
### message.ack
Receive events when server or recipient gets the message, read or played it.
`ackName` field contains message status (`ack` has the same meaning, but show the value in int, but we keep it for backward compatability, they much to each other)
Possible message ack statuses:
- `ackName: ERROR, ack: -1`
- `ackName: PENDING, ack: 0`
- `ackName: SERVER, ack: 1`
- `ackName: DEVICE, ack: 2`
- `ackName: READ, ack: 3`
- `ackName: PLAYED, ack: 4`
The payload may have more fields, it depends on the engine you use, but here's a minimum amount that all engines send:
```json
{
"event": "message.ack",
"session": "default",
"engine": "WEBJS",
"payload": {
...
"id":"true_11111111111@c.us_4CC5EDD64BC22EBA6D639F2AF571346C",
"from":"11111111111@c.us",
"participant": null,
"fromMe":true,
"ack":3,
"ackName":"READ"
}
}
```
@@ -266,6 +284,60 @@ It's an internal engine's state, not **session** `status`.
}
```
### poll.vote
We have a dedicated page [how to send polls and receive votes]({{< relref "/docs/how-to/polls" >}})!
```json
{
"event": "poll.vote",
"session": "default",
"payload": {
"vote": {
"id": "false_1111111111@c.us_83ACBE602A05C79B234B54415E95EE8A",
"to": "me",
"from": "1111111@c.us",
"fromMe": false,
"selectedOptions": ["Awesome!"],
"timestamp": 1692861427
},
"poll": {
"id": "true_1111111111@c.us_BAE5F2EF5C69001E",
"to": "1111111111@c.us",
"from": "me",
"fromMe": true
}
},
"engine": "NOWEB"
}
```
### poll.vote.failed
We have a dedicated page [how to send polls and receive votes]({{< relref "/docs/how-to/polls" >}})!
```json
{
"event": "poll.vote.failed",
"session": "default",
"payload": {
"vote": {
"id": "false_11111111111@c.us_2E8C4CDA89EDE3BC0BC7F605364B8451",
"to": "me",
"from": "111111111@c.us",
"fromMe": false,
"selectedOptions": [],
"timestamp": 1692956972
},
"poll": {
"id": "true_1111111111@c.us_BAE595F4E0A2042C",
"to": "111111111@c.us",
"from": "me",
"fromMe": true
}
},
"engine": "NOWEB"
}
```
## Webhooks Advanced ![](/images/versions/plus.png)
### HMAC authentication
@@ -12,6 +12,15 @@ menu:
weight: 199
toc: true
---
## 2023.9
September 2023
- Add `GET /api/sessions/{session/me` endpoint so you [can get the phone number associated with the session](https://waha.devlike.pro/docs/how-to/sessions/#get-me)
- Add [polls support in NOWEB engine](https://waha.devlike.pro/docs/how-to/polls)
- Add dedicated [Get QR](https://waha.devlike.pro/docs/how-to/sessions/#get-qr) endpoint!
- Support [pairing method (NOWEB)](https://waha.devlike.pro/docs/how-to/sessions/#get-pairing-code) - you can connect with a code instead of QR.
- Add string field `ackName: DEVICE|READ|...` in [message.ack payload](https://waha.devlike.pro/docs/how-to/webhooks/#messageack)
- Support `message.ack` event webhook in **NOWEB** engine
## 2023.8
August 2023
- Added [stories (aka status) endpoints](https://waha.devlike.pro/docs/how-to/send-messages/#send-status-aka-stories) to **NOWEB** engine!
+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","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
{"openapi":"3.0.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. NOWEB and NOWEB_MOBILE engines only","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. NOWEB_MOBILE engine only","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/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"}}}}},"taLine truncated
+1
View File
@@ -38,6 +38,7 @@
"express-basic-auth": "^1.2.1",
"file-type": "16.5.4",
"https-proxy-agent": "^7.0.0",
"libphonenumber-js": "^1.10.36",
"link-preview-js": "^3.0.4",
"lodash": "^4.17.21",
"mime-types": "^2.1.27",
+67
View File
@@ -0,0 +1,67 @@
import { Body, Controller, Get, Post, Res } 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 { OTPRequest, RequestCodeRequest } from '../structures/auth.dto';
import { SessionApiParam, SessionParam } from './helpers';
import { WAHASessionStatus } from '../structures/enums.dto';
import { UnprocessableEntityException } from '@nestjs/common/exceptions/unprocessable-entity.exception';
import { Readable } from 'stream';
import { Response } from 'express';
@ApiSecurity('api_key')
@Controller('api/:session/auth')
@ApiTags('auth')
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) {
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);
}
@Post('request-code')
@SessionApiParam
@ApiOperation({
summary: 'Request authentication code. NOWEB and NOWEB_MOBILE engines only',
})
requestCode(
@SessionParam session: WhatsappSession,
@Body() request: RequestCodeRequest,
) {
return session.requestCode(request.phoneNumber, request.method);
}
@Post('authorize-code')
@SessionApiParam
@ApiOperation({
summary: 'Send OTP authentication code. NOWEB_MOBILE engine only',
})
authorizeCode(
@SessionParam session: WhatsappSession,
@Body() request: OTPRequest,
) {
return session.authorizeCode(request.code);
}
}
export { AuthController };
+10 -1
View File
@@ -11,6 +11,7 @@ import {
MessageImageRequest,
MessageLinkPreviewRequest,
MessageLocationRequest,
MessagePollRequest,
MessageReactionRequest,
MessageReplyRequest,
MessageTextButtonsRequest,
@@ -18,8 +19,9 @@ import {
MessageTextRequest,
MessageVoiceRequest,
SendSeenRequest,
WANumberExistResult,
} from '../structures/chatting.dto';
import { WAMessage, WANumberExistResult } from '../structures/responses.dto';
import { WAMessage } from '../structures/responses.dto';
@ApiSecurity('api_key')
@Controller('api')
@@ -66,6 +68,13 @@ export class ChattingController {
return whatsapp.sendTextButtons(request);
}
@Post('/sendPoll')
@ApiOperation({ summary: 'Send a poll with options' })
sendPoll(@Body() request: MessagePollRequest) {
const whatsapp = this.manager.getSession(request.session);
return whatsapp.sendPoll(request);
}
@Post('/sendLocation')
sendLocation(@Body() request: MessageLocationRequest) {
const whatsapp = this.manager.getSession(request.session);
+4 -2
View File
@@ -3,9 +3,11 @@ import { ApiOperation, ApiSecurity, ApiTags } from '@nestjs/swagger';
import { SessionManager } from '../core/abc/manager.abc';
import { SessionQuery } from '../structures/base.dto';
import { CheckNumberStatusQuery } from '../structures/chatting.dto';
import {
CheckNumberStatusQuery,
WANumberExistResult,
} from '../structures/chatting.dto';
import { ContactQuery, ContactRequest } from '../structures/contacts.dto';
import { WANumberExistResult } from '../structures/responses.dto';
@ApiSecurity('api_key')
@Controller('api/contacts')
+8 -2
View File
@@ -51,8 +51,14 @@ export class GroupsController {
@Put(':id/settings/security/info-admin-only')
@SessionApiParam
@ApiOperation({ summary: 'Updates the group settings to only allow admins to edit group info (title, description, photo).' })
setGroupAdminOnly(@SessionParam session: WhatsappSession, @Param('id') id: string) {
@ApiOperation({
summary:
'Updates the group settings to only allow admins to edit group info (title, description, photo).',
})
setGroupAdminOnly(
@SessionParam session: WhatsappSession,
@Param('id') id: string,
) {
return session.setInfoAdminsOnly(id, true);
}
+23 -2
View File
@@ -6,16 +6,21 @@ import { SessionManager } from '../core/abc/manager.abc';
import { parseBool } from '../helpers';
import {
ListSessionsQuery,
MeInfo,
SessionDTO,
SessionInfo,
SessionLogoutRequest,
SessionStartRequest,
SessionStopRequest,
} from '../structures/sessions.dto';
import { SessionApiParam, SessionParam } from './helpers';
import { WhatsappSession } from '../core/abc/session.abc';
import { WAHAChatPresences } from '../structures/presence.dto';
@ApiSecurity('api_key')
@Controller('api/sessions')
@ApiTags('sessions')
export class SessionsController {
class SessionsController {
constructor(private manager: SessionManager) {}
@Post('/start/')
@@ -45,8 +50,24 @@ export class SessionsController {
}
@Get('/')
async list(@Query() query: ListSessionsQuery): Promise<SessionDTO[]> {
async list(@Query() query: ListSessionsQuery): Promise<SessionInfo[]> {
const all = parseBool(query.all);
return this.manager.getSessions(all);
}
}
@ApiSecurity('api_key')
@Controller('api/sessions/:session')
@ApiTags('sessions')
class SessionController {
constructor(private manager: SessionManager) {}
@Get('me')
@SessionApiParam
@ApiOperation({ summary: 'Get information about the authenticated account' })
getMe(@SessionParam session: WhatsappSession): Promise<MeInfo> {
return session.getSessionMeInfo();
}
}
export { SessionController, SessionsController };
+1 -1
View File
@@ -3,7 +3,7 @@ import { ConfigService } from '@nestjs/config';
import { parseBool } from './helpers';
import { WAHAEngine } from './structures/enums.dto';
import { WebhookConfig } from './structures/webhooks.dto';
import { WebhookConfig } from './structures/webhooks.config.dto';
@Injectable()
export class WhatsappConfigService {
+2 -1
View File
@@ -3,6 +3,7 @@ import { OnApplicationShutdown } from '@nestjs/common';
import { WAHAEngine } from '../../structures/enums.dto';
import {
SessionDTO,
SessionInfo,
SessionLogoutRequest,
SessionStartRequest,
SessionStopRequest,
@@ -35,5 +36,5 @@ export abstract class SessionManager implements OnApplicationShutdown {
abstract getSession(name: string, error?: boolean): WhatsappSession;
abstract getSessions(all: boolean): Promise<SessionDTO[]>;
abstract getSessions(all: boolean): Promise<SessionInfo[]>;
}
+35 -1
View File
@@ -12,6 +12,7 @@ import {
MessageImageRequest,
MessageLinkPreviewRequest,
MessageLocationRequest,
MessagePollRequest,
MessageReactionRequest,
MessageReplyRequest,
MessageTextButtonsRequest,
@@ -30,9 +31,14 @@ import {
ParticipantsRequest,
} from '../../structures/groups.dto';
import { WAHAChatPresences } from '../../structures/presence.dto';
import { ProxyConfig, SessionConfig } from '../../structures/sessions.dto';
import {
MeInfo,
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,
@@ -147,8 +153,32 @@ export abstract class WhatsappSession {
/**
* START - Methods for API
*/
/**
* Auth methods
*/
public getQR(): Promise<Buffer> {
throw new NotImplementedByEngineError();
}
public requestCode(phoneNumber: string, method: string) {
throw new NotImplementedByEngineError();
}
public authorizeCode(code: string) {
throw new NotImplementedByEngineError();
}
abstract getScreenshot(): Promise<Buffer | string>;
public getSessionMeInfo(): Promise<MeInfo | null> {
throw new NotImplementedByEngineError();
}
/**
* Other methods
*/
abstract checkNumberStatus(request: CheckNumberStatusQuery);
abstract sendText(request: MessageTextRequest);
@@ -157,6 +187,10 @@ export abstract class WhatsappSession {
abstract sendTextButtons(request: MessageTextButtonsRequest);
sendPoll(request: MessagePollRequest) {
throw new NotImplementedByEngineError();
}
abstract sendLocation(request: MessageLocationRequest);
abstract sendLinkPreview(request: MessageLinkPreviewRequest);
+1 -1
View File
@@ -1,6 +1,6 @@
import { ConsoleLogger } from '@nestjs/common';
import { WebhookConfig } from '../../structures/webhooks.dto';
import { WebhookConfig } from '../../structures/webhooks.config.dto';
import { WhatsappSession } from './session.abc';
export abstract class WebhookSender {
+7 -1
View File
@@ -8,11 +8,15 @@ 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 {
SessionController,
SessionsController,
} from '../api/sessions.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';
@@ -36,7 +40,9 @@ export const IMPORTS = [
PassportModule,
];
export const CONTROLLERS = [
AuthController,
SessionsController,
SessionController,
ChattingController,
ChatsController,
StatusController,
+6 -2
View File
@@ -10,11 +10,12 @@ import { WAHAEngine, WAHASessionStatus } from '../structures/enums.dto';
import {
ProxyConfig,
SessionDTO,
SessionInfo,
SessionLogoutRequest,
SessionStartRequest,
SessionStopRequest,
} from '../structures/sessions.dto';
import { WebhookConfig } from '../structures/webhooks.dto';
import { WebhookConfig } from '../structures/webhooks.config.dto';
import { SessionManager } from './abc/manager.abc';
import {
SessionParams,
@@ -191,7 +192,7 @@ export class SessionManagerCore extends SessionManager {
return session;
}
async getSessions(all: boolean): Promise<SessionDTO[]> {
async getSessions(all: boolean): Promise<SessionInfo[]> {
if (!this.session) {
if (!all) {
return [];
@@ -201,14 +202,17 @@ export class SessionManagerCore extends SessionManager {
name: this.DEFAULT,
status: WAHASessionStatus.STOPPED,
config: undefined,
me: null,
},
];
}
const me = await this.session.getSessionMeInfo().catch((err) => null);
return [
{
name: this.session.name,
status: this.session.status,
config: this.session.sessionConfig,
me: me,
},
];
}
+352 -24
View File
@@ -1,43 +1,55 @@
import makeWASocket, {
Browsers,
DisconnectReason,
getAggregateVotesInPollMessage,
getKeyAuthor,
isJidGroup,
jidNormalizedUser,
makeInMemoryStore,
PresenceData,
proto,
useMultiFileAuthState,
WAMessageContent,
WAMessageKey,
} from '@adiwajshing/baileys';
import { WABrowserDescription } from '@adiwajshing/baileys';
import { UnprocessableEntityException } from '@nestjs/common';
import { request } from 'express';
import * as fs from 'fs/promises';
import { Agent } from 'https';
import * as lodash from 'lodash';
import { update } from 'lodash';
import { PairingCodeResponse } from 'src/structures/auth.dto';
import { Message } from 'whatsapp-web.js';
import { flipObject } from '../helpers';
import { flipObject, splitAt } from '../helpers';
import {
Button,
ChatRequest,
CheckNumberStatusQuery,
GetMessageQuery,
MessageContactVcardRequest,
MessageDestination,
MessageFileRequest,
MessageImageRequest,
MessageLinkPreviewRequest,
MessageLocationRequest,
MessagePollRequest,
MessageReactionRequest,
MessageReplyRequest,
MessageTextButtonsRequest,
MessageTextRequest,
MessageVoiceRequest,
SendSeenRequest,
WANumberExistResult,
} from '../structures/chatting.dto';
import { ContactQuery, ContactRequest } from '../structures/contacts.dto';
import {
ACK_UNKNOWN,
SECOND,
WAHAEngine,
WAHAEvents,
WAHAPresenceStatus,
WAHASessionStatus,
WAMessageAck,
} from '../structures/enums.dto';
import {
CreateGroupRequest,
@@ -47,8 +59,13 @@ import {
WAHAChatPresences,
WAHAPresenceData,
} from '../structures/presence.dto';
import { WAMessage, WANumberExistResult } from '../structures/responses.dto';
import { WAMessage } from '../structures/responses.dto';
import { BROADCAST_ID, TextStatus } from '../structures/status.dto';
import {
PollVote,
PollVotePayload,
WAMessageAckBody,
} from '../structures/webhooks.dto';
import {
ensureSuffix,
WAHAInternalEvent,
@@ -60,6 +77,7 @@ 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');
@@ -69,7 +87,9 @@ const logger = require('pino')();
export const BaileysEvents = {
CONNECTION_UPDATE: 'connection.update',
CREDS_UPDATE: 'creds.update',
MESSAGES_UPDATE: 'messages.update',
MESSAGES_UPSERT: 'messages.upsert',
MESSAGE_RECEIPT_UPDATE: 'message-receipt.update',
GROUPS_UPSERT: 'groups.upsert',
PRESENCE_UPDATE: 'presence.update',
};
@@ -109,6 +129,7 @@ export class WhatsappSessionNoWebCore extends WhatsappSession {
logger: logger,
mobile: false,
defaultQueryTimeoutMs: undefined,
getMessage: (key) => this.getMessage(key),
};
}
@@ -120,6 +141,7 @@ export class WhatsappSessionNoWebCore extends WhatsappSession {
const socketConfig = this.getSocketConfig(agent, state);
const sock: any = makeWASocket(socketConfig);
sock.ev.on(BaileysEvents.CREDS_UPDATE, saveCreds);
this.debugLogEnginesEvents(sock);
return sock;
}
@@ -131,9 +153,12 @@ export class WhatsappSessionNoWebCore extends WhatsappSession {
}
connectStore() {
this.log.debug(`Connecting store...`);
if (!this.store) {
this.log.debug(`Making a new auth store...`);
this.store = makeInMemoryStore({});
}
this.log.debug(`Binding store to socket...`);
this.store.bind(this.sock.ev);
}
@@ -146,6 +171,22 @@ export class WhatsappSessionNoWebCore extends WhatsappSession {
async buildClient() {
this.sock = await this.makeSocket();
this.connectStore();
this.listenConnectionEvents();
this.events.emit(WAHAInternalEvent.engine_start);
}
protected async getMessage(
key: WAMessageKey,
): Promise<WAMessageContent | undefined> {
if (!this.store) {
return proto.Message.fromObject({});
}
const msg = await this.store.loadMessage(key.remoteJid!, key.id!);
return msg?.message || undefined;
}
protected listenConnectionEvents() {
this.log.debug(`Start listening ${BaileysEvents.CONNECTION_UPDATE}...`);
this.sock.ev.on(BaileysEvents.CONNECTION_UPDATE, async (update) => {
const { connection, lastDisconnect, qr } = update;
if (connection === 'connecting') {
@@ -179,7 +220,19 @@ export class WhatsappSessionNoWebCore extends WhatsappSession {
});
}
});
this.events.emit(WAHAInternalEvent.engine_start);
}
protected debugLogEnginesEvents(sock) {
if (!process.env.DEBUG) {
return;
}
sock.ev.process((events) => {
this.log.debug('Engine events');
this.log.debug('=============');
console.log(JSON.stringify(events));
this.log.debug('=============');
});
}
stop() {
@@ -189,9 +242,58 @@ export class WhatsappSessionNoWebCore extends WhatsappSession {
return;
}
async getSessionMeInfo(): Promise<MeInfo | null> {
const me = this.sock.authState?.creds?.me;
if (!me) {
return null;
}
const meId = jidNormalizedUser(me.id);
const meInfo: MeInfo = {
id: toCusFormat(meId),
pushName: me.name,
};
return meInfo;
}
/**
* START - Methods for API
*/
/**
* Auth methods
*/
public async getQR(): Promise<Buffer> {
return Promise.resolve(this.qr.get());
}
public async requestCode(
phoneNumber: string,
method: string,
): Promise<PairingCodeResponse> {
if (method) {
const err = `NOWEB engine doesn't support any 'method', remove it and try again`;
throw new UnprocessableEntityException(err);
}
if (this.status == WAHASessionStatus.STARTING) {
this.log.debug('Waiting for connection update...');
await this.sock.waitForConnectionUpdate((update) => !!update.qr);
}
if (this.status != WAHASessionStatus.SCAN_QR_CODE) {
const err = `Can request code only in SCAN_QR_CODE status. The current status is ${this.status}`;
throw new UnprocessableEntityException(err);
}
this.log.debug('Requesting pairing code...');
const code: string = await this.sock.requestPairingCode(phoneNumber);
// show it as ABCD-ABCD
const parts = splitAt(code, 4);
const codeRepr = parts.join('-');
this.log.debug(`Your code: ${codeRepr}`);
return { code: codeRepr };
}
async getScreenshot(): Promise<Buffer | string> {
if (this.status === WAHASessionStatus.STARTING) {
throw new UnprocessableEntityException(
@@ -208,6 +310,9 @@ export class WhatsappSessionNoWebCore extends WhatsappSession {
}
}
/**
* Other methods
*/
async checkNumberStatus(
request: CheckNumberStatusQuery,
): Promise<WANumberExistResult> {
@@ -246,6 +351,21 @@ export class WhatsappSessionNoWebCore extends WhatsappSession {
return this.sock.sendMessage(request.chatId, buttonMessage);
}
async sendPoll(request: MessagePollRequest) {
const requestPoll = request.poll;
const poll = {
name: requestPoll.name,
values: requestPoll.options,
selectableCount: requestPoll.multipleAnswers
? requestPoll.options.length
: 1,
};
const message = { poll: poll };
const remoteJid = toJID(request.chatId);
const result = await this.sock.sendMessage(remoteJid, message);
return this.toWAMessage(result);
}
sendContactVCard(request: MessageContactVcardRequest) {
throw new NotImplementedByEngineError();
}
@@ -470,21 +590,49 @@ export class WhatsappSessionNoWebCore extends WhatsappSession {
subscribe(event, handler) {
if (event === WAHAEvents.MESSAGE) {
return this.sock.ev.on(BaileysEvents.MESSAGES_UPSERT, ({ messages }) =>
this.handleIncomingMessages(messages, handler, false),
);
this.sock.ev.on(BaileysEvents.MESSAGES_UPSERT, ({ messages }) => {
this.handleIncomingMessages(messages, handler, false);
});
} else if (event === WAHAEvents.MESSAGE_ANY) {
return this.sock.ev.on(BaileysEvents.MESSAGES_UPSERT, ({ messages }) =>
this.sock.ev.on(BaileysEvents.MESSAGES_UPSERT, ({ messages }) =>
this.handleIncomingMessages(messages, handler, true),
);
} else if (event === WAHAEvents.MESSAGE_ACK) {
// Direct message ack
this.sock.ev.on(BaileysEvents.MESSAGES_UPDATE, (events) => {
events
.filter(isMine)
.filter(isAckUpdateMessageEvent)
.map(this.convertMessageUpdateToMessageAck)
.forEach(handler);
});
// Group message ack
this.sock.ev.on(BaileysEvents.MESSAGE_RECEIPT_UPDATE, (events) => {
events
.filter(isMine)
.map(this.convertMessageReceiptUpdateToMessageAck)
.forEach(handler);
});
} else if (event === WAHAEvents.STATE_CHANGE) {
return this.sock.ev.on(BaileysEvents.CONNECTION_UPDATE, handler);
this.sock.ev.on(BaileysEvents.CONNECTION_UPDATE, handler);
} else if (event === WAHAEvents.GROUP_JOIN) {
return this.sock.ev.on(BaileysEvents.GROUPS_UPSERT, handler);
this.sock.ev.on(BaileysEvents.GROUPS_UPSERT, handler);
} else if (event === WAHAEvents.PRESENCE_UPDATE) {
return this.sock.ev.on(BaileysEvents.PRESENCE_UPDATE, (data) =>
this.sock.ev.on(BaileysEvents.PRESENCE_UPDATE, (data) =>
handler(this.toWahaPresences(data.id, data.presences)),
);
} else if (event === WAHAEvents.POLL_VOTE) {
this.sock.ev.on(BaileysEvents.MESSAGES_UPDATE, (events) => {
events.forEach((event) =>
this.handleMessagesUpdatePollVote(event, handler),
);
});
} else if (event === WAHAEvents.POLL_VOTE_FAILED) {
this.sock.ev.on(BaileysEvents.MESSAGES_UPSERT, ({ messages }) => {
messages.forEach((message) =>
this.handleMessageUpsertPollVoteFailed(message, handler),
);
});
} else {
throw new NotImplementedByEngineError(
`Engine does not support webhook event: ${event}`,
@@ -497,6 +645,8 @@ export class WhatsappSessionNoWebCore extends WhatsappSession {
// if there is no text or media message
if (!message) return;
if (!message.message) return;
// Ignore poll votes, we have dedicated handler for that
if (message.message.pollUpdateMessage) return;
// Do not include my messages
if (!includeFromMe && message.key.fromMe) {
continue;
@@ -506,18 +656,17 @@ export class WhatsappSessionNoWebCore extends WhatsappSession {
}
private processIncomingMessage(message) {
return this.downloadMedia(message).then(this.toWAMessage);
return this.downloadMedia(message)
.then(this.toWAMessage)
.catch((error) => {
this.log.error('Failed to process incoming message');
this.log.error(error);
console.trace(error);
});
}
protected toWAMessage(message): Promise<WAMessage> {
const isGroupMessage = Boolean(message.key.participant);
let participant: string;
let to: string;
if (isGroupMessage) {
participant = message.key.participant;
to = message.key.remoteJid;
}
const from = message.key.remoteJid;
const fromToParticipant = getFromToParticipant(message);
const id = buildMessageId(message.key);
let body = message.message.conversation;
if (!body) {
@@ -528,23 +677,138 @@ export class WhatsappSessionNoWebCore extends WhatsappSession {
return Promise.resolve({
id: id,
timestamp: message.messageTimestamp,
from: toCusFormat(from),
from: toCusFormat(fromToParticipant.from),
fromMe: message.key.fromMe,
body: body,
to: toCusFormat(to),
participant: toCusFormat(participant),
to: toCusFormat(fromToParticipant.to),
participant: toCusFormat(fromToParticipant.participant),
// @ts-ignore
hasMedia: Boolean(message.mediaUrl),
// @ts-ignore
mediaUrl: message.mediaUrl,
// @ts-ignore
ack: message.ack,
// @ts-ignore
ackName: WAMessageAck[message.ack] || ACK_UNKNOWN,
location: message.location,
vCards: message.vCards,
_data: message,
});
}
protected convertMessageUpdateToMessageAck(event): WAMessageAckBody {
const message = event;
const fromToParticipant = getFromToParticipant(message);
const id = buildMessageId(message.key);
const ack = message.update.status - 1;
const body: WAMessageAckBody = {
id: id,
from: toCusFormat(fromToParticipant.from),
to: toCusFormat(fromToParticipant.to),
participant: toCusFormat(fromToParticipant.participant),
fromMe: message.key.fromMe,
ack: ack,
ackName: WAMessageAck[ack] || ACK_UNKNOWN,
};
return body;
}
protected convertMessageReceiptUpdateToMessageAck(event): WAMessageAckBody {
const fromToParticipant = getFromToParticipant(event);
const id = buildMessageId(event.key);
const receipt = event.receipt;
let ack;
if (receipt.receiptTimestamp) {
ack = WAMessageAck.SERVER;
} else if (receipt.playedTimestamp) {
ack = WAMessageAck.PLAYED;
} else if (receipt.readTimestamp) {
ack = WAMessageAck.READ;
}
const body: WAMessageAckBody = {
id: id,
from: toCusFormat(fromToParticipant.from),
to: toCusFormat(fromToParticipant.to),
participant: toCusFormat(fromToParticipant.participant),
fromMe: event.key.fromMe,
ack: ack,
ackName: WAMessageAck[ack] || ACK_UNKNOWN,
};
return body;
}
protected async handleMessagesUpdatePollVote(event, handler) {
const { key, update } = event;
const pollUpdates = update?.pollUpdates;
if (!pollUpdates) {
return;
}
const pollCreationMessageKey = key;
const pollCreationMessage = await this.getMessage(key);
// Handle updates one by one, so we can get Vote Message for the specific vote
for (const pollUpdate of pollUpdates) {
const votes = getAggregateVotesInPollMessage({
message: pollCreationMessage,
pollUpdates: [pollUpdate],
});
// Get selected options for the author
const selectedOptions = [];
for (const voteAggregation of votes) {
for (const voter of voteAggregation.voters) {
if (voter === getKeyAuthor(pollUpdate.pollUpdateMessageKey)) {
selectedOptions.push(voteAggregation.name);
}
}
}
// Build payload and call the handler
const voteDestination = getDestination(pollUpdate.pollUpdateMessageKey);
const pollVote: PollVote = {
...voteDestination,
selectedOptions: selectedOptions,
timestamp: pollUpdate.senderTimestampMs,
};
const payload: PollVotePayload = {
vote: pollVote,
poll: getDestination(pollCreationMessageKey),
};
handler(payload);
}
}
protected async handleMessageUpsertPollVoteFailed(message, handler) {
const pollUpdateMessage = message.message?.pollUpdateMessage;
if (!pollUpdateMessage) {
return;
}
const pollCreationMessageKey = pollUpdateMessage.pollCreationMessageKey;
const pollCreationMessage = await this.getMessage(pollCreationMessageKey);
if (pollCreationMessage) {
// We found message, so later the engine will issue a message.update message
return;
}
// We didn't find the creation message, so send failed one
const pollUpdateMessageKey = message.key;
const voteDestination = getDestination(pollUpdateMessageKey);
const pollVote: PollVote = {
...voteDestination,
selectedOptions: [],
// change to below line when the PR merged, so we have the same timestamps
// https://github.com/WhiskeySockets/Baileys/pull/348
// Or without toNumber() - it depends on the PR above
// timestamp: pollUpdateMessage.senderTimestampMs.toNumber()
timestamp: message.messageTimestamp,
};
const payload: PollVotePayload = {
vote: pollVote,
poll: getDestination(pollCreationMessageKey),
};
handler(payload);
}
private toWahaPresences(
remoteJid,
storedPresences: PresenceData[],
@@ -590,6 +854,9 @@ function toCusFormat(remoteJid) {
if (!remoteJid) {
return;
}
if (remoteJid == 'me') {
return remoteJid;
}
const number = remoteJid.split('@')[0];
return ensureSuffix(number);
}
@@ -638,3 +905,64 @@ function parseMessageId(messageId) {
function getId(object) {
return object.id;
}
function isMine(message) {
return message?.key?.fromMe;
}
function isNotMine(message) {
return !message?.key?.fromMe;
}
function isAckUpdateMessageEvent(event) {
return event?.update.status != null;
}
function getFromToParticipant(message) {
const isGroupMessage = Boolean(message.key.participant);
let participant: string;
let to: string;
if (isGroupMessage) {
participant = message.key.participant;
to = message.key.remoteJid;
}
const from = message.key.remoteJid;
return {
from: from,
to: to,
participant: participant,
};
}
function getTo(key, meId = undefined) {
// For group - always to group JID
const isGroupMessage = Boolean(key.participant);
if (isGroupMessage) {
return key.remoteJid;
}
if (key.fromMe) {
return key.remoteJid;
}
return meId || 'me';
}
function getFrom(key, meId) {
// For group - always from participant
const isGroupMessage = Boolean(key.participant);
if (isGroupMessage) {
return key.participant;
}
if (key.fromMe) {
return meId || 'me';
}
return key.remoteJid;
}
function getDestination(key, meId = undefined): MessageDestination {
return {
id: buildMessageId(key),
to: toCusFormat(getTo(key, meId)),
from: toCusFormat(getFrom(key, meId)),
fromMe: key.fromMe,
};
}
+2 -1
View File
@@ -15,13 +15,14 @@ import {
MessageReplyRequest,
MessageTextButtonsRequest,
MessageTextRequest,
WANumberExistResult,
} from '../structures/chatting.dto';
import {
WAHAEngine,
WAHAEvents,
WAHASessionStatus,
} from '../structures/enums.dto';
import { WAMessage, WANumberExistResult } from '../structures/responses.dto';
import { WAMessage } from '../structures/responses.dto';
import { WAHAInternalEvent, WhatsappSession } from './abc/session.abc';
import { NotImplementedByEngineError } from './exceptions';
import { QR } from './QR';
+58 -14
View File
@@ -28,24 +28,33 @@ import {
MessageTextRequest,
MessageVoiceRequest,
SendSeenRequest,
WANumberExistResult,
} from '../structures/chatting.dto';
import { ContactQuery, ContactRequest } from '../structures/contacts.dto';
import {
ACK_UNKNOWN,
WAHAEngine,
WAHAEvents,
WAHAPresenceStatus,
WAHASessionStatus,
WAMessageAck,
} from '../structures/enums.dto';
import {
CreateGroupRequest,
ParticipantsRequest,
} from '../structures/groups.dto';
import { WAMessage, WANumberExistResult } from '../structures/responses.dto';
import { WAMessage } from '../structures/responses.dto';
import { WAHAInternalEvent, WhatsappSession } from './abc/session.abc';
import {
AvailableInPlusVersion,
NotImplementedByEngineError,
} from './exceptions';
import { QR } from './QR';
import { MeInfo } from '../structures/sessions.dto';
import { jidNormalizedUser } from '@adiwajshing/baileys';
// eslint-disable-next-line @typescript-eslint/no-var-requires
const QRCode = require('qrcode');
// eslint-disable-next-line @typescript-eslint/no-var-requires
const qrcode = require('qrcode-terminal');
@@ -54,6 +63,12 @@ export class WhatsappSessionWebJSCore extends WhatsappSession {
engine = WAHAEngine.WEBJS;
whatsapp: Client;
protected qr: QR;
public constructor(config) {
super(config);
this.qr = new QR();
}
protected buildClient() {
const clientOptions: ClientOptions = {
@@ -86,23 +101,12 @@ export class WhatsappSessionWebJSCore extends WhatsappSession {
async start() {
this.whatsapp = this.buildClient();
this.whatsapp.initialize().catch((error) => {
this.status = WAHASessionStatus.FAILED;
this.log.error(error);
return;
});
// Connect events
this.whatsapp.on(Events.QR_RECEIVED, (qr) => {
qrcode.generate(qr, { small: true });
this.status = WAHASessionStatus.SCAN_QR_CODE;
});
this.whatsapp.on(Events.AUTHENTICATED, () => {
this.status = WAHASessionStatus.WORKING;
this.log.log(`Session '${this.name}' has been authenticated!`);
});
this.listenConnectionEvents();
this.events.emit(WAHAInternalEvent.engine_start);
return this;
}
@@ -111,9 +115,49 @@ export class WhatsappSessionWebJSCore extends WhatsappSession {
return this.whatsapp.destroy();
}
async getSessionMeInfo(): Promise<MeInfo | null> {
const clientInfo = this.whatsapp?.info;
if (!clientInfo) {
return null;
}
const wid = clientInfo.wid;
const meInfo: MeInfo = {
id: wid._serialized,
pushName: clientInfo.pushname,
};
return meInfo;
}
protected listenConnectionEvents() {
this.whatsapp.on(Events.QR_RECEIVED, (qr) => {
this.log.debug('QR received');
// Convert to image and save
QRCode.toDataURL(qr).then((url) => {
this.qr.save(url);
});
// Print in terminal
qrcode.generate(qr, { small: true });
this.status = WAHASessionStatus.SCAN_QR_CODE;
});
this.whatsapp.on(Events.AUTHENTICATED, () => {
this.status = WAHASessionStatus.WORKING;
this.qr.save('');
this.log.log(`Session '${this.name}' has been authenticated!`);
});
}
/**
* START - Methods for API
*/
/**
* Auth methods
*/
public async getQR(): Promise<Buffer> {
return Promise.resolve(this.qr.get());
}
async getScreenshot(): Promise<Buffer | string> {
if (this.status === WAHASessionStatus.FAILED) {
throw new UnprocessableEntityException(
@@ -478,8 +522,8 @@ export class WhatsappSessionWebJSCore extends WhatsappSession {
hasMedia: Boolean(message.mediaUrl),
// @ts-ignore
mediaUrl: message.mediaUrl,
// @ts-ignore
ack: message.ack,
ackName: WAMessageAck[message.ack] || ACK_UNKNOWN,
location: message.location,
vCards: message.vCards,
_data: message.rawData,
+1
View File
@@ -15,6 +15,7 @@ export class SwaggerModuleCore {
.setExternalDoc('Documentation', 'https://waha.devlike.pro/')
.setVersion(VERSION.version)
.addTag('sessions', 'Control WhatsApp sessions')
.addTag('auth', 'Authentication')
.addTag('screenshot', 'Get screenshot of WhatsApp and show QR code')
.addTag('chatting', 'Chatting methods')
.addTag(
+2 -2
View File
@@ -1,12 +1,12 @@
import { ConsoleLogger } from '@nestjs/common';
import { WAHAEvents } from '../structures/enums.dto';
import { WAWebhook } from '../structures/responses.dto';
import { WebhookConfig } from '../structures/webhooks.dto';
import { WebhookConfig } from '../structures/webhooks.config.dto';
import { WhatsappSession } from './abc/session.abc';
import { WebhookConductor, WebhookSender } from './abc/webhooks.abc';
import { NotImplementedByEngineError } from './exceptions';
import request = require('request');
import { WAWebhook } from '../structures/webhooks.dto';
export class WebhookSenderCore extends WebhookSender {
send(json) {
+7 -1
View File
@@ -25,4 +25,10 @@ function flipObject(object) {
);
}
export { flipObject, parseBool };
function splitAt(str: string, index) {
const fst = [...str];
const snd = fst.splice(index);
return [fst.join(''), snd.join('')];
}
export { flipObject, parseBool, splitAt };
+24
View File
@@ -0,0 +1,24 @@
import { ApiProperty } from '@nestjs/swagger';
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.',
example: null,
required: false,
})
method: string;
}
export class OTPRequest {
code: string;
}
export class PairingCodeResponse {
code: string;
}
+36 -6
View File
@@ -8,6 +8,7 @@ import {
VoiceBinaryFile,
VoiceRemoteFile,
} from './files.dto';
import { ChatIdProperty } from './properties.dto';
/**
* Queries
@@ -25,9 +26,7 @@ export class MessageTextQuery extends SessionQuery {
}
export class ChatQuery extends SessionQuery {
@ApiProperty({
example: '11111111111@c.us',
})
@ChatIdProperty()
chatId: string;
}
@@ -48,9 +47,7 @@ export class GetPresenceQuery extends ChatQuery {}
* Requests
*/
export class ChatRequest extends SessionBaseRequest {
@ApiProperty({
example: '11111111111@c.us',
})
@ChatIdProperty()
chatId: string;
}
@@ -163,3 +160,36 @@ export class MessageReactionRequest extends MessageRequest {
})
reaction: string;
}
export class WANumberExistResult {
numberExists: boolean;
}
export class MessagePoll {
@ApiProperty({
example: 'How are you?',
})
name: string;
@ApiProperty({
example: ['Awesome!', 'Good!', 'Not bad!'],
})
options: string[];
multipleAnswers = false;
}
export class MessagePollRequest extends ChatRequest {
poll: MessagePoll;
}
export class MessageDestination {
@ApiProperty({
description: 'Message ID',
example: 'false_11111111111@c.us_AAAAAAAAAAAAAAAAAAAA',
})
id: string;
to: string;
from: string;
fromMe: boolean;
}
+13
View File
@@ -8,6 +8,8 @@ export enum WAHAEvents {
GROUP_JOIN = 'group.join',
GROUP_LEAVE = 'group.leave',
PRESENCE_UPDATE = 'presence.update',
POLL_VOTE = 'poll.vote',
POLL_VOTE_FAILED = 'poll.vote.failed',
}
export enum WAHASessionStatus {
@@ -22,6 +24,7 @@ export enum WAHAEngine {
VENOM = 'VENOM',
WEBJS = 'WEBJS',
NOWEB = 'NOWEB',
NOWEB_MOBILE = 'NOWEB_MOBILE',
}
export enum WAHAPresenceStatus {
@@ -31,3 +34,13 @@ export enum WAHAPresenceStatus {
RECORDING = 'recording',
PAUSED = 'paused',
}
export enum WAMessageAck {
ERROR = -1,
PENDING = 0,
SERVER = 1,
DEVICE = 2,
READ = 3,
PLAYED = 4,
}
export const ACK_UNKNOWN = 'UNKNOWN';
+18
View File
@@ -0,0 +1,18 @@
import { ApiProperty } from '@nestjs/swagger';
function ChatIdProperty(options = undefined) {
options = options || {};
if (!options.example) {
options.example = '11111111111@c.us';
}
return ApiProperty(options);
}
function MessageIdProperty() {
return ApiProperty({
description: 'Message ID',
example: 'false_11111111111@c.us_AAAAAAAAAAAAAAAAAAAA',
});
}
export { ChatIdProperty, MessageIdProperty };
+10 -61
View File
@@ -1,52 +1,7 @@
import { ApiProperty } from '@nestjs/swagger';
import { WAHAEngine, WAHAEvents } from './enums.dto';
export class WANumberExistResult {
numberExists: boolean;
}
export class WAGroupNotification {
@ApiProperty({
description: 'ID that represents the groupNotification',
})
id: any;
@ApiProperty({
description: 'Unix timestamp for when the groupNotification was created',
})
timestamp: number;
@ApiProperty({
description: 'ID for the Chat that this groupNotification was sent for',
})
chatId: string;
@ApiProperty({
description: 'ContactId for the user that produced the GroupNotification',
})
author: string;
@ApiProperty({
description: 'Extra content',
})
body: string;
@ApiProperty({
description:
'Contact IDs for the users that were affected by this GroupNotification',
})
recipientIds: string[];
}
export enum WAMessageAck {
ACK_ERROR = -1,
ACK_PENDING = 0,
ACK_SERVER = 1,
ACK_DEVICE = 2,
ACK_READ = 3,
ACK_PLAYED = 4,
}
import { WAMessageAck } from './enums.dto';
import { ChatIdProperty, MessageIdProperty } from './properties.dto';
export class WALocation {
description?: string | null;
@@ -55,10 +10,7 @@ export class WALocation {
}
export class WAMessage {
@ApiProperty({
description: 'Message ID',
example: 'false_11111111111@c.us_AAAAAAAAAAAAAAAAAAAA',
})
@MessageIdProperty()
id: string;
/** */
@@ -68,7 +20,7 @@ export class WAMessage {
})
timestamp: number;
@ApiProperty({
@ChatIdProperty({
description:
'ID for the Chat that this message was sent to, except if the message was sent by the current user ',
})
@@ -79,7 +31,7 @@ export class WAMessage {
})
fromMe: boolean;
@ApiProperty({
@ChatIdProperty({
description: `
* ID for who this message is for.
* If the message is sent by the current user, it will be the Chat to which the message is being sent.
@@ -115,6 +67,11 @@ export class WAMessage {
})
ack: WAMessageAck;
@ApiProperty({
description: 'ACK status name for the message',
})
ackName: string;
@ApiProperty({
description:
'If the message was sent to a group, this field will contain the user that sent the message.',
@@ -139,11 +96,3 @@ export class WAMessage {
})
_data?: any;
}
export class WAWebhook {
event: WAHAEvents;
session: string;
engine: WAHAEngine;
// eslint-disable-next-line @typescript-eslint/ban-types
payload: WAMessage | WAGroupNotification | object;
}
+12 -1
View File
@@ -1,7 +1,8 @@
import { ApiProperty } from '@nestjs/swagger';
import { WAHASessionStatus } from './enums.dto';
import { WebhookConfig } from './webhooks.dto';
import { WebhookConfig } from './webhooks.config.dto';
import { ChatIdProperty } from './properties.dto';
/**
* Queries
@@ -66,3 +67,13 @@ export class SessionDTO {
status: WAHASessionStatus;
config: SessionConfig;
}
export class MeInfo {
@ChatIdProperty()
id: string;
pushName: string;
}
export class SessionInfo extends SessionDTO {
me: MeInfo | null;
}
+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[];
}
+71 -42
View File
@@ -1,60 +1,89 @@
import { ApiProperty } from '@nestjs/swagger';
export class RetriesConfiguration {
@ApiProperty({
example: 2,
})
delaySeconds: number;
import { WAHAEngine, WAHAEvents, WAMessageAck } from './enums.dto';
import { WAMessage } from './responses.dto';
import { MessageDestination } from './chatting.dto';
import { ChatIdProperty, MessageIdProperty } from './properties.dto';
@ApiProperty({
example: 15,
})
attempts: number;
}
export class CustomHeader {
@ApiProperty({
example: 'X-My-Custom-Header',
})
name: string;
export class WAMessageAckBody {
@MessageIdProperty()
id: string;
@ApiProperty({
example: 'Value',
})
value: string;
@ChatIdProperty()
from: string;
@ChatIdProperty()
to: string;
@ChatIdProperty()
participant: string;
fromMe: boolean;
ack: WAMessageAck;
ackName: string;
}
export class HmacConfiguration {
export class WAGroupPayload {
@ApiProperty({
example: 'your-secret-key',
description: 'ID that represents the groupNotification',
})
key: string;
id: any;
@ApiProperty({
description: 'Unix timestamp for when the groupNotification was created',
})
timestamp: number;
@ChatIdProperty({
description: 'ID for the Chat that this groupNotification was sent for',
})
chatId: string;
@ApiProperty({
description: 'ContactId for the user that produced the GroupNotification',
})
author: string;
@ApiProperty({
description: 'Extra content',
})
body: string;
@ApiProperty({
description:
'Contact IDs for the users that were affected by this GroupNotification',
})
recipientIds: string[];
}
export class WebhookConfig {
export class PollVote extends MessageDestination {
@ApiProperty({
example: 'https://httpbin.org/post',
required: true,
description: 'Option that user has selected',
example: ['Awesome!'],
})
url: string;
selectedOptions: string[];
@ApiProperty({
example: ['message'],
required: true,
description: 'Timestamp, ms',
example: 1692861369,
})
events: string[];
timestamp: number;
}
@ApiProperty({
example: null,
})
hmac?: HmacConfiguration;
export class PollVotePayload {
vote: PollVote;
poll: MessageDestination;
}
@ApiProperty({
example: null,
})
retries?: RetriesConfiguration;
@ApiProperty({
example: null,
})
customHeaders?: CustomHeader[];
export class WAWebhook {
event: WAHAEvents;
session: string;
engine: WAHAEngine;
payload:
| WAMessage
| WAGroupPayload
| WAMessageAckBody
| PollVotePayload
// eslint-disable-next-line @typescript-eslint/ban-types
| object;
}
+1 -1
View File
@@ -26,7 +26,7 @@ export function getWAHAVersion(): WAHAVersion {
}
export const VERSION = {
version: '2023.8.10',
version: '2023.9.3',
tier: getWAHAVersion(),
browser: getBrowserExecutablePath(),
};
+6 -5
View File
@@ -7,7 +7,7 @@ __metadata:
"@adiwajshing/baileys@github:WhiskeySockets/Baileys":
version: 6.4.0
resolution: "@adiwajshing/baileys@https://github.com/WhiskeySockets/Baileys.git#commit=555cc9b3145ea2a94aeb34eec30eb966154b16f8"
resolution: "@adiwajshing/baileys@https://github.com/WhiskeySockets/Baileys.git#commit=49ab16f5925789b85219bec2f35686234dab08a0"
dependencies:
"@adiwajshing/keyed-db": ^0.2.4
"@hapi/boom": ^9.1.3
@@ -37,7 +37,7 @@ __metadata:
optional: true
sharp:
optional: true
checksum: ce07f852b94750d562f4be56aeeaffb35f5df909fc0e38ae1c6b2d3fcefe8078751e49abfc35f1d30ed9dc68baa2d31fc1114624d8f74301ba2ed455110a2d76
checksum: 0faf172db4dcfb9d6df9ff2ef7d2fe5d66aeb7c24e516197a59cbf2bb54648734d5601b992986b9be71e0774bab552be279633d79ea1fc9c8b02426b788973a1
languageName: node
linkType: hard
@@ -8275,7 +8275,7 @@ __metadata:
languageName: node
linkType: hard
"libphonenumber-js@npm:^1.10.20":
"libphonenumber-js@npm:^1.10.20, libphonenumber-js@npm:^1.10.36":
version: 1.10.36
resolution: "libphonenumber-js@npm:1.10.36"
checksum: e5a52e13add03db3cb8c0b7401f4cc5a4167a78965954a6e61b64bf412aacd5424e37ea6ab6ef9f77fc8994e7812bd758585f78b8fb871ca75e2c6b6507982fb
@@ -13123,6 +13123,7 @@ __metadata:
file-type: 16.5.4
https-proxy-agent: ^7.0.0
jest: 26.4.2
libphonenumber-js: ^1.10.36
link-preview-js: ^3.0.4
lodash: ^4.17.21
mime-types: ^2.1.27
@@ -13150,7 +13151,7 @@ __metadata:
"whatsapp-web.js@https://github.com/pedroslopez/whatsapp-web.js":
version: 1.22.1
resolution: "whatsapp-web.js@https://github.com/pedroslopez/whatsapp-web.js.git#commit=b47998878ae62732832986793f8e7ceae9ddb96f"
resolution: "whatsapp-web.js@https://github.com/pedroslopez/whatsapp-web.js.git#commit=abac063b779570729476cf42e29dc694e5345ca6"
dependencies:
"@pedroslopez/moduleraid": ^5.0.2
archiver: ^5.3.1
@@ -13169,7 +13170,7 @@ __metadata:
optional: true
unzipper:
optional: true
checksum: 8084bde72fb558dbf541e51862c39c74f5dd0b29a99b753fd51589d066b305d05bb5a9593a86b2b20c49b38754e365443bc14be824134a4dc170985b6ed36d19
checksum: 2100a4c826d25c5697fe304f6e036556b4c1c28f310262389c17b10344a182b0203eac6ac8823e3624c1e0c1b93187bd9447a1775aa69e01c7329659a6edcebc
languageName: node
linkType: hard