diff --git a/.gitignore b/.gitignore
index 807d8031..1111b272 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,4 +1,4 @@
-.sessions
+.*sessions
tokens
files
venv
diff --git a/Dockerfile b/Dockerfile
index 45c22d62..53787670 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -4,6 +4,15 @@
FROM node:18 as build
ENV PUPPETEER_SKIP_DOWNLOAD=True
+
+# Install https://github.com/WhiskeySockets/Baileys
+WORKDIR /
+RUN git clone https://github.com/WhiskeySockets/Baileys WhiskeySockets-Baileys && \
+ cd WhiskeySockets-Baileys && \
+ git checkout b7878e531144133b48d9e5970787e7b256d21063 && \
+ yarn install && \
+ yarn build:all
+
# npm packages
WORKDIR /src
COPY package.json .
diff --git a/docs/site/config/_default/params.toml b/docs/site/config/_default/params.toml
index 2bc862f6..b9b1a318 100644
--- a/docs/site/config/_default/params.toml
+++ b/docs/site/config/_default/params.toml
@@ -74,7 +74,7 @@ copyRight = "Copyright (c) 2021-2023 Dev Like A Pro"
alert = true
alertDismissable = false
# alertText = "Introducing the Doks child theme, several DX + UX updates, and more! Check out Doks v0.2"
-alertText = "Enjoy Core version or want to get WAHA Plus verison? You can support the project ->"
+alertText = "Enjoy Core version or want to get WAHA Plus verison? Support the project ->"
# Edit Page
# repoHost [Github | Gitea | GitLab | Bitbucket | BitbucketServer ] is used for building the edit link based on git hoster
diff --git a/docs/site/content/en/docs/help/changelog.md b/docs/site/content/en/docs/help/changelog.md
index 04a4c0bb..064c3da6 100644
--- a/docs/site/content/en/docs/help/changelog.md
+++ b/docs/site/content/en/docs/help/changelog.md
@@ -5,18 +5,31 @@ lead: "Changelog"
date: 2020-10-06T08:49:31+00:00
lastmod: 2020-10-06T08:49:31+00:00
draft: false
-images: []
+images: [ ]
menu:
docs:
parent: "help"
weight: 600
toc: true
---
-## 2021.3
+
+## 2021.5 - May 2023
+
+- Added new [NOWEB engine]({{< relref "/docs/how-to/engines" >}}). **NOWEB** engine does not require a browser to work
+ with
+ WhatsApp Web, it does so directly using a WebSocket.
+ - Less CPU and RAM usage!
+ - Send Locations API works!
+ - Send Link Preview API works!
+ - β Read the article before using it [How to avoid blocking ->]({{< relref "/docs/help/how-to-avoid-blocking" >}}).
+
+## 2021.4 - March 2023
+
- Add [Groups API]({{< relref "/docs/how-to/groups" >}})
- Use Chromium by default instead of Chrome
-## 2021.1
+## 2021.1 - January 2023
+
- Added [Contacts API]({{< relref "/docs/how-to/contacts" >}})
- Get all contacts
- Get a contact
@@ -25,33 +38,42 @@ toc: true
- Check number exists (is registered in WhatsApp) - works even if the number is not in the contact list
- Block and unblock contact
-## 2022.12
+## 2022.12 - December 2023
+
- Added `GET /messages/` endpoint to get chat messages [#31](https://github.com/devlikeapro/whatsapp-http-api/issues/31)
### Security 
+
- Added `WHATSAPP_SWAGGER_USERNAME` and `WHATSAPP_SWAGGER_PASSWORD` to hide and protect swagger panel.
## 2022.11
+
**Please test changes in test environment before update production!!**
### Engine 
-1. WAHA has changed its underlying engine from Venom to Whatsapp Web.JS. It might change the response and webhook's payloads.
+
+1. WAHA has changed its underlying engine from Venom to Whatsapp Web.JS. It might change the response and webhook's
+ payloads.
2. Optimize CPU and memory consumption.
-
### Requests 
+
- For all `/api/session/` requests use `name` field instead of `sessionName`.
- For all "chatting" requests use `session` field instead of `sessionName`.
### Sessions 
+
Now you don't have to scan QR code each time you run WAHA, WAHA saves it for you! Available only in Plus version.
### Authentication 
+
Now you can authenticate all requests for WAHA - use `WHATSAPP_API_KEY=secret` environment variable to set "secret key".
-If `WHATSAPP_API_KEY` is set - requests must have `X-Api-Key` header with `secret` value, where `secret` - any random secret key.
+If `WHATSAPP_API_KEY` is set - requests must have `X-Api-Key` header with `secret` value, where `secret` - any random
+secret key.
### Webhooks 
+
#### Configuration
Instead of setting each webhook via environment variables - we use two environments variables:
diff --git a/docs/site/content/en/docs/help/faq.md b/docs/site/content/en/docs/help/faq.md
index a355163d..55219fee 100644
--- a/docs/site/content/en/docs/help/faq.md
+++ b/docs/site/content/en/docs/help/faq.md
@@ -5,7 +5,7 @@ lead: "Answers to frequently asked questions."
date: 2020-10-06T08:49:31+00:00
lastmod: 2020-10-06T08:49:31+00:00
draft: false
-images: []
+images: [ ]
menu:
docs:
parent: "help"
@@ -13,7 +13,28 @@ weight: 630
toc: true
---
+## How to update?
+
+{{< alert icon="π‘" text="Please test all new versions in the development environment before updating production!" />}}
+
+ For **Core** version the command is
+
+```bash
+docker pull devlikeapro/whatsapp-http-api
+```
+
+ For **Plus** version, we use login to get the image before:
+
+```bash
+docker login -u devlikeapro -p {PASSWORD}
+docker pull devlikeapro/whatsapp-http-api-plus
+docker logout
+```
+
+Read more about how to get `PASSWORD` for [Plus Version β]({{< relref "plus-version" >}})
+
## How much CPU and Memory does WhatsApp HTTP API consume?
+
The benchmark has been made on **Intel(R) Core(TM) i7-10510U CPU @ 1.80GHz**.
It may differ from case to case, it depends on usage pattern - how many messages you get, how many send, etc.
@@ -22,6 +43,7 @@ It may differ from case to case, it depends on usage pattern - how many messages
| 1 | 30% | 400MB |
| 10 | 270% | 1.5GB |
-
The nature of WhatsApp HTTP API and its underlying engines - is to run **real** WhatsApp Web version in Chrome and
communicate with it to prevent blocking from WhatsApp. It's the reason why it's so demanding on resources.
+
+If you're looking for less resource demanded engine - [have a look at **NOWEB** engine ->]({{< relref "/docs/how-to/engines" >}})
diff --git a/docs/site/content/en/docs/help/how-to-avoid-blocking.md b/docs/site/content/en/docs/help/how-to-avoid-blocking.md
new file mode 100644
index 00000000..ec0d2ff5
--- /dev/null
+++ b/docs/site/content/en/docs/help/how-to-avoid-blocking.md
@@ -0,0 +1,58 @@
+---
+title: "How to Avoid Blocking"
+description: "How to Avoid Blocking From Whatsapp"
+date: 2020-11-12T13:26:54+01:00
+lastmod: 2020-11-12T13:26:54+01:00
+draft: false
+images: [ ]
+menu:
+ docs:
+ parent: "help"
+weight: 610
+toc: true
+---
+
+How to Avoid Blocking in WhatsApp when developing bots.
+
+It's important to keep in mind that WhatsApp has strict policies in place to prevent spamming and abuse of their
+platform.
+
+If you're developing a bot for WhatsApp, it's crucial to follow these guidelines to avoid getting blocked.
+
+## Guidelines to Follow
+
+### 1. Only Reply to Messages
+
+When developing a bot for WhatsApp, you **should never initiate a conversation**.
+Instead, your bot **should only reply** to messages that it receives.
+This will prevent your bot from being flagged as spam by WhatsApp's users and algorithms.
+
+### 2. Avoid Spamming and Sending Unnecessary Content
+
+Sending too many messages or sending content that the user did not request can also lead to your bot being blocked. Make
+sure to only send relevant and useful information to the user.
+Additionally, do not send too many messages at once, as this can also trigger spam filters.
+
+### 3. Other Considerations
+
+There are other guidelines to follow when developing a bot for WhatsApp, such as avoiding the use of banned words and
+not sharing sensitive or inappropriate content.
+Make sure to read WhatsApp's policies thoroughly to ensure that your bot complies with all of their rules.
+
+## How to Process Messages
+
+When processing messages in your bot, it's important to follow certain steps to avoid being flagged as spam.
+Here's a recommended process to follow:
+
+1. **Send seen** before processing the message. This can be done by sending a `POST /api/sendSeen/` request to
+ the WAHA API.
+
+2. **Start typing** before sending a message and wait for a random interval depending on the size of the message. This can
+ be done by sending a `POST /api/startTyping/` request.
+
+3. **Stop typing** before sending the message. This can be done by sending a `POST /api/stopTyping/` request.
+
+4. **Send the text message** using the `POST /api/sendText` request.
+
+By following these steps, you can ensure that your bot processes messages in a way that's compliant with WhatsApp's
+guidelines and reduces the risk of being blocked.
diff --git a/docs/site/content/en/docs/help/how-to-update.md b/docs/site/content/en/docs/help/how-to-update.md
deleted file mode 100644
index 2fe198da..00000000
--- a/docs/site/content/en/docs/help/how-to-update.md
+++ /dev/null
@@ -1,34 +0,0 @@
----
-title: "How to Update"
-description: "Regularly update the installed WAHA to keep your bot stable, usable, and secure."
-lead: "Regularly update the installed WAHA to keep your bot stable, usable, and secure."
-date: 2020-11-12T13:26:54+01:00
-lastmod: 2020-11-12T13:26:54+01:00
-draft: false
-images: []
-menu:
- docs:
- parent: "help"
-weight: 610
-toc: true
----
-
-{{< alert icon="π‘" text="Please test all new versions in the development environment before updating production!" />}}
-
-## Core
- For Core version the command is
-
-```bash
-docker pull devlikeapro/whatsapp-http-api
-```
-
-## Plus
- For Plus version, we use login to get the image before:
-
-```bash
-docker login -u devlikeapro -p {PASSWORD}
-docker pull devlikeapro/whatsapp-http-api-plus
-docker logout
-```
-
-Read more about how to get `PASSWORD` for [Plus Version β]({{< relref "plus-version" >}})
diff --git a/docs/site/content/en/docs/how-to/config/index.md b/docs/site/content/en/docs/how-to/config/index.md
index 37b6c732..36c2cf85 100644
--- a/docs/site/content/en/docs/how-to/config/index.md
+++ b/docs/site/content/en/docs/how-to/config/index.md
@@ -34,25 +34,3 @@ docker run -it -e "WHATSAPP_HOOK_EVENTS=*" -e WHATSAPP_HOOK_URL=https://httpbin.
- `WHATSAPP_FILES_LIFETIME`- to keep free space files will be removed after this time (default: `180`, in seconds)
- `WHATSAPP_FILES_FOLDER` - folder where will be stored files from chats (images, voice messages) (
default: `/tmp/whatsapp-files`)
-
-## Engines
-
-Under the hood **WAHA** allows you to use two different engines. You can control what you want to run by settings
-`WHATSAPP_DEFAULT_ENGINE` environment variables.
-
-If you have any problems with one engine - try another!
-
-{{< alert icon="π" text="API responses and webhook payloads may differ significantly! We do our best to keep requests body stable between engines." />}}
-
-### WhatsApp WebJS 
-
-**WAHA** uses **WhatsApp WebJS** engine by default
-
-- https://github.com/pedroslopez/whatsapp-web.js
-- `WHATSAPP_DEFAULT_ENGINE=WEBJS`
-
-### Venom 
-
-- https://github.com/orkestral/venom
-- `WHATSAPP_DEFAULT_ENGINE=VENOM`
-
diff --git a/docs/site/content/en/docs/how-to/engines/index.md b/docs/site/content/en/docs/how-to/engines/index.md
new file mode 100644
index 00000000..d9b3673c
--- /dev/null
+++ b/docs/site/content/en/docs/how-to/engines/index.md
@@ -0,0 +1,50 @@
+---
+title: "Engines"
+description: "Engines"
+lead: ""
+date: 2020-10-06T08:48:45+00:00
+lastmod: 2020-10-06T08:48:45+00:00
+draft: false
+images: [ ]
+weight: 800
+---
+
+Under the hood **WAHA** allows you to use two different engines. You can control what you want to run by settings
+`WHATSAPP_DEFAULT_ENGINE` environment variables.
+
+```bash
+docker run -it -e "WHATSAPP_DEFAULT_ENGINE=WEBJS" devlikeapro/whatsapp-http-api
+```
+
+If you have any problems with one engine - try another!
+
+{{< alert icon="π" text="API responses and webhook payloads may differ significantly! We do our best to keep requests body stable between engines." />}}
+
+## WEBJS 
+
+A WhatsApp API client that connects through the WhatsApp Web browser app.
+It uses Puppeteer to run a real instance of Whatsapp Web to avoid getting blocked.
+
+**WAHA** uses **WhatsApp WebJS** engine by default.
+
+- `WHATSAPP_DEFAULT_ENGINE=WEBJS`
+- https://github.com/pedroslopez/whatsapp-web.js
+
+## NOWEB 
+
+**NOWEB** engine does not require a browser to work with WhatsApp Web, it does so directly using a WebSocket.
+Not running Chromium saves you CPU and Memory, so you can run more instances on a single server!
+
+οΈWorking with the engine requires more attention, because it's easy to be blocked with it!
+
+β Read the article before using **NOWEB** engine [How to avoid blocking ->]({{< relref "/docs/help/how-to-avoid-blocking" >}}).
+
+- `WHATSAPP_DEFAULT_ENGINE=NOWEB`
+- https://github.com/WhiskeySockets/Baileys
+
+## VENOM 
+
+It's a high-performance system developed with JavaScript to create a bot for WhatsApp
+
+- `WHATSAPP_DEFAULT_ENGINE=VENOM`
+- https://github.com/orkestral/venom
diff --git a/docs/site/content/en/docs/how-to/plus-version/index.md b/docs/site/content/en/docs/how-to/plus-version/index.md
index 5aa4b746..d091136b 100644
--- a/docs/site/content/en/docs/how-to/plus-version/index.md
+++ b/docs/site/content/en/docs/how-to/plus-version/index.md
@@ -32,8 +32,7 @@ The Plus additional features are:
4. [**Security** β]({{< relref "/docs/how-to/security" >}})
5. [**Saving sessions** β]({{< relref "/docs/how-to/sessions" >}})
6. [**Support multiple sessions** β]({{< relref "/docs/how-to/sessions" >}})
-7. **Instant updates** - no one month delay in getting bugfixes or new features
-8. **Priority in support** - on both bugs and features
+7. **Priority in support** - on both bugs and features
## Differences
diff --git a/examples/python/whatsapp_download_files_bot.py b/examples/python/whatsapp_download_files_bot.py
index 792d4d13..4c045828 100644
--- a/examples/python/whatsapp_download_files_bot.py
+++ b/examples/python/whatsapp_download_files_bot.py
@@ -25,6 +25,17 @@ def send_message(chat_id, text):
)
response.raise_for_status()
+def send_seen(chat_id, message_id, participant):
+ response = requests.post(
+ "http://localhost:3000/api/sendSeen",
+ json={
+ "session": "default",
+ "chatId": chat_id,
+ "messageId": message_id,
+ "participant": participant,
+ },
+ )
+ response.raise_for_status()
@app.route("/")
def whatsapp_echo():
@@ -48,7 +59,13 @@ def whatsapp_webhook():
return "No files in the message"
# Number in format 791111111@c.us
- from_ = payload["from"]
+ chat_id = payload["from"]
+ # Message ID - false_11111111111@c.us_AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+ message_id = payload['id']
+ # For groups - who sent the message
+ participant = payload.get('participant')
+ # IMPORTANT - Always send seen before sending new message
+ send_seen(chat_id=chat_id, message_id=message_id, participant=participant)
# Download the file and download it to the current folder
client_url = payload["mediaUrl"]
@@ -61,7 +78,7 @@ def whatsapp_webhook():
# Send a text back via WhatsApp HTTP API
text = f"We have downloaded file here: {path}"
print(text)
- send_message(chat_id=from_, text=text)
+ send_message(chat_id=chat_id, text=text)
# Send OK back
return "OK"
diff --git a/examples/python/whatsapp_echo_bot.py b/examples/python/whatsapp_echo_bot.py
index 907dca33..dbdaed96 100644
--- a/examples/python/whatsapp_echo_bot.py
+++ b/examples/python/whatsapp_echo_bot.py
@@ -24,6 +24,31 @@ def send_message(chat_id, text):
)
response.raise_for_status()
+def reply(chat_id, message_id, text):
+ response = requests.post(
+ "http://localhost:3000/api/reply",
+ json={
+ "chatId": chat_id,
+ "text": text,
+ "reply_to": message_id,
+ "session": "default",
+ },
+ )
+ response.raise_for_status()
+
+
+def send_seen(chat_id, message_id, participant):
+ response = requests.post(
+ "http://localhost:3000/api/sendSeen",
+ json={
+ "session": "default",
+ "chatId": chat_id,
+ "messageId": message_id,
+ "participant": participant,
+ },
+ )
+ response.raise_for_status()
+
@app.route("/")
def whatsapp_echo():
@@ -45,9 +70,19 @@ def whatsapp_webhook():
payload = data["payload"]
# The text
text = payload["body"]
- # Number in format 1231231231@c.us
- from_ = payload["from"]
+ # Number in format 1231231231@c.us or @g.us for group
+ chat_id = payload["from"]
+ # Message ID - false_11111111111@c.us_AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+ message_id = payload['id']
+ # For groups - who sent the message
+ participant = payload.get('participant')
+ # IMPORTANT - Always send seen before sending new message
+ send_seen(chat_id=chat_id, message_id=message_id, participant=participant)
+
# Send a text back via WhatsApp HTTP API
- send_message(chat_id=from_, text=text)
+ send_message(chat_id=chat_id, text=text)
+ # OR reply on the message
+ # reply(chat_id=chat_id, message_id=message_id, text=text)
+
# Send OK back
return "OK"
diff --git a/package.json b/package.json
index 50d52cf2..930ab009 100644
--- a/package.json
+++ b/package.json
@@ -22,6 +22,8 @@
"test:e2e": "jest --config ./test/jest-e2e.json"
},
"dependencies": {
+ "@adiwajshing/baileys": "file:../WhiskeySockets-Baileys",
+ "@adiwajshing/keyed-db": "^0.2.4",
"@nestjs/common": "^9.0.9",
"@nestjs/config": "^0.5.0",
"@nestjs/core": "^9.0.9",
@@ -29,12 +31,16 @@
"@nestjs/platform-express": "^9.0.9",
"@nestjs/serve-static": "^2.1.3",
"@nestjs/swagger": "^6.0.5",
+ "@types/ws": "^8.5.4",
"class-validator": "^0.12.2",
"del": "^6.0.0",
"express-basic-auth": "^1.2.1",
+ "file-type": "16.5.4",
+ "link-preview-js": "^3.0.4",
"mime-types": "^2.1.27",
"passport": "^0.6.0",
"passport-headerapikey": "^1.2.2",
+ "qrcode": "^1.5.1",
"qrcode-terminal": "^0.12.0",
"reflect-metadata": "^0.1.13",
"request": "^2.88.2",
diff --git a/src/api/chatting.controller.ts b/src/api/chatting.controller.ts
index def5f72e..1901f7c7 100644
--- a/src/api/chatting.controller.ts
+++ b/src/api/chatting.controller.ts
@@ -13,7 +13,7 @@ import {
MessageTextButtonsRequest,
MessageTextQuery,
MessageTextRequest,
- MessageVoiceRequest
+ MessageVoiceRequest, SendSeenRequest
} from "../structures/chatting.dto";
import {WAMessage, WANumberExistResult} from "../structures/responses.dto";
import {SessionManager} from "../core/abc/manager.abc";
@@ -105,7 +105,7 @@ export class ChattingController {
}
@Post('/sendSeen')
- sendSeen(@Body() chat: ChatRequest) {
+ sendSeen(@Body() chat: SendSeenRequest) {
const whatsapp = this.manager.getSession(chat.session)
return whatsapp.sendSeen(chat)
}
diff --git a/src/core/QR.ts b/src/core/QR.ts
new file mode 100644
index 00000000..4bcaf020
--- /dev/null
+++ b/src/core/QR.ts
@@ -0,0 +1,11 @@
+export class QR {
+ private base64: string;
+
+ save(base64) {
+ this.base64 = base64.replace(/^data:image\/png;base64,/, '');
+ }
+
+ get(): Buffer {
+ return Buffer.from(this.base64, "base64")
+ }
+}
diff --git a/src/core/abc/session.abc.ts b/src/core/abc/session.abc.ts
index 90fe02f3..11de8605 100644
--- a/src/core/abc/session.abc.ts
+++ b/src/core/abc/session.abc.ts
@@ -23,6 +23,8 @@ import * as fs from "fs";
const CHROME_PATH = "/usr/bin/google-chrome-stable"
const CHROMIUM_PATH = "/usr/bin/chromium"
+import { EventEmitter } from 'events'
+
export function getBrowserExecutablePath() {
if (fs.existsSync(CHROME_PATH)) {
@@ -30,14 +32,27 @@ export function getBrowserExecutablePath() {
}
return CHROMIUM_PATH
}
+export function ensureSuffix(phone) {
+ const suffix = "@c.us"
+ if (phone.includes("@")) {
+ return phone
+ }
+ return phone + suffix
+}
+
+export enum WAHAInternalEvent {
+ engine_start = "engine.start",
+}
export abstract class WhatsappSession {
public status: WhatsappStatus;
+ public events: EventEmitter
public constructor(public name: string, protected storage: MediaStorage, protected log: ConsoleLogger) {
this.name = name
this.status = WhatsappStatus.STARTING
this.log = log
+ this.events = new EventEmitter()
}
getBrowserExecutablePath() {
@@ -216,11 +231,7 @@ export abstract class WhatsappSession {
* @param phone
*/
protected ensureSuffix(phone) {
- const suffix = "@c.us"
- if (phone.includes("@")) {
- return phone
- }
- return phone + suffix
+ return ensureSuffix(phone)
}
protected deserializeId(messageId: string): MessageId {
diff --git a/src/core/manager.core.ts b/src/core/manager.core.ts
index 7cc44ebd..0259a0cd 100644
--- a/src/core/manager.core.ts
+++ b/src/core/manager.core.ts
@@ -1,6 +1,6 @@
import {ConsoleLogger, Injectable, NotFoundException, UnprocessableEntityException} from "@nestjs/common";
import {SessionManager} from "./abc/manager.abc";
-import {WhatsappSession} from "./abc/session.abc";
+import {WAHAInternalEvent, WhatsappSession} from "./abc/session.abc";
import {WhatsappEngine} from "../structures/enums.dto";
import {SessionDTO, SessionStartRequest, SessionStopRequest} from "../structures/sessions.dto";
import {WhatsappConfigService} from "../config.service";
@@ -9,6 +9,7 @@ import {WhatsappSessionWebJSCore} from "./session.webjs.core";
import {DOCS_URL} from "./exceptions";
import {WebhookConductorCore} from "./webhooks.core";
import {MediaStorageCore} from "./storage.core";
+import {WhatsappSessionNoWebCore} from "./session.noweb.core";
export class OnlyDefaultSessionIsAllowed extends UnprocessableEntityException {
constructor() {
@@ -49,6 +50,8 @@ export class SessionManagerCore extends SessionManager {
return WhatsappSessionWebJSCore
} else if (engine === WhatsappEngine.VENOM) {
return WhatsappSessionVenomCore
+ } else if (engine === WhatsappEngine.NOWEB) {
+ return WhatsappSessionNoWebCore
} else {
throw new NotFoundException(`Unknown whatsapp engine '${engine}'.`)
}
@@ -100,7 +103,8 @@ export class SessionManagerCore extends SessionManager {
const session = new this.EngineClass(name, storage, log)
this.session = session
- session.start().then(() => webhook.configure(session))
+ session.events.on(WAHAInternalEvent.engine_start, () => webhook.configure(session))
+ session.start()
return {name: session.name, status: session.status}
}
diff --git a/src/core/session.noweb.core.ts b/src/core/session.noweb.core.ts
new file mode 100644
index 00000000..7a8d2f4f
--- /dev/null
+++ b/src/core/session.noweb.core.ts
@@ -0,0 +1,468 @@
+import {SECOND, WAEvents, WhatsappStatus} from "../structures/enums.dto";
+import makeWASocket, {DisconnectReason, makeInMemoryStore, isJidGroup, useMultiFileAuthState} from '@adiwajshing/baileys'
+
+import {ensureSuffix, WAHAInternalEvent, WhatsappSession} from "./abc/session.abc";
+import {WAMessage, WANumberExistResult} from "../structures/responses.dto";
+import {
+ Button,
+ ChatRequest,
+ CheckNumberStatusQuery,
+ GetMessageQuery,
+ MessageContactVcardRequest,
+ MessageFileRequest,
+ MessageImageRequest,
+ MessageLinkPreviewRequest,
+ MessageLocationRequest,
+ MessageReactionRequest,
+ MessageReplyRequest,
+ MessageTextButtonsRequest,
+ MessageTextRequest,
+ MessageVoiceRequest,
+ SendSeenRequest
+} from "../structures/chatting.dto";
+import {AvailableInPlusVersion, NotImplementedByEngineError} from "./exceptions";
+import {ContactQuery, ContactRequest} from "../structures/contacts.dto";
+import {CreateGroupRequest, ParticipantsRequest} from "../structures/groups.dto";
+import {QR} from "./QR";
+import {MediaStorage} from "./abc/storage.abc";
+import {ConsoleLogger, UnprocessableEntityException} from "@nestjs/common";
+import {Message} from "whatsapp-web.js";
+import * as fs from "fs";
+import * as path from "path";
+import * as os from "os";
+
+// eslint-disable-next-line @typescript-eslint/no-var-requires
+const QRCode = require('qrcode')
+// eslint-disable-next-line @typescript-eslint/no-var-requires
+const logger = require('pino')()
+
+
+export const BaileysEvents = {
+ CONNECTION_UPDATE: "connection.update",
+ CREDS_UPDATE: "creds.update",
+ MESSAGES_UPSERT: "messages.upsert",
+ GROUPS_UPSERT: 'groups.upsert',
+}
+
+export class WhatsappSessionNoWebCore extends WhatsappSession {
+ sock: any
+ store: any
+ private qr: QR
+ authFolder: string
+
+ public constructor(public name: string, protected storage: MediaStorage, protected log: ConsoleLogger) {
+ super(name, storage, log);
+ this.qr = new QR()
+ this.authFolder = this.getAuthFolder()
+ }
+
+ start() {
+ return this.buildClient()
+ }
+
+ protected getAuthFolder() {
+ return fs.mkdtempSync(path.join(os.tmpdir(), 'noweb-'))
+ }
+
+ async makeSocket() {
+ const {state, saveCreds} = await useMultiFileAuthState(this.authFolder)
+ const sock: any = makeWASocket({
+ auth: state,
+ printQRInTerminal: true,
+ browser: ['Linux', 'Chrome', '111.0.5563.64'],
+ logger: logger,
+ })
+ sock.ev.on(BaileysEvents.CREDS_UPDATE, saveCreds)
+ return sock
+ }
+ connectStore(){
+ if (!this.store){
+ this.store = makeInMemoryStore({ })
+ }
+ this.store.bind(this.sock.ev)
+ }
+
+ async buildClient() {
+ this.sock = await this.makeSocket()
+ this.connectStore()
+ this.sock.ev.on(BaileysEvents.CONNECTION_UPDATE, async (update) => {
+ const {connection, lastDisconnect, qr} = update
+ if (connection === 'connecting') {
+ this.qr.save("")
+ this.status = WhatsappStatus.WORKING
+ return
+ } else if (connection === 'close') {
+ const shouldReconnect = (lastDisconnect.error)?.output?.statusCode !== DisconnectReason.loggedOut
+ this.log.error('connection closed due to ', lastDisconnect.error, ', reconnecting ', shouldReconnect)
+ this.qr.save("")
+ this.status = WhatsappStatus.FAILED
+ // reconnect if not logged out
+ if (shouldReconnect) {
+ setTimeout(() => this.buildClient(), 2 * SECOND)
+ }
+ }
+
+ // Save QR
+ if (qr) {
+ this.status = WhatsappStatus.SCAN_QR_CODE
+ QRCode.toDataURL(qr).then((url) => {
+ this.qr.save(url)
+ })
+ }
+ })
+ this.events.emit(WAHAInternalEvent.engine_start)
+ }
+
+ stop() {
+ this.sock.ws.removeAllListeners()
+ this.log.log('socket connection terminated')
+ return
+ }
+
+ /**
+ * START - Methods for API
+ */
+ async getScreenshot(): Promise {
+ if (this.status === WhatsappStatus.STARTING) {
+ throw new UnprocessableEntityException(`The session is starting, please try again after few seconds`);
+ } else if (this.status === WhatsappStatus.SCAN_QR_CODE) {
+ return Promise.resolve(this.qr.get())
+ } else if (this.status === WhatsappStatus.WORKING) {
+ throw new UnprocessableEntityException(`Can not get screenshot for non chrome based engine.`);
+ } else {
+ throw new UnprocessableEntityException(`Unknown status - ${this.status}`);
+ }
+ }
+
+ async checkNumberStatus(request: CheckNumberStatusQuery): Promise {
+ const phone = request.phone.split("@")[0]
+ const [result] = await this.sock.onWhatsApp(phone)
+ if (!result) {
+ return {numberExists: false}
+ }
+ return {numberExists: result.exists}
+ }
+
+ sendText(request: MessageTextRequest) {
+ const chatId = this.ensureSuffix(request.chatId)
+ return this.sock.sendMessage(chatId, {text: request.text})
+ }
+
+ sendTextButtons(request: MessageTextButtonsRequest) {
+ const buttons = request.buttons.map((button: Button) => {
+ return {
+ buttonId: button.id, buttonText: {displayText: button.text}, type: 1
+ }
+ })
+
+ const buttonMessage = {
+ text: request.title,
+ buttons: buttons,
+ headerType: 1
+ }
+
+ return this.sock.sendMessage(request.chatId, buttonMessage)
+ }
+
+ sendContactVCard(request: MessageContactVcardRequest) {
+ throw new NotImplementedByEngineError()
+ }
+
+ async reply(request: MessageReplyRequest) {
+ const {id} = parseMessageId(request.reply_to)
+ const message = await this.store.loadMessage(toJID(request.chatId), id)
+ return await this.sock.sendMessage(request.chatId, {text: request.text}, {quoted: message})
+ }
+
+ sendImage(request: MessageImageRequest) {
+ throw new AvailableInPlusVersion()
+ }
+
+ sendFile(request: MessageFileRequest) {
+ throw new AvailableInPlusVersion()
+ }
+
+ sendVoice(request: MessageVoiceRequest) {
+ throw new AvailableInPlusVersion()
+ }
+
+ sendLocation(request: MessageLocationRequest) {
+ return this.sock.sendMessage(
+ request.chatId,
+ {location: {degreesLatitude: request.latitude, degreesLongitude: request.longitude}}
+ )
+ }
+
+ sendLinkPreview(request: MessageLinkPreviewRequest) {
+ const text = `${request.title}\n${request.url}`
+ const chatId = this.ensureSuffix(request.chatId)
+ return this.sock.sendMessage(chatId, {text: text})
+ }
+
+
+ async sendSeen(request: SendSeenRequest) {
+ const key = parseMessageId(request.messageId)
+ const data = {
+ remoteJid: key.remoteJid,
+ id: key.id,
+ participant: request.participant
+ }
+ return this.sock.readMessages([data])
+ }
+
+ async startTyping(request: ChatRequest) {
+ return this.sock.sendPresenceUpdate('composing', request.chatId)
+ }
+
+ async stopTyping(request: ChatRequest) {
+ return this.sock.sendPresenceUpdate("paused", request.chatId)
+ }
+
+ async getMessages(query: GetMessageQuery) {
+ throw new NotImplementedByEngineError()
+ }
+
+ async setReaction(request: MessageReactionRequest) {
+ const key = parseMessageId(request.messageId)
+ const reactionMessage = {
+ react: {
+ text: request.reaction,
+ key: key,
+ }
+ }
+ return this.sock.sendMessage(key.remoteJid, reactionMessage)
+ }
+
+
+ /**
+ * Contacts methods
+ */
+ getContact(query: ContactQuery) {
+ throw new NotImplementedByEngineError()
+ }
+
+ getContacts() {
+ throw new NotImplementedByEngineError()
+ }
+
+ public async getContactAbout(query: ContactQuery) {
+ throw new NotImplementedByEngineError()
+ }
+
+ public async getContactProfilePicture(query: ContactQuery) {
+ throw new NotImplementedByEngineError()
+ }
+
+ public async blockContact(request: ContactRequest) {
+ throw new NotImplementedByEngineError()
+ }
+
+ public async unblockContact(request: ContactRequest) {
+ throw new NotImplementedByEngineError()
+ }
+
+ /**
+ * Group methods
+ */
+ public createGroup(request: CreateGroupRequest) {
+ const participants = request.participants.map(getId)
+ return this.sock.groupCreate(request.name, participants)
+ }
+
+ public async getGroups() {
+ return await this.sock.groupFetchAllParticipating()
+ }
+
+ public async getGroup(id) {
+ const groups = await this.sock.groupFetchAllParticipating()
+ return groups[id]
+ }
+
+ public async deleteGroup(id) {
+ throw new NotImplementedByEngineError()
+ }
+
+ public async leaveGroup(id) {
+ return this.sock.groupLeave(id)
+ }
+
+ public async setDescription(id, description) {
+ return this.sock.groupUpdateDescription(id, description)
+ }
+
+ public async setSubject(id, subject) {
+ return this.sock.groupUpdateSubject(id, subject)
+ }
+
+ public async getInviteCode(id): Promise {
+ return this.sock.groupInviteCode(id)
+ }
+
+ public async revokeInviteCode(id): Promise {
+ await this.sock.groupRevokeInvite(id)
+ return this.sock.groupInviteCode(id)
+ }
+
+ public async getParticipants(id) {
+ const groups = await this.sock.groupFetchAllParticipating()
+ return groups[id].participants
+ }
+
+ public async addParticipants(id, request: ParticipantsRequest) {
+ const participants = request.participants.map(getId)
+ return this.sock.groupParticipantsUpdate(id, participants, "add")
+ }
+
+ public async removeParticipants(id, request: ParticipantsRequest) {
+ const participants = request.participants.map(getId)
+ return this.sock.groupParticipantsUpdate(id, participants, "remove")
+ }
+
+ public async promoteParticipantsToAdmin(id, request: ParticipantsRequest) {
+ const participants = request.participants.map(getId)
+ return this.sock.groupParticipantsUpdate(id, participants, "promote")
+ }
+
+ public async demoteParticipantsToUser(id, request: ParticipantsRequest) {
+ const participants = request.participants.map(getId)
+ return this.sock.groupParticipantsUpdate(id, participants, "demote")
+ }
+
+ /**
+ * END - Methods for API
+ */
+
+ subscribe(event, handler) {
+ if (event === WAEvents.MESSAGE) {
+ return this.sock.ev.on(BaileysEvents.MESSAGES_UPSERT,
+ ({messages}) => this.handleIncomingMessages(messages, handler, false)
+ )
+ } else if (event === WAEvents.MESSAGE_ANY) {
+ return this.sock.ev.on(BaileysEvents.MESSAGES_UPSERT,
+ ({messages}) => this.handleIncomingMessages(messages, handler, true)
+ )
+ } else if (event === WAEvents.STATE_CHANGE) {
+ return this.sock.ev.on(BaileysEvents.CONNECTION_UPDATE, handler)
+ } else if (event === WAEvents.GROUP_JOIN) {
+ return this.sock.ev.on(BaileysEvents.GROUPS_UPSERT, handler)
+ } else {
+ throw new NotImplementedByEngineError(`Engine does not support webhook event: ${event}`)
+ }
+ }
+
+ private handleIncomingMessages(messages, handler, includeFromMe) {
+ for (const message of messages) {
+ // if there is no text or media message
+ if (!message) return;
+ if (!message.message) return
+ // Do not include my messages
+ if (!includeFromMe && message.key.fromMe) {
+ continue
+ }
+ this.processIncomingMessage(message).then(handler)
+ }
+ }
+
+ private processIncomingMessage(message) {
+ return this.downloadMedia(message).then(this.toWAMessage)
+ }
+
+ protected toWAMessage(message): Promise {
+ 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 id = buildMessageId(message.key)
+ return Promise.resolve({
+ id: id,
+ timestamp: message.messageTimestamp,
+ from: toCusFormat(from),
+ fromMe: message.key.fromMe,
+ body: message.message.conversation,
+ to: toCusFormat(to),
+ participant: toCusFormat(participant),
+ // @ts-ignore
+ hasMedia: Boolean(message.mediaUrl),
+ // @ts-ignore
+ mediaUrl: message.mediaUrl,
+ // @ts-ignore
+ ack: message.ack,
+ location: message.location,
+ vCards: message.vCards,
+ _data: message,
+ })
+ }
+
+ protected async downloadMedia(message: Message) {
+ if (!message.hasMedia) {
+ return message
+ }
+
+ // @ts-ignore
+ message.mediaUrl = await this.storage.save(message.key.id, "", undefined)
+ return message
+ }
+
+}
+
+/**
+ * Convert from 11111111111@s.whatsapp.net to 11111111111@c.us
+ */
+function toCusFormat(remoteJid) {
+ if (isJidGroup(remoteJid)) {
+ return remoteJid
+ }
+ if (!remoteJid) {
+ return
+ }
+ const number = remoteJid.split("@")[0]
+ return ensureSuffix(number)
+}
+
+/**
+ * Convert from 11111111111@c.us to 11111111111@s.whatsapp.net
+ * @param chatId
+ */
+function toJID(chatId) {
+ if (isJidGroup(chatId)) {
+ return chatId
+ }
+ const number = chatId.split("@")[0]
+ return number + "@s.whatsapp.net"
+}
+
+/**
+ * Build WAHA message id from engine one
+ * {id: "AAA", remoteJid: "11111111111@s.whatsapp.net", "fromMe": false}
+ * false_11111111111@c.us_AA
+ */
+function buildMessageId({id, remoteJid, fromMe}) {
+ const chatId = toCusFormat(remoteJid)
+ return `${fromMe || false}_${chatId}_${id}`
+}
+
+/**
+ * Parse message id from WAHA to engine
+ * false_11111111111@c.us_AAA
+ * {id: "AAA", remoteJid: "11111111111@s.whatsapp.net", "fromMe": false}
+ */
+function parseMessageId(messageId) {
+ const parts = messageId.split("_")
+ if (parts.length != 3) {
+ throw new Error("Message id be in format false_11111111111@c.us_AAAAAAAAAAAAAAAAAAAA")
+ }
+ const fromMe = parts[0] == "true"
+ const chatId = parts[1]
+ const remoteJid = toJID(chatId)
+ const id = parts[2]
+ return {fromMe: fromMe, id: id, remoteJid: remoteJid}
+}
+
+
+function getId(object) {
+ return object.id
+}
diff --git a/src/core/session.venom.core.ts b/src/core/session.venom.core.ts
index 451b1e32..d784d853 100644
--- a/src/core/session.venom.core.ts
+++ b/src/core/session.venom.core.ts
@@ -1,8 +1,9 @@
-import {WhatsappSession} from "./abc/session.abc";
+import {WAHAInternalEvent, WhatsappSession} from "./abc/session.abc";
import {
Button,
ChatRequest,
- CheckNumberStatusQuery, GetMessageQuery,
+ CheckNumberStatusQuery,
+ GetMessageQuery,
MessageContactVcardRequest,
MessageFileRequest,
MessageImageRequest,
@@ -20,20 +21,7 @@ import {NotImplementedByEngineError} from "./exceptions";
import {MediaStorage} from "./abc/storage.abc";
import {UnprocessableEntityException} from "@nestjs/common/exceptions/unprocessable-entity.exception";
import {ConsoleLogger} from "@nestjs/common";
-import {ContactQuery} from "../structures/contacts.dto";
-
-class QR {
- private base64: string;
-
- save(base64) {
- this.base64 = base64.replace(/^data:image\/png;base64,/, '');
- }
-
- get(): Buffer {
- return Buffer.from(this.base64, "base64")
- }
-
-}
+import {QR} from "./QR";
export class WhatsappSessionVenomCore extends WhatsappSession {
whatsapp: Whatsapp;
@@ -84,6 +72,7 @@ export class WhatsappSessionVenomCore extends WhatsappSession {
}
this.status = WhatsappStatus.WORKING
+ this.events.emit(WAHAInternalEvent.engine_start)
return this
}
@@ -126,10 +115,10 @@ export class WhatsappSessionVenomCore extends WhatsappSession {
async checkNumberStatus(request: CheckNumberStatusQuery): Promise {
try {
- return await this.whatsapp.checkNumberStatus(this.ensureSuffix(request.phone))
+ return await this.whatsapp.checkNumberStatus(this.ensureSuffix(request.phone))
} catch (error) {
// Catch number doesn't exist error and return it as is
- if (error.status === 404 && !error.numberExists){
+ if (error.status === 404 && !error.numberExists) {
return error
}
throw error
@@ -177,11 +166,11 @@ export class WhatsappSessionVenomCore extends WhatsappSession {
return {
buttonId: button.id,
buttonText: {
- displayText: button.body
+ displayText: button.text
}
}
})
- return this.whatsapp.sendButtons(this.ensureSuffix(request.chatId), request.title, buttons, request.text)
+ return this.whatsapp.sendButtons(this.ensureSuffix(request.chatId), request.title, buttons, request.footer)
}
startTyping(chat: ChatRequest) {
@@ -192,12 +181,12 @@ export class WhatsappSessionVenomCore extends WhatsappSession {
return this.whatsapp.stopTyping(chat.chatId)
}
- async getMessages(query: GetMessageQuery){
+ async getMessages(query: GetMessageQuery) {
const messages = await this.whatsapp.getAllMessagesInChat(query.chatId, true, false)
// Go over messages, download media, and convert to right format.
const result = []
for (const [count, message] of messages.entries()) {
- if (count > query.limit){
+ if (count > query.limit) {
// Have enough in the list, stop processing
break
}
diff --git a/src/core/session.webjs.core.ts b/src/core/session.webjs.core.ts
index 2b9818b6..236bec8c 100644
--- a/src/core/session.webjs.core.ts
+++ b/src/core/session.webjs.core.ts
@@ -2,9 +2,10 @@ import {UnprocessableEntityException} from "@nestjs/common/exceptions/unprocessa
import {Buttons, Chat, Client, Contact, Events, GroupChat, Location, Message} from "whatsapp-web.js";
import {Message as MessageInstance} from "whatsapp-web.js/src/structures"
import {WAEvents, WhatsappStatus} from "../structures/enums.dto";
-import {WhatsappSession} from "./abc/session.abc";
+import {WAHAInternalEvent, WhatsappSession} from "./abc/session.abc";
import {WAMessage, WANumberExistResult} from "../structures/responses.dto";
import {
+ Button,
ChatRequest,
CheckNumberStatusQuery,
GetMessageQuery,
@@ -17,7 +18,7 @@ import {
MessageReplyRequest,
MessageTextButtonsRequest,
MessageTextRequest,
- MessageVoiceRequest
+ MessageVoiceRequest, SendSeenRequest
} from "../structures/chatting.dto";
import {AvailableInPlusVersion, NotImplementedByEngineError} from "./exceptions";
import {ContactQuery, ContactRequest} from "../structures/contacts.dto";
@@ -59,6 +60,7 @@ export class WhatsappSessionWebJSCore extends WhatsappSession {
this.status = WhatsappStatus.WORKING
this.log.log(`Session '${this.name}' has been authenticated!`)
});
+ this.events.emit(WAHAInternalEvent.engine_start)
return this
}
@@ -86,7 +88,13 @@ export class WhatsappSessionWebJSCore extends WhatsappSession {
}
sendTextButtons(request: MessageTextButtonsRequest) {
- const message = new Buttons("", request.buttons, request.title, request.text)
+ const buttons = request.buttons.map((button: Button) => {
+ return {
+ id: button.id,
+ body: button.text,
+ }
+ })
+ const message = new Buttons("", buttons, request.title, request.footer)
return this.whatsapp.sendMessage(this.ensureSuffix(request.chatId), message)
}
@@ -127,7 +135,7 @@ export class WhatsappSessionWebJSCore extends WhatsappSession {
}
- async sendSeen(request: ChatRequest) {
+ async sendSeen(request: SendSeenRequest) {
const chat: Chat = await this.whatsapp.getChatById(request.chatId)
await chat.sendSeen()
}
@@ -157,8 +165,6 @@ export class WhatsappSessionWebJSCore extends WhatsappSession {
async setReaction(request: MessageReactionRequest) {
const messageId = this.deserializeId(request.messageId)
- console.log(messageId)
-
// Recreate instance to react on it
const message = new MessageInstance(this.whatsapp)
message.id = messageId
diff --git a/src/core/webhooks.core.ts b/src/core/webhooks.core.ts
index 0e267f10..45b651bc 100644
--- a/src/core/webhooks.core.ts
+++ b/src/core/webhooks.core.ts
@@ -1,4 +1,4 @@
-import {WhatsappSession} from "./abc/session.abc";
+import {WAHAInternalEvent, WhatsappSession} from "./abc/session.abc";
import {ConsoleLogger} from "@nestjs/common";
import {WAEvents} from "../structures/enums.dto";
import {WAWebhook} from "../structures/responses.dto";
diff --git a/src/structures/chatting.dto.ts b/src/structures/chatting.dto.ts
index df1043b5..35155b00 100644
--- a/src/structures/chatting.dto.ts
+++ b/src/structures/chatting.dto.ts
@@ -41,6 +41,22 @@ export class ChatRequest extends SessionBaseRequest {
chatId: string
}
+export class SendSeenRequest extends ChatRequest {
+ @ApiProperty({
+ example: 'false_11111111111@c.us_AAAAAAAAAAAAAAAAAAAA',
+ required: false,
+ description: "NOWEB engine only - it's important to mark ALL messages as seen"
+ })
+ messageId?: string
+
+ @ApiProperty({
+ example: '11111111111@c.us',
+ required: false,
+ description: "NOWEB engine only - the ID of the user that sent the message (undefined for individual chats)"
+ })
+ participant?: string
+}
+
export class MessageRequest extends SessionBaseRequest {
@ApiProperty({
example: "false_11111111111@c.us_AAAAAAAAAAAAAAAAAAAA",
@@ -59,12 +75,12 @@ export class MessageTextRequest extends ChatRequest {
export class Button {
id: string
- body: string
+ text: string
}
export class MessageTextButtonsRequest extends ChatRequest {
title: string;
- text: string;
+ footer?: string
buttons?: Array