project push

This commit is contained in:
Anton Voylenko 2024-11-28 21:07:25 +02:00
commit 2399a31aac
31 changed files with 20580 additions and 0 deletions

30
.dockerignore Normal file
View file

@ -0,0 +1,30 @@
# Ignore node_modules
node_modules
# Ignore dotenv files
.env
.env.example
# Ignore logs
logs
# Ignore test files
tests
# Ignore session files
sessions
sessions_test
# Ignore git related files
.git
.gitignore
# Ignore other unnecessary files
README.md
CONTRIBUTING.md
LICENSE.md
Dockerfile
docker-compose.yml
swagger.yml
.github
assets

23
.env.example Normal file
View file

@ -0,0 +1,23 @@
## Application ##
PORT=3000 # OPTIONAL, DEFAULT 3000
API_KEY=your_global_api_key_here # OPTIONAL, DEFAULT EMPTY
BASE_WEBHOOK_URL=http://localhost:3000/localCallbackExample # MANDATORY
ENABLE_LOCAL_CALLBACK_EXAMPLE=TRUE # OPTIONAL, DISABLE FOR PRODUCTION
RATE_LIMIT_MAX=1000 # OPTIONAL, THE MAXIUM NUMBER OF CONNECTIONS TO ALLOW PER TIME FRAME
RATE_LIMIT_WINDOW_MS=1000 # OPTIONAL, TIME FRAME FOR WHICH REQUESTS ARE CHECKED IN MS
## Client ##
MAX_ATTACHMENT_SIZE=10000000 # IF REACHED, MEDIA ATTACHMENT BODY WILL BE NULL
SET_MESSAGES_AS_SEEN=TRUE # WILL MARK THE MESSAGES AS READ AUTOMATICALLY
# ALL CALLBACKS: auth_failure|authenticated|call|change_state|disconnected|group_join|group_leave|group_update|loading_screen|media_uploaded|message|message_ack|message_create|message_reaction|message_revoke_everyone|qr|ready|contact_changed|unread_count|message_edit|message_ciphertext
DISABLED_CALLBACKS=message_ack|message_reaction|unread_count|message_edit|message_ciphertext # PREVENT SENDING CERTAIN TYPES OF CALLBACKS BACK TO THE WEBHOOK
WEB_VERSION='2.2328.5' # OPTIONAL, THE VERSION OF WHATSAPP WEB TO USE
WEB_VERSION_CACHE_TYPE=none # OPTIONAL, DETERMINTES WHERE TO GET THE WHATSAPP WEB VERSION(local, remote or none), DEFAULT 'none'
RECOVER_SESSIONS=TRUE # OPTIONAL, SHOULD WE RECOVER THE SESSION IN CASE OF PAGE FAILURES
CHROME_BIN= # OPTIONAL, PATH TO CHROME BINARY
HEADLESS=TRUE # OPTIONAL, RUN CHROME IN HEADLESS MODE
## Session File Storage ##
SESSIONS_PATH=./sessions # OPTIONAL
ENABLE_SWAGGER_ENDPOINT=TRUE # OPTIONAL

16
.eslintrc.js Normal file
View file

@ -0,0 +1,16 @@
module.exports = {
env: {
browser: true,
es2021: true,
jest: true
},
extends: 'standard',
overrides: [
],
parserOptions: {
ecmaVersion: 'latest',
sourceType: 'module'
},
rules: {
}
}

25
.github/workflows/pull_request.yml vendored Normal file
View file

@ -0,0 +1,25 @@
name: CI/CD Pipeline for Pull Requests to Master
'on':
pull_request:
branches:
- master
jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
node-version:
- 18.x
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: 'Use Node.js ${{ matrix.node-version }}'
uses: actions/setup-node@v4
with:
node-version: '${{ matrix.node-version }}'
- name: Install dependencies
run: npm ci
- name: Run tests
run: npm test
timeout-minutes: 1

50
.github/workflows/push.yml vendored Normal file
View file

@ -0,0 +1,50 @@
name: CI/CD Pipeline for Docker Tag Push
'on':
push:
tags:
- 'v*'
jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
node-version:
- 18.x
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: 'Use Node.js ${{ matrix.node-version }}'
uses: actions/setup-node@v4
with:
node-version: '${{ matrix.node-version }}'
- name: Install dependencies
run: npm ci
- name: Run tests
run: npm test
timeout-minutes: 1
docker:
needs: test
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v3
- name: Set up QEMU
uses: docker/setup-qemu-action@v2
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
- name: Login to Docker Hub
uses: docker/login-action@v2
with:
username: '${{ secrets.DOCKER_HUB_USERNAME }}'
password: '${{ secrets.DOCKER_HUB_TOKEN }}'
- name: Build and push with dynamic tag
uses: docker/build-push-action@v5
with:
platforms: linux/amd64,linux/arm64,linux/arm/v7
push: true
tags: |
avoylenko/wwebjs-api:${{ github.ref_name }}
avoylenko/wwebjs-api:latest

20
.gitignore vendored Normal file
View file

@ -0,0 +1,20 @@
# Ignore node_modules
node_modules
# Ignore dotenv files
.env
# Ignore sessions
sessions
sessions_test
.wwebjs_cache
# Ignore logs
logs
# Ignore test coverage reports
coverage
# Ignore other unnecessary files
.DS_Store
.vscode

57
CONTRIBUTING.md Normal file
View file

@ -0,0 +1,57 @@
# Contributing to WWebJS REST API Wrapper
Welcome to WWebJS API Wrapper! We appreciate your interest in contributing to this project. Please follow the guidelines below to contribute effectively.
## Getting Started
1. Fork the repository.
2. Clone your forked repository to your local machine.
3. Install the necessary dependencies by running `npm install`.
4. Create a new branch for your contribution.
## Code Style
- Follow the existing code style and conventions in the project.
- Use meaningful variable and function names.
- Add comments to your code, especially for complex or tricky parts.
## Pull Requests
- Create a pull request from your branch to the `master` branch of this repository.
- Provide a clear and descriptive title for your pull request.
- Include a detailed description of the changes you made in the pull request.
- Reference any related issues in your pull request description using the `#` symbol followed by the issue number.
## Testing
- Write appropriate unit tests for your code.
- Make sure all existing tests pass.
- Provide instructions for testing your changes, if necessary.
## Documentation
- Update the README.md file with any relevant information about your contribution, including installation instructions, usage examples, and API documentation.
## Code Block Example
When providing code examples or error messages, please use code blocks. You can create a code block by wrapping your code or message with triple backticks (\```) on separate lines, like this:
\```
// Example code block
const hello = "Hello, world!";
console.log(hello);
\```
This will render as:
```
// Example code block
const hello = "Hello, world!";
console.log(hello);
```
## Contact Us
If you have any questions or need further assistance, feel free to contact us by opening an issue or reaching out to us through email or chat.
Thank you for your contribution!

33
Dockerfile Normal file
View file

@ -0,0 +1,33 @@
# Use the official Node.js Alpine image as the base image
FROM node:18-alpine
# Set the working directory
WORKDIR /usr/src/app
# Install Chromium
ENV CHROME_BIN="/usr/bin/chromium-browser" \
PUPPETEER_SKIP_CHROMIUM_DOWNLOAD="true" \
NODE_ENV="production"
RUN set -x \
&& apk update \
&& apk upgrade \
&& apk add --no-cache \
udev \
ttf-freefont \
chromium \
ffmpeg
# Copy package.json and package-lock.json to the working directory
COPY package*.json ./
# Install the dependencies
RUN npm ci --only=production --ignore-scripts
# Copy the rest of the source code to the working directory
COPY . .
# Expose the port the API will run on
EXPOSE 3000
# Start the API
CMD ["npm", "start"]

19
LICENSE.md Normal file
View file

@ -0,0 +1,19 @@
# MIT License
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
The WhatsApp Web.js REST API Wrapper is licensed under the MIT License, which is a permissive open source license that allows you to use, modify, and distribute the software for both commercial and non-commercial purposes. Please see the full license text below.
## License
MIT License
```
MIT License
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
```

178
README.md Normal file
View file

@ -0,0 +1,178 @@
# WWebJS REST API
REST API wrapper for the [whatsapp-web.js](https://github.com/pedroslopez/whatsapp-web.js) library, providing an easy-to-use interface to interact with the WhatsApp Web platform.
It is designed to be used as a docker container, scalable, secure, and easy to integrate with other non-NodeJS projects.
This project is a fork of [whatsapp-api](https://github.com/chrishubert/whatsapp-api). As the project was abandoned by the original author, all future improvements will be in this repo.
The project is a work in progress: star it, create issues, features or pull requests ❣️
**NOTE**: I can't guarantee you will not be blocked by using this method, although it has worked for me. WhatsApp does not allow bots or unofficial clients on their platform, so this shouldn't be considered totally safe.
## Table of Contents
[1. Quick Start with Docker](#quick-start-with-docker)
[2. Features](#features)
[3. Run Locally](#run-locally)
[4. Testing](#testing)
[5. Documentation](#documentation)
[6. Deploy to Production](#deploy-to-production)
[7. Contributing](#contributing)
[8. License](#license)
[9. Star History](#star-history)
## Quick Start with Docker
[![dockeri.co](https://dockerico.blankenship.io/image/avoylenko/wwebjs-api)](https://hub.docker.com/r/avoylenko/wwebjs-api)
1. Clone the repository:
```bash
git clone https://github.com/avoylenko/wwebjs-api.git
cd wwebjs-api
```
3. Run the Docker Compose:
```bash
docker-compose pull && docker-compose up
```
4. Visit http://localhost:3000/session/start/ABCD
5. Scan the QR on your console using WhatsApp mobile app -> Linked Device -> Link a Device (it may take time to setup the session)
6. Visit http://localhost:3000/client/getContacts/ABCD
7. EXTRA: Look at all the callbacks data in `./session/message_log.txt`
![Quick Start](./assets/basic_start.gif)
## Features
1. API and Callbacks
| Actions | Status | Sessions | Status | Callbacks | Status |
| ----------------------------| ------| ----------------------------------------| ------| ----------------------------------------------| ------|
| Send Image Message | ✅ | Initiate session | ✅ | Callback QR code | ✅ |
| Send Video Message | ✅ | Terminate session | ✅ | Callback new message | ✅ |
| Send Audio Message | ✅ | Terminate inactive sessions | ✅ | Callback status change | ✅ |
| Send Document Message | ✅ | Terminate all sessions | ✅ | Callback message media attachment | ✅ |
| Send File URL | ✅ | Healthcheck | ✅ | | |
| Send Button Message | ✅ | Local test callback | | | |
| Send Contact Message | ✅ | | | | |
| Send List Message | ✅ | | | | |
| Set Status | ✅ | | | | |
| Send Button With Media | ✅ | | | | |
| Is On Whatsapp? | ✅ | | | | |
| Download Profile Pic | ✅ | | | | |
| User Status | ✅ | | | | |
| Block/Unblock User | ✅ | | | | |
| Update Profile Picture | ✅ | | | | |
| Create Group | ✅ | | | | |
| Leave Group | ✅ | | | | |
| All Groups | ✅ | | | | |
| Invite User | ✅ | | | | |
| Make Admin | ✅ | | | | |
| Demote Admin | ✅ | | | | |
| Group Invite Code | ✅ | | | | |
| Update Group Participants | ✅ | | | | |
| Update Group Setting | ✅ | | | | |
| Update Group Subject | ✅ | | | | |
| Update Group Description | ✅ | | | | |
3. Handle multiple client sessions (session data saved locally), identified by unique id
4. All endpoints may be secured by a global API key
5. On server start, all existing sessions are restored
6. Set messages automatically as read
7. Disable any of the callbacks
## Run Locally
1. Clone the repository:
```bash
git clone https://github.com/avoylenko/wwebjs-api.git
cd wwebjs-api
```
2. Install the dependencies:
```bash
npm install
```
3. Copy the `.env.example` file to `.env` and update the required environment variables:
```bash
cp .env.example .env
```
4. Run the application:
```bash
npm run start
```
5. Access the API at `http://localhost:3000`
## Testing
Run the test suite with the following command:
```bash
npm run test
```
## Documentation
API documentation can be found in the [`swagger.json`](https://raw.githubusercontent.com/avoylenko/wwebjs-api/master/swagger.json) file. See this file directly into [Swagger Editor](https://editor.swagger.io/?url=https://raw.githubusercontent.com/avoylenko/wwebjs-api/master/swagger.json) or any other OpenAPI-compatible tool to view and interact with the API documentation.
This documentation is straightforward if you are familiar with whatsapp-web.js library (https://docs.wwebjs.dev/)
If you are still confused - open an issue and I'll improve it.
Also, there is an option to run the documentation endpoint locally by setting the `ENABLE_SWAGGER_ENDPOINT` environment variable. Restart the service and go to `/api-docs` endpoint to see it.
By default, all callback events are delivered to the webhook defined with the `BASE_WEBHOOK_URL` environment variable.
This can be overridden by setting the `*_WEBHOOK_URL` environment variable, where `*` is your sessionId.
For example, if you have the sessionId defined as `DEMO`, the environment variable must be `DEMO_WEBHOOK_URL`.
By setting the `DISABLED_CALLBACKS` environment variable you can specify what events you are **not** willing to receive on your webhook.
### Scanning QR code
In order to validate a new WhatsApp Web instance you need to scan the QR code using your mobile phone. Official documentation can be found at (https://faq.whatsapp.com/1079327266110265/?cms_platform=android) page. The service itself delivers the QR code content as a webhook event or you can use the REST endpoints (`/session/qr/:sessionId` or `/session/qr/:sessionId/image` to get the QR code as a png image).
## Deploy to Production
- Load the docker image in docker-compose, or your Kubernetes environment
- Disable the `ENABLE_LOCAL_CALLBACK_EXAMPLE` environment variable
- Set the `API_KEY` environment variable to protect the REST endpoints
- Run periodically the `/api/terminateInactiveSessions` endpoint to prevent useless sessions to take up space and resources(only in case you are not in control of the sessions)
## Contributing
Please read [CONTRIBUTING.md](./CONTRIBUTING.md) for details on our code of conduct, and the process for submitting pull requests to us.
## Disclaimer
This project is not affiliated, associated, authorized, endorsed by, or in any way officially connected with WhatsApp or any of its subsidiaries or its affiliates. The official WhatsApp website can be found at https://whatsapp.com. "WhatsApp" as well as related names, marks, emblems and images are registered trademarks of their respective owners.
## License
This project is licensed under the MIT License - see the [LICENSE.md](./LICENSE.md) file for details.
## Star History
[![Star History Chart](https://api.star-history.com/svg?repos=avoylenko/wwebjs-api&type=Date)](https://star-history.com/#avoylenko/wwebjs-api&Date)

BIN
assets/basic_start.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 591 KiB

26
docker-compose.yml Normal file
View file

@ -0,0 +1,26 @@
version: '3.8'
services:
app:
container_name: wwebjs_api
# build: .
image: avoylenko/wwebjs-api:latest # Pull the image from Docker Hub
restart: always
ports:
- "3000:3000"
environment:
# - API_KEY=your_global_api_key_here # OPTIONAL
- BASE_WEBHOOK_URL=http://localhost:3000/localCallbackExample
- ENABLE_LOCAL_CALLBACK_EXAMPLE=TRUE # OPTIONAL, NOT RECOMMENDED FOR PRODUCTION
- MAX_ATTACHMENT_SIZE=5000000 # IN BYTES
- SET_MESSAGES_AS_SEEN=TRUE # WILL MARK THE MESSAGES AS READ AUTOMATICALLY
# ALL CALLBACKS: auth_failure|authenticated|call|change_state|disconnected|group_join|group_leave|group_update|loading_screen|media_uploaded|message|message_ack|message_create|message_reaction|message_revoke_everyone|qr|ready|contact_changed
- DISABLED_CALLBACKS=message_ack|message_reaction # PREVENT SENDING CERTAIN TYPES OF CALLBACKS BACK TO THE WEBHOOK
- ENABLE_SWAGGER_ENDPOINT=TRUE # OPTIONAL, ENABLES THE /api-docs ENDPOINT
# - RATE_LIMIT_MAX=1000 # OPTIONAL, THE MAXIUM NUMBER OF CONNECTIONS TO ALLOW PER TIME FRAME
# - RATE_LIMIT_WINDOW_MS=1000 # OPTIONAL, TIME FRAME FOR WHICH REQUESTS ARE CHECKED IN MS
# - WEB_VERSION='2.2328.5' # OPTIONAL, THE VERSION OF WHATSAPP WEB TO USE
# - WEB_VERSION_CACHE_TYPE=none # OPTIONAL, DETERMINES WHERE TO GET THE WHATSAPP WEB VERSION(local, remote or none), DEFAULT 'none'
# - RECOVER_SESSIONS=TRUE # OPTIONAL, SHOULD WE RECOVER THE SESSION IN CASE OF PAGE FAILURES
volumes:
- ./sessions:/usr/src/app/sessions # Mount the local ./sessions/ folder to the container's /usr/src/app/sessions folder

8282
package-lock.json generated Normal file

File diff suppressed because it is too large Load diff

57
package.json Normal file
View file

@ -0,0 +1,57 @@
{
"name": "wwebjs-api",
"version": "1.0.0",
"description": "REST API wrapper for whatsapp-web.js",
"main": "server.js",
"scripts": {
"start": "node server.js",
"test": "jest --runInBand",
"swagger": "node swagger.js"
},
"dependencies": {
"axios": "^1.7.8",
"dotenv": "^16.4.5",
"express": "^4.21.1",
"express-rate-limit": "^7.4.1",
"qr-image": "^3.2.0",
"qrcode-terminal": "^0.12.0",
"swagger-ui-express": "^5.0.1",
"whatsapp-web.js": "^1.26.0"
},
"devDependencies": {
"eslint": "^8.38.0",
"eslint-config-standard": "^17.0.0",
"eslint-plugin-import": "^2.27.5",
"eslint-plugin-n": "^15.7.0",
"eslint-plugin-promise": "^6.1.1",
"jest": "^29.5.0",
"supertest": "^6.3.3",
"swagger-autogen": "^2.23.7"
},
"keywords": [
"whatsapp",
"whatsapp-web",
"wwebjs",
"api",
"wrapper",
"rest",
"express",
"axios"
],
"author": {
"name": "Anton Voylenko",
"email": "anton.voylenko@gmail.com"
},
"license": "MIT",
"engines": {
"node": ">=18"
},
"repository": {
"type": "git",
"url": "https://github.com/avoylenko/wwebjs-api.git"
},
"bugs": {
"url": "https://github.com/avoylenko/wwebjs-api/issues"
},
"homepage": "https://github.com/avoylenko/wwebjs-api"
}

16
server.js Normal file
View file

@ -0,0 +1,16 @@
const app = require('./src/app')
const { baseWebhookURL } = require('./src/config')
require('dotenv').config()
// Start the server
const port = process.env.PORT || 3000
// Check if BASE_WEBHOOK_URL environment variable is available
if (!baseWebhookURL) {
console.error('BASE_WEBHOOK_URL environment variable is not set. Exiting...')
process.exit(1) // Terminate the application with an error code
}
app.listen(port, () => {
console.log(`Server running on port ${port}`)
})

17
src/app.js Normal file
View file

@ -0,0 +1,17 @@
require('./routes')
const express = require('express')
const { restoreSessions } = require('./sessions')
const { routes } = require('./routes')
const { maxAttachmentSize } = require('./config')
const app = express()
// Initialize Express app
app.disable('x-powered-by')
app.use(express.json({ limit: maxAttachmentSize + 1000000 }))
app.use(express.urlencoded({ limit: maxAttachmentSize + 1000000, extended: true }))
app.use('/', routes)
restoreSessions()
module.exports = app

37
src/config.js Normal file
View file

@ -0,0 +1,37 @@
// Load environment variables from .env file
require('dotenv').config()
// setup global const
const sessionFolderPath = process.env.SESSIONS_PATH || './sessions'
const enableLocalCallbackExample = (process.env.ENABLE_LOCAL_CALLBACK_EXAMPLE || '').toLowerCase() === 'true'
const globalApiKey = process.env.API_KEY
const baseWebhookURL = process.env.BASE_WEBHOOK_URL
const maxAttachmentSize = parseInt(process.env.MAX_ATTACHMENT_SIZE) || 10000000
const setMessagesAsSeen = (process.env.SET_MESSAGES_AS_SEEN || '').toLowerCase() === 'true'
const disabledCallbacks = process.env.DISABLED_CALLBACKS ? process.env.DISABLED_CALLBACKS.split('|') : []
const enableSwaggerEndpoint = (process.env.ENABLE_SWAGGER_ENDPOINT || '').toLowerCase() === 'true'
const webVersion = process.env.WEB_VERSION
const webVersionCacheType = process.env.WEB_VERSION_CACHE_TYPE || 'none'
const rateLimitMax = parseInt(process.env.RATE_LIMIT_MAX) || 1000
const rateLimitWindowMs = parseInt(process.env.RATE_LIMIT_WINDOW_MS) || 1000
const recoverSessions = (process.env.RECOVER_SESSIONS || '').toLowerCase() === 'true'
const chromeBin = process.env.CHROME_BIN || null
const headless = process.env.HEADLESS ? (process.env.HEADLESS).toLowerCase() === 'true' : true
module.exports = {
sessionFolderPath,
enableLocalCallbackExample,
globalApiKey,
baseWebhookURL,
maxAttachmentSize,
setMessagesAsSeen,
disabledCallbacks,
enableSwaggerEndpoint,
webVersion,
webVersionCacheType,
rateLimitMax,
rateLimitWindowMs,
recoverSessions,
chromeBin,
headless
}

View file

@ -0,0 +1,237 @@
const { sessions } = require('../sessions')
const { sendErrorResponse } = require('../utils')
/**
* @function
* @async
* @name getClassInfo
* @description Gets information about a chat using the chatId and sessionId
* @param {Object} req - Request object
* @param {Object} res - Response object
* @param {string} req.body.chatId - The ID of the chat to get information for
* @param {string} req.params.sessionId - The ID of the session to use
* @returns {Object} - Returns a JSON object with the success status and chat information
* @throws {Error} - Throws an error if chat is not found or if there is a server error
*/
const getClassInfo = async (req, res) => {
try {
const { chatId } = req.body
const client = sessions.get(req.params.sessionId)
const chat = await client.getChatById(chatId)
if (!chat) { sendErrorResponse(res, 404, 'Chat not Found') }
res.json({ success: true, chat })
} catch (error) {
sendErrorResponse(res, 500, error.message)
}
}
/**
* Clears all messages in a chat.
*
* @function
* @async
* @param {Object} req - The request object.
* @param {Object} res - The response object.
* @param {string} req.params.sessionId - The ID of the session.
* @param {string} req.body.chatId - The ID of the chat to clear messages from.
* @throws {Error} If the chat is not found or there is an internal server error.
* @returns {Object} The success status and the cleared messages.
*/
const clearMessages = async (req, res) => {
try {
const { chatId } = req.body
const client = sessions.get(req.params.sessionId)
const chat = await client.getChatById(chatId)
if (!chat) { sendErrorResponse(res, 404, 'Chat not Found') }
const clearMessages = await chat.clearMessages()
res.json({ success: true, clearMessages })
} catch (error) {
sendErrorResponse(res, 500, error.message)
}
}
/**
* Stops typing or recording in chat immediately.
*
* @function
* @async
* @param {Object} req - Request object.
* @param {Object} res - Response object.
* @param {string} req.body.chatId - ID of the chat to clear the state for.
* @param {string} req.params.sessionId - ID of the session the chat belongs to.
* @returns {Promise<void>} - A Promise that resolves with a JSON object containing a success flag and the result of clearing the state.
* @throws {Error} - If there was an error while clearing the state.
*/
const clearState = async (req, res) => {
try {
const { chatId } = req.body
const client = sessions.get(req.params.sessionId)
const chat = await client.getChatById(chatId)
if (!chat) { sendErrorResponse(res, 404, 'Chat not Found') }
const clearState = await chat.clearState()
res.json({ success: true, clearState })
} catch (error) {
sendErrorResponse(res, 500, error.message)
}
}
/**
* Delete a chat.
*
* @async
* @function
* @param {Object} req - The request object.
* @param {Object} res - The response object.
* @param {string} req.params.sessionId - The session ID.
* @param {string} req.body.chatId - The ID of the chat to be deleted.
* @returns {Object} A JSON response indicating whether the chat was deleted successfully.
* @throws {Object} If there is an error while deleting the chat, an error response is sent with a status code of 500.
* @throws {Object} If the chat is not found, an error response is sent with a status code of 404.
*/
const deleteChat = async (req, res) => {
try {
const { chatId } = req.body
const client = sessions.get(req.params.sessionId)
const chat = await client.getChatById(chatId)
if (!chat) { sendErrorResponse(res, 404, 'Chat not Found') }
const deleteChat = await chat.delete()
res.json({ success: true, deleteChat })
} catch (error) {
sendErrorResponse(res, 500, error.message)
}
}
/**
* Fetches messages from a specified chat.
*
* @function
* @async
*
* @param {Object} req - The request object containing sessionId, chatId, and searchOptions.
* @param {string} req.params.sessionId - The ID of the session associated with the chat.
* @param {Object} req.body - The body of the request containing chatId and searchOptions.
* @param {string} req.body.chatId - The ID of the chat from which to fetch messages.
* @param {Object} req.body.searchOptions - The search options to use when fetching messages.
*
* @param {Object} res - The response object to send the fetched messages.
* @returns {Promise<Object>} A JSON object containing the success status and fetched messages.
*
* @throws {Error} If the chat is not found or there is an error fetching messages.
*/
const fetchMessages = async (req, res) => {
try {
/*
#swagger.requestBody = {
required: true,
schema: {
type: 'object',
properties: {
chatId: {
type: 'string',
description: 'Unique whatsApp identifier for the given Chat (either group or personnal)',
example: '6281288888888@c.us'
},
searchOptions: {
type: 'object',
description: 'Search options for fetching messages',
example: '{}'
}
}
}
}
*/
const { chatId, searchOptions } = req.body
const client = sessions.get(req.params.sessionId)
const chat = await client.getChatById(chatId)
if (!chat) { sendErrorResponse(res, 404, 'Chat not Found') }
const messages = await chat.fetchMessages(searchOptions)
res.json({ success: true, messages })
} catch (error) {
sendErrorResponse(res, 500, error.message)
}
}
/**
* Gets the contact for a chat
* @async
* @function
* @param {Object} req - The HTTP request object
* @param {Object} res - The HTTP response object
* @param {string} req.params.sessionId - The ID of the current session
* @param {string} req.body.chatId - The ID of the chat to get the contact for
* @returns {Promise<void>} - Promise that resolves with the chat's contact information
* @throws {Error} - Throws an error if chat is not found or if there is an error getting the contact information
*/
const getContact = async (req, res) => {
try {
const { chatId } = req.body
const client = sessions.get(req.params.sessionId)
const chat = await client.getChatById(chatId)
if (!chat) { sendErrorResponse(res, 404, 'Chat not Found') }
const contact = await chat.getContact()
res.json({ success: true, contact })
} catch (error) {
sendErrorResponse(res, 500, error.message)
}
}
/**
* Send a recording state to a WhatsApp chat.
* @async
* @function
* @param {object} req - The request object.
* @param {object} res - The response object.
* @param {string} req.params.sessionId - The session ID.
* @param {object} req.body - The request body.
* @param {string} req.body.chatId - The ID of the chat to send the recording state to.
* @returns {object} - An object containing a success message and the result of the sendStateRecording method.
* @throws {object} - An error object containing a status code and error message if an error occurs.
*/
const sendStateRecording = async (req, res) => {
try {
const { chatId } = req.body
const client = sessions.get(req.params.sessionId)
const chat = await client.getChatById(chatId)
if (!chat) { sendErrorResponse(res, 404, 'Chat not Found') }
const sendStateRecording = await chat.sendStateRecording()
res.json({ success: true, sendStateRecording })
} catch (error) {
sendErrorResponse(res, 500, error.message)
}
}
/**
* Send a typing state to a WhatsApp chat.
* @async
* @function
* @param {object} req - The request object.
* @param {object} res - The response object.
* @param {string} req.params.sessionId - The session ID.
* @param {object} req.body - The request body.
* @param {string} req.body.chatId - The ID of the chat to send the typing state to.
* @returns {object} - An object containing a success message and the result of the sendStateTyping method.
* @throws {object} - An error object containing a status code and error message if an error occurs.
*/
const sendStateTyping = async (req, res) => {
try {
const { chatId } = req.body
const client = sessions.get(req.params.sessionId)
const chat = await client.getChatById(chatId)
if (!chat) { sendErrorResponse(res, 404, 'Chat not Found') }
const sendStateTyping = await chat.sendStateTyping()
res.json({ success: true, sendStateTyping })
} catch (error) {
sendErrorResponse(res, 500, error.message)
}
}
module.exports = {
getClassInfo,
clearMessages,
clearState,
deleteChat,
fetchMessages,
getContact,
sendStateRecording,
sendStateTyping
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,218 @@
const { sessions } = require('../sessions')
const { sendErrorResponse } = require('../utils')
/**
* Retrieves information about a WhatsApp contact by ID.
*
* @async
* @function
* @param {Object} req - The request object.
* @param {Object} res - The response object.
* @param {string} req.params.sessionId - The ID of the current session.
* @param {string} req.body.contactId - The ID of the contact to retrieve information for.
* @throws {Error} If there is an error retrieving the contact information.
* @returns {Object} The contact information object.
*/
const getClassInfo = async (req, res) => {
try {
const { contactId } = req.body
const client = sessions.get(req.params.sessionId)
const contact = await client.getContactById(contactId)
if (!contact) {
sendErrorResponse(res, 404, 'Contact not Found')
}
res.json({ success: true, result: contact })
} catch (error) {
sendErrorResponse(res, 500, error.message)
}
}
/**
* Blocks a WhatsApp contact by ID.
*
* @async
* @function
* @param {Object} req - The request object.
* @param {Object} res - The response object.
* @param {string} req.params.sessionId - The ID of the current session.
* @param {string} req.body.contactId - The ID of the contact to block.
* @throws {Error} If there is an error blocking the contact.
* @returns {Object} The result of the blocking operation.
*/
const block = async (req, res) => {
try {
const { contactId } = req.body
const client = sessions.get(req.params.sessionId)
const contact = await client.getContactById(contactId)
if (!contact) {
sendErrorResponse(res, 404, 'Contact not Found')
}
const result = await contact.block()
res.json({ success: true, result })
} catch (error) {
sendErrorResponse(res, 500, error.message)
}
}
/**
* Retrieves the 'About' information of a WhatsApp contact by ID.
*
* @async
* @function
* @param {Object} req - The request object.
* @param {Object} res - The response object.
* @param {string} req.params.sessionId - The ID of the current session.
* @param {string} req.body.contactId - The ID of the contact to retrieve 'About' information for.
* @throws {Error} If there is an error retrieving the contact information.
* @returns {Object} The 'About' information of the contact.
*/
const getAbout = async (req, res) => {
try {
const { contactId } = req.body
const client = sessions.get(req.params.sessionId)
const contact = await client.getContactById(contactId)
if (!contact) {
sendErrorResponse(res, 404, 'Contact not Found')
}
const result = await contact.getAbout()
res.json({ success: true, result })
} catch (error) {
sendErrorResponse(res, 500, error.message)
}
}
/**
* Retrieves the chat information of a contact with a given contactId.
*
* @async
* @function getChat
* @param {Object} req - The request object.
* @param {Object} res - The response object.
* @param {string} req.params.sessionId - The session ID.
* @param {string} req.body.contactId - The ID of the client whose chat information is being retrieved.
* @throws {Error} If the contact with the given contactId is not found or if there is an error retrieving the chat information.
* @returns {Promise<void>} A promise that resolves with the chat information of the contact.
*/
const getChat = async (req, res) => {
try {
const { contactId } = req.body
const client = sessions.get(req.params.sessionId)
const contact = await client.getContactById(contactId)
if (!contact) { sendErrorResponse(res, 404, 'Contact not Found') }
const result = await contact.getChat()
res.json({ success: true, result })
} catch (error) {
sendErrorResponse(res, 500, error.message)
}
}
/**
* Retrieves the formatted number of a contact with a given contactId.
*
* @async
* @function getFormattedNumber
* @param {Object} req - The request object.
* @param {Object} res - The response object.
* @param {string} req.params.sessionId - The session ID.
* @param {string} req.body.contactId - The ID of the client whose chat information is being retrieved.
* @throws {Error} If the contact with the given contactId is not found or if there is an error retrieving the chat information.
* @returns {Promise<void>} A promise that resolves with the formatted number of the contact.
*/
const getFormattedNumber = async (req, res) => {
try {
const { contactId } = req.body
const client = sessions.get(req.params.sessionId)
const contact = await client.getContactById(contactId)
if (!contact) { sendErrorResponse(res, 404, 'Contact not Found') }
const result = await contact.getFormattedNumber()
res.json({ success: true, result })
} catch (error) {
sendErrorResponse(res, 500, error.message)
}
}
/**
* Retrieves the country code of a contact with a given contactId.
*
* @async
* @function getCountryCode
* @param {Object} req - The request object.
* @param {Object} res - The response object.
* @param {string} req.params.sessionId - The session ID.
* @param {string} req.body.contactId - The ID of the client whose chat information is being retrieved.
* @throws {Error} If the contact with the given contactId is not found or if there is an error retrieving the chat information.
* @returns {Promise<void>} A promise that resolves with the country code of the contact.
*/
const getCountryCode = async (req, res) => {
try {
const { contactId } = req.body
const client = sessions.get(req.params.sessionId)
const contact = await client.getContactById(contactId)
if (!contact) { sendErrorResponse(res, 404, 'Contact not Found') }
const result = await contact.getCountryCode()
res.json({ success: true, result })
} catch (error) {
sendErrorResponse(res, 500, error.message)
}
}
/**
* Retrieves the profile picture url of a contact with a given contactId.
*
* @async
* @function getProfilePicUrl
* @param {Object} req - The request object.
* @param {Object} res - The response object.
* @param {string} req.params.sessionId - The session ID.
* @param {string} req.body.contactId - The ID of the client whose chat information is being retrieved.
* @throws {Error} If the contact with the given contactId is not found or if there is an error retrieving the chat information.
* @returns {Promise<void>} A promise that resolves with the profile picture url of the contact.
*/
const getProfilePicUrl = async (req, res) => {
try {
const { contactId } = req.body
const client = sessions.get(req.params.sessionId)
const contact = await client.getContactById(contactId)
if (!contact) { sendErrorResponse(res, 404, 'Contact not Found') }
const result = await contact.getProfilePicUrl() || null
res.json({ success: true, result })
} catch (error) {
sendErrorResponse(res, 500, error.message)
}
}
/**
* Unblocks the contact with a given contactId.
*
* @async
* @function unblock
* @param {Object} req - The request object.
* @param {Object} res - The response object.
* @param {string} req.params.sessionId - The session ID.
* @param {string} req.body.contactId - The ID of the client whose contact is being unblocked.
* @throws {Error} If the contact with the given contactId is not found or if there is an error unblocking the contact.
* @returns {Promise<void>} A promise that resolves with the result of unblocking the contact.
*/
const unblock = async (req, res) => {
try {
const { contactId } = req.body
const client = sessions.get(req.params.sessionId)
const contact = await client.getContactById(contactId)
if (!contact) { sendErrorResponse(res, 404, 'Contact not Found') }
const result = await contact.unblock()
res.json({ success: true, result })
} catch (error) {
sendErrorResponse(res, 500, error.message)
}
}
module.exports = {
getClassInfo,
block,
getAbout,
getChat,
unblock,
getFormattedNumber,
getCountryCode,
getProfilePicUrl
}

View file

@ -0,0 +1,355 @@
const { MessageMedia } = require('whatsapp-web.js')
const { sessions } = require('../sessions')
const { sendErrorResponse } = require('../utils')
/**
* Adds participants to a group chat.
* @async
* @function
* @param {Object} req - The request object containing the chatId and contactIds in the body.
* @param {string} req.body.chatId - The ID of the group chat.
* @param {Array<string>} req.body.contactIds - An array of contact IDs to be added to the group.
* @param {Object} res - The response object.
* @returns {Object} Returns a JSON object containing a success flag and the updated participants list.
* @throws {Error} Throws an error if the chat is not a group chat.
*/
const addParticipants = async (req, res) => {
try {
const { chatId, contactIds } = req.body
const client = sessions.get(req.params.sessionId)
const chat = await client.getChatById(chatId)
if (!chat.isGroup) { throw new Error('The chat is not a group') }
await chat.addParticipants(contactIds)
res.json({ success: true, participants: chat.participants })
} catch (error) {
sendErrorResponse(res, 500, error.message)
}
}
/**
* Removes participants from a group chat
*
* @async
* @function
* @param {Object} req - Express request object
* @param {Object} res - Express response object
* @returns {Promise<Object>} Returns a JSON object with success flag and updated participants list
* @throws {Error} If chat is not a group
*/
const removeParticipants = async (req, res) => {
try {
const { chatId, contactIds } = req.body
const client = sessions.get(req.params.sessionId)
const chat = await client.getChatById(chatId)
if (!chat.isGroup) { throw new Error('The chat is not a group') }
await chat.removeParticipants(contactIds)
res.json({ success: true, participants: chat.participants })
} catch (error) {
sendErrorResponse(res, 500, error.message)
}
}
/**
* Promotes participants in a group chat to admin
*
* @async
* @function
* @param {Object} req - Express request object
* @param {Object} res - Express response object
* @returns {Promise<Object>} Returns a JSON object with success flag and updated participants list
* @throws {Error} If chat is not a group
*/
const promoteParticipants = async (req, res) => {
try {
const { chatId, contactIds } = req.body
const client = sessions.get(req.params.sessionId)
const chat = await client.getChatById(chatId)
if (!chat.isGroup) { throw new Error('The chat is not a group') }
await chat.promoteParticipants(contactIds)
res.json({ success: true, participants: chat.participants })
} catch (error) {
sendErrorResponse(res, 500, error.message)
}
}
/**
* Demotes admin participants in a group chat
*
* @async
* @function
* @param {Object} req - Express request object
* @param {Object} res - Express response object
* @returns {Promise<Object>} Returns a JSON object with success flag and updated participants list
* @throws {Error} If chat is not a group
*/
const demoteParticipants = async (req, res) => {
try {
const { chatId, contactIds } = req.body
const client = sessions.get(req.params.sessionId)
const chat = await client.getChatById(chatId)
if (!chat.isGroup) { throw new Error('The chat is not a group') }
await chat.demoteParticipants(contactIds)
res.json({ success: true, participants: chat.participants })
} catch (error) {
sendErrorResponse(res, 500, error.message)
}
}
/**
* Gets the invite code for a group chat
*
* @async
* @function
* @param {Object} req - Express request object
* @param {Object} res - Express response object
* @returns {Promise<Object>} Returns a JSON object with success flag and invite code
* @throws {Error} If chat is not a group
*/
const getInviteCode = async (req, res) => {
try {
const { chatId } = req.body
const client = sessions.get(req.params.sessionId)
const chat = await client.getChatById(chatId)
if (!chat.isGroup) { throw new Error('The chat is not a group') }
const inviteCode = await chat.getInviteCode()
res.json({ success: true, inviteCode })
} catch (error) {
sendErrorResponse(res, 500, error.message)
}
}
/**
* Sets the subject of a group chat
*
* @async
* @function
* @param {Object} req - Express request object
* @param {Object} res - Express response object
* @returns {Promise<Object>} Returns a JSON object with success flag and updated chat object
* @throws {Error} If chat is not a group
*/
const setSubject = async (req, res) => {
try {
const { chatId, subject } = req.body
const client = sessions.get(req.params.sessionId)
const chat = await client.getChatById(chatId)
if (!chat.isGroup) { throw new Error('The chat is not a group') }
const success = await chat.setSubject(subject)
res.json({ success, chat })
} catch (error) {
sendErrorResponse(res, 500, error.message)
}
}
/**
* Sets the description of a group chat
*
* @async
* @function
* @param {Object} req - Express request object
* @param {Object} res - Express response object
* @returns {Promise<Object>} Returns a JSON object with success flag and updated chat object
* @throws {Error} If chat is not a group
*/
const setDescription = async (req, res) => {
try {
const { chatId, description } = req.body
const client = sessions.get(req.params.sessionId)
const chat = await client.getChatById(chatId)
if (!chat.isGroup) { throw new Error('The chat is not a group') }
const success = await chat.setDescription(description)
res.json({ success, chat })
} catch (error) {
sendErrorResponse(res, 500, error.message)
}
}
/**
* Leaves a group chat
*
* @async
* @function
* @param {Object} req - Express request object
* @param {Object} res - Express response object
* @returns {Promise<Object>} Returns a JSON object with success flag and outcome of leaving the chat
* @throws {Error} If chat is not a group
*/
const leave = async (req, res) => {
try {
const { chatId } = req.body
const client = sessions.get(req.params.sessionId)
const chat = await client.getChatById(chatId)
if (!chat.isGroup) { throw new Error('The chat is not a group') }
const outcome = await chat.leave()
res.json({ success: true, outcome })
} catch (error) {
sendErrorResponse(res, 500, error.message)
}
}
/**
* Retrieves information about a chat based on the provided chatId
*
* @async
* @function getClassInfo
* @param {object} req - The request object
* @param {object} res - The response object
* @param {string} req.body.chatId - The chatId of the chat to retrieve information about
* @param {string} req.params.sessionId - The sessionId of the client making the request
* @throws {Error} The chat is not a group.
* @returns {Promise<void>} - A JSON response with success true and chat object containing chat information
*/
const getClassInfo = async (req, res) => {
try {
const { chatId } = req.body
const client = sessions.get(req.params.sessionId)
const chat = await client.getChatById(chatId)
if (!chat.isGroup) { throw new Error('The chat is not a group') }
res.json({ success: true, chat })
} catch (error) {
sendErrorResponse(res, 500, error.message)
}
}
/**
* Revokes the invite link for a group chat based on the provided chatId
*
* @async
* @function revokeInvite
* @param {object} req - The request object
* @param {object} res - The response object
* @param {string} req.body.chatId - The chatId of the group chat to revoke the invite for
* @param {string} req.params.sessionId - The sessionId of the client making the request
* @throws {Error} The chat is not a group.
* @returns {Promise<void>} - A JSON response with success true and the new invite code for the group chat
*/
const revokeInvite = async (req, res) => {
try {
const { chatId } = req.body
const client = sessions.get(req.params.sessionId)
const chat = await client.getChatById(chatId)
if (!chat.isGroup) { throw new Error('The chat is not a group') }
const newInviteCode = await chat.revokeInvite()
res.json({ success: true, newInviteCode })
} catch (error) {
sendErrorResponse(res, 500, error.message)
}
}
/**
* Sets admins-only status of a group chat's info or messages.
*
* @async
* @function setInfoAdminsOnly
* @param {Object} req - Request object.
* @param {Object} res - Response object.
* @param {string} req.params.sessionId - ID of the user's session.
* @param {Object} req.body - Request body.
* @param {string} req.body.chatId - ID of the group chat.
* @param {boolean} req.body.adminsOnly - Desired admins-only status.
* @returns {Promise<void>} Promise representing the success or failure of the operation.
* @throws {Error} If the chat is not a group.
*/
const setInfoAdminsOnly = async (req, res) => {
try {
const { chatId, adminsOnly } = req.body
const client = sessions.get(req.params.sessionId)
const chat = await client.getChatById(chatId)
if (!chat.isGroup) { throw new Error('The chat is not a group') }
const result = await chat.setInfoAdminsOnly(adminsOnly)
res.json({ success: true, result })
} catch (error) {
sendErrorResponse(res, 500, error.message)
}
}
/**
* Sets admins-only status of a group chat's messages.
*
* @async
* @function setMessagesAdminsOnly
* @param {Object} req - Request object.
* @param {Object} res - Response object.
* @param {string} req.params.sessionId - ID of the user's session.
* @param {Object} req.body - Request body.
* @param {string} req.body.chatId - ID of the group chat.
* @param {boolean} req.body.adminsOnly - Desired admins-only status.
* @returns {Promise<void>} Promise representing the success or failure of the operation.
* @throws {Error} If the chat is not a group.
*/
const setMessagesAdminsOnly = async (req, res) => {
try {
const { chatId, adminsOnly } = req.body
const client = sessions.get(req.params.sessionId)
const chat = await client.getChatById(chatId)
if (!chat.isGroup) { throw new Error('The chat is not a group') }
const result = await chat.setMessagesAdminsOnly(adminsOnly)
res.json({ success: true, result })
} catch (error) {
sendErrorResponse(res, 500, error.message)
}
}
/**
* Set the group Picture
* @param {Object} req - The request object.
* @param {Object} res - The response object.
* @param {Object} req.body.pictureMimetype - The mimetype of the image.
* @param {Object} req.body.pictureData - The new group picture in base64 format.
* @param {Object} req.body.chatId - ID of the group chat.
* @param {string} req.params.sessionId - The ID of the session for the user.
* @returns {Object} Returns a JSON object with a success status and the result of the function.
* @throws {Error} If there is an issue setting the group picture, an error will be thrown.
*/
const setPicture = async (req, res) => {
try {
const { pictureMimetype, pictureData, chatId } = req.body
const client = sessions.get(req.params.sessionId)
const media = new MessageMedia(pictureMimetype, pictureData)
const chat = await client.getChatById(chatId)
if (!chat.isGroup) { throw new Error('The chat is not a group') }
const result = await chat.setPicture(media)
res.json({ success: true, result })
} catch (error) {
sendErrorResponse(res, 500, error.message)
}
}
/**
* Delete the group Picture
* @param {Object} req - The request object.
* @param {Object} res - The response object.
* @param {Object} req.body.chatId - ID of the group chat.
* @param {string} req.params.sessionId - The ID of the session for the user.
* @returns {Object} Returns a JSON object with a success status and the result of the function.
* @throws {Error} If there is an issue setting the group picture, an error will be thrown.
*/
const deletePicture = async (req, res) => {
try {
const { chatId } = req.body
const client = sessions.get(req.params.sessionId)
const chat = await client.getChatById(chatId)
if (!chat.isGroup) { throw new Error('The chat is not a group') }
const result = await chat.deletePicture()
res.json({ success: true, result })
} catch (error) {
sendErrorResponse(res, 500, error.message)
}
}
module.exports = {
getClassInfo,
addParticipants,
demoteParticipants,
getInviteCode,
leave,
promoteParticipants,
removeParticipants,
revokeInvite,
setDescription,
setInfoAdminsOnly,
setMessagesAdminsOnly,
setSubject,
setPicture,
deletePicture
}

View file

@ -0,0 +1,55 @@
const fsp = require('fs').promises
const qrcode = require('qrcode-terminal')
const { sessionFolderPath } = require('../config')
const { sendErrorResponse } = require('../utils')
/**
* Responds to ping request with 'pong'
*
* @function ping
* @async
* @param {Object} req - Express request object
* @param {Object} res - Express response object
* @returns {Promise<void>} - Promise that resolves once response is sent
* @throws {Object} - Throws error if response fails
*/
const ping = async (req, res) => {
/*
#swagger.tags = ['Various']
*/
try {
res.json({ success: true, message: 'pong' })
} catch (error) {
sendErrorResponse(res, 500, error.message)
}
}
/**
* Example local callback function that generates a QR code and writes a log file
*
* @function localCallbackExample
* @async
* @param {Object} req - Express request object containing a body object with dataType and data
* @param {string} req.body.dataType - Type of data (in this case, 'qr')
* @param {Object} req.body.data - Data to generate a QR code from
* @param {Object} res - Express response object
* @returns {Promise<void>} - Promise that resolves once response is sent
* @throws {Object} - Throws error if response fails
*/
const localCallbackExample = async (req, res) => {
/*
#swagger.tags = ['Various']
*/
try {
const { dataType, data } = req.body
if (dataType === 'qr') { qrcode.generate(data.qr, { small: true }) }
await fsp.writeFile(`${sessionFolderPath}/message_log.txt`, `${JSON.stringify(req.body)}\r\n`, { flag: 'a+' }, _ => _)
res.json({ success: true })
} catch (error) {
console.log(error)
await fsp.writeFile(`${sessionFolderPath}/message_log.txt`, `(ERROR) ${JSON.stringify(error)}\r\n`, { flag: 'a+' }, _ => _)
sendErrorResponse(res, 500, error.message)
}
}
module.exports = { ping, localCallbackExample }

View file

@ -0,0 +1,376 @@
const { sessions } = require('../sessions')
const { sendErrorResponse } = require('../utils')
/**
* Get message by its ID from a given chat using the provided client.
* @async
* @function
* @param {object} client - The chat client.
* @param {string} messageId - The ID of the message to get.
* @param {string} chatId - The ID of the chat to search in.
* @returns {Promise<object>} - A Promise that resolves with the message object that matches the provided ID, or undefined if no such message exists.
* @throws {Error} - Throws an error if the provided client, message ID or chat ID is invalid.
*/
const _getMessageById = async (client, messageId, chatId) => {
const chat = await client.getChatById(chatId)
const messages = await chat.fetchMessages({ limit: 100 })
const message = messages.find((message) => { return message.id.id === messageId })
return message
}
/**
* Gets information about a message's class.
* @async
* @function
* @param {Object} req - The request object.
* @param {Object} res - The response object.
* @param {string} req.params.sessionId - The session ID.
* @param {string} req.body.messageId - The message ID.
* @param {string} req.body.chatId - The chat ID.
* @returns {Promise<void>} - A Promise that resolves with no value when the function completes.
*/
const getClassInfo = async (req, res) => {
try {
const { messageId, chatId } = req.body
const client = sessions.get(req.params.sessionId)
const message = await _getMessageById(client, messageId, chatId)
if (!message) { throw new Error('Message not Found') }
res.json({ success: true, message })
} catch (error) {
sendErrorResponse(res, 500, error.message)
}
}
/**
* Deletes a message.
* @async
* @function
* @param {Object} req - The request object.
* @param {Object} res - The response object.
* @param {string} req.params.sessionId - The session ID.
* @param {string} req.body.messageId - The message ID.
* @param {string} req.body.chatId - The chat ID.
* @param {boolean} req.body.everyone - Whether to delete the message for everyone or just the sender.
* @returns {Promise<void>} - A Promise that resolves with no value when the function completes.
*/
const deleteMessage = async (req, res) => {
try {
const { messageId, chatId, everyone } = req.body
const client = sessions.get(req.params.sessionId)
const message = await _getMessageById(client, messageId, chatId)
if (!message) { throw new Error('Message not Found') }
const result = await message.delete(everyone)
res.json({ success: true, result })
} catch (error) {
sendErrorResponse(res, 500, error.message)
}
}
/**
* Downloads media from a message.
* @async
* @function
* @param {Object} req - The request object.
* @param {Object} res - The response object.
* @param {string} req.params.sessionId - The session ID.
* @param {string} req.body.messageId - The message ID.
* @param {string} req.body.chatId - The chat ID.
* @param {boolean} req.body.everyone - Whether to download the media for everyone or just the sender.
* @returns {Promise<void>} - A Promise that resolves with no value when the function completes.
*/
const downloadMedia = async (req, res) => {
try {
const { messageId, chatId, everyone } = req.body
const client = sessions.get(req.params.sessionId)
const message = await _getMessageById(client, messageId, chatId)
if (!message) { throw new Error('Message not Found') }
const messageMedia = await message.downloadMedia(everyone)
res.json({ success: true, messageMedia })
} catch (error) {
sendErrorResponse(res, 500, error.message)
}
}
/**
* Forwards a message to a destination chat.
* @async
* @function forward
* @param {Object} req - The request object received by the server.
* @param {Object} req.body - The body of the request object.
* @param {string} req.body.messageId - The ID of the message to forward.
* @param {string} req.body.chatId - The ID of the chat that contains the message to forward.
* @param {string} req.body.destinationChatId - The ID of the chat to forward the message to.
* @param {string} req.params.sessionId - The ID of the session to use the Telegram API with.
* @param {Object} res - The response object to be sent back to the client.
* @returns {Object} - The response object with a JSON body containing the result of the forward operation.
* @throws Will throw an error if the message is not found or if there is an error during the forward operation.
*/
const forward = async (req, res) => {
try {
const { messageId, chatId, destinationChatId } = req.body
const client = sessions.get(req.params.sessionId)
const message = await _getMessageById(client, messageId, chatId)
if (!message) { throw new Error('Message not Found') }
const result = await message.forward(destinationChatId)
res.json({ success: true, result })
} catch (error) {
sendErrorResponse(res, 500, error.message)
}
}
/**
* Gets information about a message.
* @async
* @function getInfo
* @param {Object} req - The request object received by the server.
* @param {Object} req.body - The body of the request object.
* @param {string} req.body.messageId - The ID of the message to get information about.
* @param {string} req.body.chatId - The ID of the chat that contains the message to get information about.
* @param {string} req.params.sessionId - The ID of the session to use the Telegram API with.
* @param {Object} res - The response object to be sent back to the client.
* @returns {Object} - The response object with a JSON body containing the information about the message.
* @throws Will throw an error if the message is not found or if there is an error during the get info operation.
*/
const getInfo = async (req, res) => {
try {
const { messageId, chatId } = req.body
const client = sessions.get(req.params.sessionId)
const message = await _getMessageById(client, messageId, chatId)
if (!message) { throw new Error('Message not Found') }
const info = await message.getInfo()
res.json({ success: true, info })
} catch (error) {
sendErrorResponse(res, 500, error.message)
}
}
/**
* Retrieves a list of contacts mentioned in a specific message
*
* @async
* @function
* @param {Object} req - The HTTP request object
* @param {Object} req.body - The request body
* @param {string} req.body.messageId - The ID of the message to retrieve mentions from
* @param {string} req.body.chatId - The ID of the chat where the message was sent
* @param {string} req.params.sessionId - The ID of the session for the client making the request
* @param {Object} res - The HTTP response object
* @returns {Promise<void>} - The JSON response with the list of contacts
* @throws {Error} - If there's an error retrieving the message or mentions
*/
const getMentions = async (req, res) => {
try {
const { messageId, chatId } = req.body
const client = sessions.get(req.params.sessionId)
const message = await _getMessageById(client, messageId, chatId)
if (!message) { throw new Error('Message not Found') }
const contacts = await message.getMentions()
res.json({ success: true, contacts })
} catch (error) {
sendErrorResponse(res, 500, error.message)
}
}
/**
* Retrieves the order information contained in a specific message
*
* @async
* @function
* @param {Object} req - The HTTP request object
* @param {Object} req.body - The request body
* @param {string} req.body.messageId - The ID of the message to retrieve the order from
* @param {string} req.body.chatId - The ID of the chat where the message was sent
* @param {string} req.params.sessionId - The ID of the session for the client making the request
* @param {Object} res - The HTTP response object
* @returns {Promise<void>} - The JSON response with the order information
* @throws {Error} - If there's an error retrieving the message or order information
*/
const getOrder = async (req, res) => {
try {
const { messageId, chatId } = req.body
const client = sessions.get(req.params.sessionId)
const message = await _getMessageById(client, messageId, chatId)
if (!message) { throw new Error('Message not Found') }
const order = await message.getOrder()
res.json({ success: true, order })
} catch (error) {
sendErrorResponse(res, 500, error.message)
}
}
/**
* Retrieves the payment information from a specific message identified by its ID.
*
* @async
* @function getPayment
* @param {Object} req - The HTTP request object.
* @param {Object} res - The HTTP response object.
* @param {string} req.params.sessionId - The session ID associated with the client making the request.
* @param {Object} req.body - The message ID and chat ID associated with the message to retrieve payment information from.
* @param {string} req.body.messageId - The ID of the message to retrieve payment information from.
* @param {string} req.body.chatId - The ID of the chat the message is associated with.
* @returns {Object} An object containing a success status and the payment information for the specified message.
* @throws {Object} If the specified message is not found or if an error occurs during the retrieval process.
*/
const getPayment = async (req, res) => {
try {
const { messageId, chatId } = req.body
const client = sessions.get(req.params.sessionId)
const message = await _getMessageById(client, messageId, chatId)
if (!message) { throw new Error('Message not Found') }
const payment = await message.getPayment()
res.json({ success: true, payment })
} catch (error) {
sendErrorResponse(res, 500, error.message)
}
}
/**
* Retrieves the quoted message information from a specific message identified by its ID.
*
* @async
* @function getQuotedMessage
* @param {Object} req - The HTTP request object.
* @param {Object} res - The HTTP response object.
* @param {string} req.params.sessionId - The session ID associated with the client making the request.
* @param {Object} req.body - The message ID and chat ID associated with the message to retrieve quoted message information from.
* @param {string} req.body.messageId - The ID of the message to retrieve quoted message information from.
* @param {string} req.body.chatId - The ID of the chat the message is associated with.
* @returns {Object} An object containing a success status and the quoted message information for the specified message.
* @throws {Object} If the specified message is not found or if an error occurs during the retrieval process.
*/
const getQuotedMessage = async (req, res) => {
try {
const { messageId, chatId } = req.body
const client = sessions.get(req.params.sessionId)
const message = await _getMessageById(client, messageId, chatId)
if (!message) { throw new Error('Message not Found') }
const quotedMessage = await message.getQuotedMessage()
res.json({ success: true, quotedMessage })
} catch (error) {
sendErrorResponse(res, 500, error.message)
}
}
/**
* React to a specific message in a chat
*
* @async
* @function react
* @param {Object} req - The HTTP request object containing the request parameters and body.
* @param {Object} res - The HTTP response object to send the result.
* @param {string} req.params.sessionId - The ID of the session to use.
* @param {Object} req.body - The body of the request.
* @param {string} req.body.messageId - The ID of the message to react to.
* @param {string} req.body.chatId - The ID of the chat the message is in.
* @param {string} req.body.reaction - The reaction to add to the message.
* @returns {Object} The HTTP response containing the result of the operation.
* @throws {Error} If there was an error during the operation.
*/
const react = async (req, res) => {
try {
const { messageId, chatId, reaction } = req.body
const client = sessions.get(req.params.sessionId)
const message = await _getMessageById(client, messageId, chatId)
if (!message) { throw new Error('Message not Found') }
const result = await message.react(reaction)
res.json({ success: true, result })
} catch (error) {
sendErrorResponse(res, 500, error.message)
}
}
/**
* Reply to a specific message in a chat
*
* @async
* @function reply
* @param {Object} req - The HTTP request object containing the request parameters and body.
* @param {Object} res - The HTTP response object to send the result.
* @param {string} req.params.sessionId - The ID of the session to use.
* @param {string} req.body.messageId - The ID of the message to reply to.
* @param {string} req.body.chatId - The ID of the chat the message is in.
* @param {string} req.body.content - The content of the message to send.
* @param {string} req.body.destinationChatId - The ID of the chat to send the reply to.
* @param {Object} req.body.options - Additional options for sending the message.
* @returns {Object} The HTTP response containing the result of the operation.
* @throws {Error} If there was an error during the operation.
*/
const reply = async (req, res) => {
try {
const { messageId, chatId, content, destinationChatId, options } = req.body
const client = sessions.get(req.params.sessionId)
const message = await _getMessageById(client, messageId, chatId)
if (!message) { throw new Error('Message not Found') }
const repliedMessage = await message.reply(content, destinationChatId, options)
res.json({ success: true, repliedMessage })
} catch (error) {
sendErrorResponse(res, 500, error.message)
}
}
/**
* @function star
* @async
* @description Stars a message by message ID and chat ID.
* @param {Object} req - The request object.
* @param {Object} res - The response object.
* @param {string} req.params.sessionId - The session ID.
* @param {string} req.body.messageId - The message ID.
* @param {string} req.body.chatId - The chat ID.
* @returns {Promise} A Promise that resolves with the result of the message.star() call.
* @throws {Error} If message is not found, it throws an error with the message "Message not Found".
*/
const star = async (req, res) => {
try {
const { messageId, chatId } = req.body
const client = sessions.get(req.params.sessionId)
const message = await _getMessageById(client, messageId, chatId)
if (!message) { throw new Error('Message not Found') }
const result = await message.star()
res.json({ success: true, result })
} catch (error) {
sendErrorResponse(res, 500, error.message)
}
}
/**
* @function unstar
* @async
* @description Unstars a message by message ID and chat ID.
* @param {Object} req - The request object.
* @param {Object} res - The response object.
* @param {string} req.params.sessionId - The session ID.
* @param {string} req.body.messageId - The message ID.
* @param {string} req.body.chatId - The chat ID.
* @returns {Promise} A Promise that resolves with the result of the message.unstar() call.
* @throws {Error} If message is not found, it throws an error with the message "Message not Found".
*/
const unstar = async (req, res) => {
try {
const { messageId, chatId } = req.body
const client = sessions.get(req.params.sessionId)
const message = await _getMessageById(client, messageId, chatId)
if (!message) { throw new Error('Message not Found') }
const result = await message.unstar()
res.json({ success: true, result })
} catch (error) {
sendErrorResponse(res, 500, error.message)
}
}
module.exports = {
getClassInfo,
deleteMessage,
downloadMedia,
forward,
getInfo,
getMentions,
getOrder,
getPayment,
getQuotedMessage,
react,
reply,
star,
unstar
}

View file

@ -0,0 +1,374 @@
const qr = require('qr-image')
const { setupSession, deleteSession, reloadSession, validateSession, flushSessions, sessions } = require('../sessions')
const { sendErrorResponse, waitForNestedObject } = require('../utils')
/**
* Starts a session for the given session ID.
*
* @function
* @async
* @param {Object} req - The HTTP request object.
* @param {Object} res - The HTTP response object.
* @param {string} req.params.sessionId - The session ID to start.
* @returns {Promise<void>}
* @throws {Error} If there was an error starting the session.
*/
const startSession = async (req, res) => {
// #swagger.summary = 'Start new session'
// #swagger.description = 'Starts a session for the given session ID.'
try {
const sessionId = req.params.sessionId
const setupSessionReturn = setupSession(sessionId)
if (!setupSessionReturn.success) {
/* #swagger.responses[422] = {
description: "Unprocessable Entity.",
content: {
"application/json": {
schema: { "$ref": "#/definitions/ErrorResponse" }
}
}
}
*/
sendErrorResponse(res, 422, setupSessionReturn.message)
return
}
/* #swagger.responses[200] = {
description: "Status of the initiated session.",
content: {
"application/json": {
schema: { "$ref": "#/definitions/StartSessionResponse" }
}
}
}
*/
// wait until the client is created
await waitForNestedObject(setupSessionReturn.client, 'pupPage')
res.json({ success: true, message: setupSessionReturn.message })
} catch (error) {
/* #swagger.responses[500] = {
description: "Server Failure.",
content: {
"application/json": {
schema: { "$ref": "#/definitions/ErrorResponse" }
}
}
}
*/
console.log('startSession ERROR', error)
sendErrorResponse(res, 500, error.message)
}
}
/**
* Status of the session with the given session ID.
*
* @function
* @async
* @param {Object} req - The HTTP request object.
* @param {Object} res - The HTTP response object.
* @param {string} req.params.sessionId - The session ID to start.
* @returns {Promise<void>}
* @throws {Error} If there was an error getting status of the session.
*/
const statusSession = async (req, res) => {
// #swagger.summary = 'Get session status'
// #swagger.description = 'Status of the session with the given session ID.'
try {
const sessionId = req.params.sessionId
const sessionData = await validateSession(sessionId)
/* #swagger.responses[200] = {
description: "Status of the session.",
content: {
"application/json": {
schema: { "$ref": "#/definitions/StatusSessionResponse" }
}
}
}
*/
res.json(sessionData)
} catch (error) {
console.log('statusSession ERROR', error)
/* #swagger.responses[500] = {
description: "Server Failure.",
content: {
"application/json": {
schema: { "$ref": "#/definitions/ErrorResponse" }
}
}
}
*/
sendErrorResponse(res, 500, error.message)
}
}
/**
* QR code of the session with the given session ID.
*
* @function
* @async
* @param {Object} req - The HTTP request object.
* @param {Object} res - The HTTP response object.
* @param {string} req.params.sessionId - The session ID to start.
* @returns {Promise<void>}
* @throws {Error} If there was an error getting status of the session.
*/
const sessionQrCode = async (req, res) => {
// #swagger.summary = 'Get session QR code'
// #swagger.description = 'QR code of the session with the given session ID.'
try {
const sessionId = req.params.sessionId
const session = sessions.get(sessionId)
if (!session) {
return res.json({ success: false, message: 'session_not_found' })
}
if (session.qr) {
return res.json({ success: true, qr: session.qr })
}
return res.json({ success: false, message: 'qr code not ready or already scanned' })
} catch (error) {
console.log('sessionQrCode ERROR', error)
/* #swagger.responses[500] = {
description: "Server Failure.",
content: {
"application/json": {
schema: { "$ref": "#/definitions/ErrorResponse" }
}
}
}
*/
sendErrorResponse(res, 500, error.message)
}
}
/**
* QR code as image of the session with the given session ID.
*
* @function
* @async
* @param {Object} req - The HTTP request object.
* @param {Object} res - The HTTP response object.
* @param {string} req.params.sessionId - The session ID to start.
* @returns {Promise<void>}
* @throws {Error} If there was an error getting status of the session.
*/
const sessionQrCodeImage = async (req, res) => {
// #swagger.summary = 'Get session QR code as image'
// #swagger.description = 'QR code as image of the session with the given session ID.'
try {
const sessionId = req.params.sessionId
const session = sessions.get(sessionId)
if (!session) {
return res.json({ success: false, message: 'session_not_found' })
}
if (session.qr) {
const qrImage = qr.image(session.qr)
/* #swagger.responses[200] = {
description: "QR image.",
content: {
"image/png": {}
}
}
*/
res.writeHead(200, {
'Content-Type': 'image/png'
})
return qrImage.pipe(res)
}
return res.json({ success: false, message: 'qr code not ready or already scanned' })
} catch (error) {
console.log('sessionQrCodeImage ERROR', error)
/* #swagger.responses[500] = {
description: "Server Failure.",
content: {
"application/json": {
schema: { "$ref": "#/definitions/ErrorResponse" }
}
}
}
*/
sendErrorResponse(res, 500, error.message)
}
}
/**
* Restarts the session with the given session ID.
*
* @function
* @async
* @param {Object} req - The HTTP request object.
* @param {Object} res - The HTTP response object.
* @param {string} req.params.sessionId - The session ID to terminate.
* @returns {Promise<void>}
* @throws {Error} If there was an error terminating the session.
*/
const restartSession = async (req, res) => {
// #swagger.summary = 'Restart session'
// #swagger.description = 'Restarts the session with the given session ID.'
try {
const sessionId = req.params.sessionId
const validation = await validateSession(sessionId)
if (validation.message === 'session_not_found') {
return res.json(validation)
}
await reloadSession(sessionId)
/* #swagger.responses[200] = {
description: "Sessions restarted.",
content: {
"application/json": {
schema: { "$ref": "#/definitions/RestartSessionResponse" }
}
}
}
*/
res.json({ success: true, message: 'Restarted successfully' })
} catch (error) {
/* #swagger.responses[500] = {
description: "Server Failure.",
content: {
"application/json": {
schema: { "$ref": "#/definitions/ErrorResponse" }
}
}
}
*/
console.log('restartSession ERROR', error)
sendErrorResponse(res, 500, error.message)
}
}
/**
* Terminates the session with the given session ID.
*
* @function
* @async
* @param {Object} req - The HTTP request object.
* @param {Object} res - The HTTP response object.
* @param {string} req.params.sessionId - The session ID to terminate.
* @returns {Promise<void>}
* @throws {Error} If there was an error terminating the session.
*/
const terminateSession = async (req, res) => {
// #swagger.summary = 'Terminate session'
// #swagger.description = 'Terminates the session with the given session ID.'
try {
const sessionId = req.params.sessionId
const validation = await validateSession(sessionId)
if (validation.message === 'session_not_found') {
return res.json(validation)
}
await deleteSession(sessionId, validation)
/* #swagger.responses[200] = {
description: "Sessions terminated.",
content: {
"application/json": {
schema: { "$ref": "#/definitions/TerminateSessionResponse" }
}
}
}
*/
res.json({ success: true, message: 'Logged out successfully' })
} catch (error) {
/* #swagger.responses[500] = {
description: "Server Failure.",
content: {
"application/json": {
schema: { "$ref": "#/definitions/ErrorResponse" }
}
}
}
*/
console.log('terminateSession ERROR', error)
sendErrorResponse(res, 500, error.message)
}
}
/**
* Terminates all inactive sessions.
*
* @function
* @async
* @param {Object} req - The HTTP request object.
* @param {Object} res - The HTTP response object.
* @returns {Promise<void>}
* @throws {Error} If there was an error terminating the sessions.
*/
const terminateInactiveSessions = async (req, res) => {
// #swagger.summary = 'Terminate inactive sessions'
// #swagger.description = 'Terminates all inactive sessions.'
try {
await flushSessions(true)
/* #swagger.responses[200] = {
description: "Sessions terminated.",
content: {
"application/json": {
schema: { "$ref": "#/definitions/TerminateSessionsResponse" }
}
}
}
*/
res.json({ success: true, message: 'Flush completed successfully' })
} catch (error) {
/* #swagger.responses[500] = {
description: "Server Failure.",
content: {
"application/json": {
schema: { "$ref": "#/definitions/ErrorResponse" }
}
}
}
*/
console.log('terminateInactiveSessions ERROR', error)
sendErrorResponse(res, 500, error.message)
}
}
/**
* Terminates all sessions.
*
* @function
* @async
* @param {Object} req - The HTTP request object.
* @param {Object} res - The HTTP response object.
* @returns {Promise<void>}
* @throws {Error} If there was an error terminating the sessions.
*/
const terminateAllSessions = async (req, res) => {
// #swagger.summary = 'Terminate all sessions'
// #swagger.description = 'Terminates all sessions.'
try {
await flushSessions(false)
/* #swagger.responses[200] = {
description: "Sessions terminated.",
content: {
"application/json": {
schema: { "$ref": "#/definitions/TerminateSessionsResponse" }
}
}
}
*/
res.json({ success: true, message: 'Flush completed successfully' })
} catch (error) {
/* #swagger.responses[500] = {
description: "Server Failure.",
content: {
"application/json": {
schema: { "$ref": "#/definitions/ErrorResponse" }
}
}
}
*/
console.log('terminateAllSessions ERROR', error)
sendErrorResponse(res, 500, error.message)
}
}
module.exports = {
startSession,
statusSession,
sessionQrCode,
sessionQrCodeImage,
restartSession,
terminateSession,
terminateInactiveSessions,
terminateAllSessions
}

188
src/middleware.js Normal file
View file

@ -0,0 +1,188 @@
const { globalApiKey, rateLimitMax, rateLimitWindowMs } = require('./config')
const { sendErrorResponse } = require('./utils')
const { validateSession } = require('./sessions')
const rateLimiting = require('express-rate-limit')
const apikey = async (req, res, next) => {
/*
#swagger.security = [{
"apiKeyAuth": []
}]
*/
/* #swagger.responses[403] = {
description: "Forbidden.",
content: {
"application/json": {
schema: { "$ref": "#/definitions/ForbiddenResponse" }
}
}
}
*/
if (globalApiKey) {
const apiKey = req.headers['x-api-key']
if (!apiKey || apiKey !== globalApiKey) {
return sendErrorResponse(res, 403, 'Invalid API key')
}
}
next()
}
const sessionNameValidation = async (req, res, next) => {
/*
#swagger.parameters['sessionId'] = {
in: 'path',
description: 'Unique identifier for the session (alphanumeric and - allowed)',
required: true,
type: 'string',
example: 'f8377d8d-a589-4242-9ba6-9486a04ef80c'
}
*/
if ((!/^[\w-]+$/.test(req.params.sessionId))) {
/* #swagger.responses[422] = {
description: "Unprocessable Entity.",
content: {
"application/json": {
schema: { "$ref": "#/definitions/ErrorResponse" }
}
}
}
*/
return sendErrorResponse(res, 422, 'Session should be alphanumerical or -')
}
next()
}
const sessionValidation = async (req, res, next) => {
const validation = await validateSession(req.params.sessionId)
if (validation.success !== true) {
/* #swagger.responses[404] = {
description: "Not Found.",
content: {
"application/json": {
schema: { "$ref": "#/definitions/NotFoundResponse" }
}
}
}
*/
return sendErrorResponse(res, 404, validation.message)
}
next()
}
const rateLimiter = rateLimiting({
limit: rateLimitMax,
windowMs: rateLimitWindowMs,
message: "You can't make any more requests at the moment. Try again later"
})
const sessionSwagger = async (req, res, next) => {
/*
#swagger.tags = ['Session']
*/
next()
}
const clientSwagger = async (req, res, next) => {
/*
#swagger.tags = ['Client']
*/
next()
}
const contactSwagger = async (req, res, next) => {
/*
#swagger.tags = ['Contact']
#swagger.requestBody = {
required: true,
schema: {
type: 'object',
properties: {
contactId: {
type: 'string',
description: 'Unique whatsApp identifier for the contact',
example: '6281288888888@c.us'
}
}
}
}
*/
next()
}
const messageSwagger = async (req, res, next) => {
/*
#swagger.tags = ['Message']
#swagger.requestBody = {
required: true,
schema: {
type: 'object',
properties: {
chatId: {
type: 'string',
description: 'The Chat id which contains the message',
example: '6281288888888@c.us'
},
messageId: {
type: 'string',
description: 'Unique whatsApp identifier for the message',
example: 'ABCDEF999999999'
}
}
}
}
*/
next()
}
const chatSwagger = async (req, res, next) => {
/*
#swagger.tags = ['Chat']
#swagger.requestBody = {
required: true,
schema: {
type: 'object',
properties: {
chatId: {
type: 'string',
description: 'Unique whatsApp identifier for the given Chat (either group or personnal)',
example: '6281288888888@c.us'
}
}
}
}
*/
next()
}
const groupChatSwagger = async (req, res, next) => {
/*
#swagger.tags = ['Group Chat']
#swagger.requestBody = {
required: true,
schema: {
type: 'object',
properties: {
chatId: {
type: 'string',
description: 'Unique whatsApp identifier for the given Chat (either group or personnal)',
example: '6281288888888@c.us'
}
}
}
}
*/
next()
}
module.exports = {
sessionValidation,
apikey,
sessionNameValidation,
sessionSwagger,
clientSwagger,
contactSwagger,
messageSwagger,
chatSwagger,
groupChatSwagger,
rateLimiter
}

191
src/routes.js Normal file
View file

@ -0,0 +1,191 @@
const express = require('express')
const routes = express.Router()
const swaggerUi = require('swagger-ui-express')
const swaggerDocument = require('../swagger.json')
const { enableLocalCallbackExample, enableSwaggerEndpoint } = require('./config')
const middleware = require('./middleware')
const healthController = require('./controllers/healthController')
const sessionController = require('./controllers/sessionController')
const clientController = require('./controllers/clientController')
const chatController = require('./controllers/chatController')
const groupChatController = require('./controllers/groupChatController')
const messageController = require('./controllers/messageController')
const contactController = require('./controllers/contactController')
/**
* ================
* HEALTH ENDPOINTS
* ================
*/
// API endpoint to check if server is alive
routes.get('/ping', healthController.ping)
// API basic callback
if (enableLocalCallbackExample) {
routes.post('/localCallbackExample', [middleware.apikey, middleware.rateLimiter], healthController.localCallbackExample)
}
/**
* ================
* SESSION ENDPOINTS
* ================
*/
const sessionRouter = express.Router()
sessionRouter.use(middleware.apikey)
sessionRouter.use(middleware.sessionSwagger)
routes.use('/session', sessionRouter)
sessionRouter.get('/start/:sessionId', middleware.sessionNameValidation, sessionController.startSession)
sessionRouter.get('/status/:sessionId', middleware.sessionNameValidation, sessionController.statusSession)
sessionRouter.get('/qr/:sessionId', middleware.sessionNameValidation, sessionController.sessionQrCode)
sessionRouter.get('/qr/:sessionId/image', middleware.sessionNameValidation, sessionController.sessionQrCodeImage)
sessionRouter.get('/restart/:sessionId', middleware.sessionNameValidation, sessionController.restartSession)
sessionRouter.get('/terminate/:sessionId', middleware.sessionNameValidation, sessionController.terminateSession)
sessionRouter.get('/terminateInactive', sessionController.terminateInactiveSessions)
sessionRouter.get('/terminateAll', sessionController.terminateAllSessions)
/**
* ================
* CLIENT ENDPOINTS
* ================
*/
const clientRouter = express.Router()
clientRouter.use(middleware.apikey)
sessionRouter.use(middleware.clientSwagger)
routes.use('/client', clientRouter)
clientRouter.get('/getClassInfo/:sessionId', [middleware.sessionNameValidation, middleware.sessionValidation], clientController.getClassInfo)
clientRouter.post('/acceptInvite/:sessionId', [middleware.sessionNameValidation, middleware.sessionValidation], clientController.acceptInvite)
clientRouter.post('/archiveChat/:sessionId', [middleware.sessionNameValidation, middleware.sessionValidation], clientController.archiveChat)
clientRouter.post('/createGroup/:sessionId', [middleware.sessionNameValidation, middleware.sessionValidation], clientController.createGroup)
clientRouter.post('/getBlockedContacts/:sessionId', [middleware.sessionNameValidation, middleware.sessionValidation], clientController.getBlockedContacts)
clientRouter.post('/getChatById/:sessionId', [middleware.sessionNameValidation, middleware.sessionValidation], clientController.getChatById)
clientRouter.post('/getChatLabels/:sessionId', [middleware.sessionNameValidation, middleware.sessionValidation], clientController.getChatLabels)
clientRouter.get('/getChats/:sessionId', [middleware.sessionNameValidation, middleware.sessionValidation], clientController.getChats)
clientRouter.post('/getChatsByLabelId/:sessionId', [middleware.sessionNameValidation, middleware.sessionValidation], clientController.getChatsByLabelId)
clientRouter.post('/getCommonGroups/:sessionId', [middleware.sessionNameValidation, middleware.sessionValidation], clientController.getCommonGroups)
clientRouter.post('/getContactById/:sessionId', [middleware.sessionNameValidation, middleware.sessionValidation], clientController.getContactById)
clientRouter.get('/getContacts/:sessionId', [middleware.sessionNameValidation, middleware.sessionValidation], clientController.getContacts)
clientRouter.post('/getInviteInfo/:sessionId', [middleware.sessionNameValidation, middleware.sessionValidation], clientController.getInviteInfo)
clientRouter.post('/getLabelById/:sessionId', [middleware.sessionNameValidation, middleware.sessionValidation], clientController.getLabelById)
clientRouter.post('/getLabels/:sessionId', [middleware.sessionNameValidation, middleware.sessionValidation], clientController.getLabels)
clientRouter.post('/addOrRemoveLabels/:sessionId', [middleware.sessionNameValidation, middleware.sessionValidation], clientController.addOrRemoveLabels)
clientRouter.post('/getNumberId/:sessionId', [middleware.sessionNameValidation, middleware.sessionValidation], clientController.getNumberId)
clientRouter.post('/isRegisteredUser/:sessionId', [middleware.sessionNameValidation, middleware.sessionValidation], clientController.isRegisteredUser)
clientRouter.post('/getProfilePicUrl/:sessionId', [middleware.sessionNameValidation, middleware.sessionValidation], clientController.getProfilePictureUrl)
clientRouter.get('/getState/:sessionId', [middleware.sessionNameValidation], clientController.getState)
clientRouter.post('/markChatUnread/:sessionId', [middleware.sessionNameValidation, middleware.sessionValidation], clientController.markChatUnread)
clientRouter.post('/muteChat/:sessionId', [middleware.sessionNameValidation, middleware.sessionValidation], clientController.muteChat)
clientRouter.post('/pinChat/:sessionId', [middleware.sessionNameValidation, middleware.sessionValidation], clientController.pinChat)
clientRouter.post('/searchMessages/:sessionId', [middleware.sessionNameValidation, middleware.sessionValidation], clientController.searchMessages)
clientRouter.post('/sendMessage/:sessionId', [middleware.sessionNameValidation, middleware.sessionValidation], clientController.sendMessage)
clientRouter.post('/sendPresenceAvailable/:sessionId', [middleware.sessionNameValidation, middleware.sessionValidation], clientController.sendPresenceAvailable)
clientRouter.post('/sendPresenceUnavailable/:sessionId', [middleware.sessionNameValidation, middleware.sessionValidation], clientController.sendPresenceUnavailable)
clientRouter.post('/sendSeen/:sessionId', [middleware.sessionNameValidation, middleware.sessionValidation], clientController.sendSeen)
clientRouter.post('/setDisplayName/:sessionId', [middleware.sessionNameValidation, middleware.sessionValidation], clientController.setDisplayName)
clientRouter.post('/setProfilePicture/:sessionId', [middleware.sessionNameValidation, middleware.sessionValidation], clientController.setProfilePicture)
clientRouter.post('/setStatus/:sessionId', [middleware.sessionNameValidation, middleware.sessionValidation], clientController.setStatus)
clientRouter.post('/unarchiveChat/:sessionId', [middleware.sessionNameValidation, middleware.sessionValidation], clientController.unarchiveChat)
clientRouter.post('/unmuteChat/:sessionId', [middleware.sessionNameValidation, middleware.sessionValidation], clientController.unmuteChat)
clientRouter.post('/unpinChat/:sessionId', [middleware.sessionNameValidation, middleware.sessionValidation], clientController.unpinChat)
clientRouter.get('/getWWebVersion/:sessionId', [middleware.sessionNameValidation, middleware.sessionValidation], clientController.getWWebVersion)
/**
* ================
* CHAT ENDPOINTS
* ================
*/
const chatRouter = express.Router()
chatRouter.use(middleware.apikey)
sessionRouter.use(middleware.chatSwagger)
routes.use('/chat', chatRouter)
chatRouter.post('/getClassInfo/:sessionId', [middleware.sessionNameValidation, middleware.sessionValidation], chatController.getClassInfo)
chatRouter.post('/clearMessages/:sessionId', [middleware.sessionNameValidation, middleware.sessionValidation], chatController.clearMessages)
chatRouter.post('/clearState/:sessionId', [middleware.sessionNameValidation, middleware.sessionValidation], chatController.clearState)
chatRouter.post('/delete/:sessionId', [middleware.sessionNameValidation, middleware.sessionValidation], chatController.deleteChat)
chatRouter.post('/fetchMessages/:sessionId', [middleware.sessionNameValidation, middleware.sessionValidation], chatController.fetchMessages)
chatRouter.post('/getContact/:sessionId', [middleware.sessionNameValidation, middleware.sessionValidation], chatController.getContact)
chatRouter.post('/sendStateRecording/:sessionId', [middleware.sessionNameValidation, middleware.sessionValidation], chatController.sendStateRecording)
chatRouter.post('/sendStateTyping/:sessionId', [middleware.sessionNameValidation, middleware.sessionValidation], chatController.sendStateTyping)
/**
* ================
* GROUP CHAT ENDPOINTS
* ================
*/
const groupChatRouter = express.Router()
groupChatRouter.use(middleware.apikey)
sessionRouter.use(middleware.groupChatSwagger)
routes.use('/groupChat', groupChatRouter)
groupChatRouter.post('/getClassInfo/:sessionId', [middleware.sessionNameValidation, middleware.sessionValidation], groupChatController.getClassInfo)
groupChatRouter.post('/addParticipants/:sessionId', [middleware.sessionNameValidation, middleware.sessionValidation], groupChatController.addParticipants)
groupChatRouter.post('/demoteParticipants/:sessionId', [middleware.sessionNameValidation, middleware.sessionValidation], groupChatController.demoteParticipants)
groupChatRouter.post('/getInviteCode/:sessionId', [middleware.sessionNameValidation, middleware.sessionValidation], groupChatController.getInviteCode)
groupChatRouter.post('/leave/:sessionId', [middleware.sessionNameValidation, middleware.sessionValidation], groupChatController.leave)
groupChatRouter.post('/promoteParticipants/:sessionId', [middleware.sessionNameValidation, middleware.sessionValidation], groupChatController.promoteParticipants)
groupChatRouter.post('/removeParticipants/:sessionId', [middleware.sessionNameValidation, middleware.sessionValidation], groupChatController.removeParticipants)
groupChatRouter.post('/revokeInvite/:sessionId', [middleware.sessionNameValidation, middleware.sessionValidation], groupChatController.revokeInvite)
groupChatRouter.post('/setDescription/:sessionId', [middleware.sessionNameValidation, middleware.sessionValidation], groupChatController.setDescription)
groupChatRouter.post('/setInfoAdminsOnly/:sessionId', [middleware.sessionNameValidation, middleware.sessionValidation], groupChatController.setInfoAdminsOnly)
groupChatRouter.post('/setMessagesAdminsOnly/:sessionId', [middleware.sessionNameValidation, middleware.sessionValidation], groupChatController.setMessagesAdminsOnly)
groupChatRouter.post('/setSubject/:sessionId', [middleware.sessionNameValidation, middleware.sessionValidation], groupChatController.setSubject)
groupChatRouter.post('/setPicture/:sessionId', [middleware.sessionNameValidation, middleware.sessionValidation], groupChatController.setPicture)
groupChatRouter.post('/deletePicture/:sessionId', [middleware.sessionNameValidation, middleware.sessionValidation], groupChatController.deletePicture)
/**
* ================
* MESSAGE ENDPOINTS
* ================
*/
const messageRouter = express.Router()
messageRouter.use(middleware.apikey)
sessionRouter.use(middleware.messageSwagger)
routes.use('/message', messageRouter)
messageRouter.post('/getClassInfo/:sessionId', [middleware.sessionNameValidation, middleware.sessionValidation], messageController.getClassInfo)
messageRouter.post('/delete/:sessionId', [middleware.sessionNameValidation, middleware.sessionValidation], messageController.deleteMessage)
messageRouter.post('/downloadMedia/:sessionId', [middleware.sessionNameValidation, middleware.sessionValidation], messageController.downloadMedia)
messageRouter.post('/forward/:sessionId', [middleware.sessionNameValidation, middleware.sessionValidation], messageController.forward)
messageRouter.post('/getInfo/:sessionId', [middleware.sessionNameValidation, middleware.sessionValidation], messageController.getInfo)
messageRouter.post('/getMentions/:sessionId', [middleware.sessionNameValidation, middleware.sessionValidation], messageController.getMentions)
messageRouter.post('/getOrder/:sessionId', [middleware.sessionNameValidation, middleware.sessionValidation], messageController.getOrder)
messageRouter.post('/getPayment/:sessionId', [middleware.sessionNameValidation, middleware.sessionValidation], messageController.getPayment)
messageRouter.post('/getQuotedMessage/:sessionId', [middleware.sessionNameValidation, middleware.sessionValidation], messageController.getQuotedMessage)
messageRouter.post('/react/:sessionId', [middleware.sessionNameValidation, middleware.sessionValidation], messageController.react)
messageRouter.post('/reply/:sessionId', [middleware.sessionNameValidation, middleware.sessionValidation], messageController.reply)
messageRouter.post('/star/:sessionId', [middleware.sessionNameValidation, middleware.sessionValidation], messageController.star)
messageRouter.post('/unstar/:sessionId', [middleware.sessionNameValidation, middleware.sessionValidation], messageController.unstar)
/**
* ================
* MESSAGE ENDPOINTS
* ================
*/
const contactRouter = express.Router()
contactRouter.use(middleware.apikey)
sessionRouter.use(middleware.contactSwagger)
routes.use('/contact', contactRouter)
contactRouter.post('/getClassInfo/:sessionId', [middleware.sessionNameValidation, middleware.sessionValidation], contactController.getClassInfo)
contactRouter.post('/block/:sessionId', [middleware.sessionNameValidation, middleware.sessionValidation], contactController.block)
contactRouter.post('/getAbout/:sessionId', [middleware.sessionNameValidation, middleware.sessionValidation], contactController.getAbout)
contactRouter.post('/getChat/:sessionId', [middleware.sessionNameValidation, middleware.sessionValidation], contactController.getChat)
contactRouter.post('/unblock/:sessionId', [middleware.sessionNameValidation, middleware.sessionValidation], contactController.unblock)
contactRouter.post('/getFormattedNumber/:sessionId', [middleware.sessionNameValidation, middleware.sessionValidation], contactController.getFormattedNumber)
contactRouter.post('/getCountryCode/:sessionId', [middleware.sessionNameValidation, middleware.sessionValidation], contactController.getCountryCode)
contactRouter.post('/getProfilePicUrl/:sessionId', [middleware.sessionNameValidation, middleware.sessionValidation], contactController.getProfilePicUrl)
/**
* ================
* SWAGGER ENDPOINTS
* ================
*/
if (enableSwaggerEndpoint) {
routes.use('/api-docs', swaggerUi.serve)
routes.get('/api-docs', swaggerUi.setup(swaggerDocument) /* #swagger.ignore = true */)
}
module.exports = { routes }

474
src/sessions.js Normal file
View file

@ -0,0 +1,474 @@
const { Client, LocalAuth } = require('whatsapp-web.js')
const fs = require('fs')
const path = require('path')
const sessions = new Map()
const { baseWebhookURL, sessionFolderPath, maxAttachmentSize, setMessagesAsSeen, webVersion, webVersionCacheType, recoverSessions, chromeBin, headless } = require('./config')
const { triggerWebhook, waitForNestedObject, checkIfEventisEnabled } = require('./utils')
// Function to validate if the session is ready
const validateSession = async (sessionId) => {
try {
const returnData = { success: false, state: null, message: '' }
// Session not Connected 😢
if (!sessions.has(sessionId) || !sessions.get(sessionId)) {
returnData.message = 'session_not_found'
return returnData
}
const client = sessions.get(sessionId)
// wait until the client is created
await waitForNestedObject(client, 'pupPage')
.catch((err) => { return { success: false, state: null, message: err.message } })
// Wait for client.pupPage to be evaluable
let maxRetry = 0
while (true) {
try {
if (client.pupPage.isClosed()) {
return { success: false, state: null, message: 'browser tab closed' }
}
await Promise.race([
client.pupPage.evaluate('1'),
new Promise(resolve => setTimeout(resolve, 1000))
])
break
} catch (error) {
if (maxRetry === 2) {
return { success: false, state: null, message: 'session closed' }
}
maxRetry++
}
}
const state = await client.getState()
returnData.state = state
if (state !== 'CONNECTED') {
returnData.message = 'session_not_connected'
return returnData
}
// Session Connected 🎉
returnData.success = true
returnData.message = 'session_connected'
return returnData
} catch (error) {
console.log(error)
return { success: false, state: null, message: error.message }
}
}
// Function to handle client session restoration
const restoreSessions = () => {
try {
if (!fs.existsSync(sessionFolderPath)) {
fs.mkdirSync(sessionFolderPath) // Create the session directory if it doesn't exist
}
// Read the contents of the folder
fs.readdir(sessionFolderPath, (_, files) => {
// Iterate through the files in the parent folder
for (const file of files) {
// Use regular expression to extract the string from the folder name
const match = file.match(/^session-(.+)$/)
if (match) {
const sessionId = match[1]
console.log('existing session detected', sessionId)
setupSession(sessionId)
}
}
})
} catch (error) {
console.log(error)
console.error('Failed to restore sessions:', error)
}
}
// Setup Session
const setupSession = (sessionId) => {
try {
if (sessions.has(sessionId)) {
return { success: false, message: `Session already exists for: ${sessionId}`, client: sessions.get(sessionId) }
}
// Disable the delete folder from the logout function (will be handled separately)
const localAuth = new LocalAuth({ clientId: sessionId, dataPath: sessionFolderPath })
delete localAuth.logout
localAuth.logout = () => { }
const clientOptions = {
puppeteer: {
executablePath: chromeBin,
headless,
args: ['--no-sandbox', '--disable-setuid-sandbox', '--disable-gpu', '--disable-dev-shm-usage']
},
authStrategy: localAuth
}
if (webVersion) {
clientOptions.webVersion = webVersion
switch (webVersionCacheType.toLowerCase()) {
case 'local':
clientOptions.webVersionCache = {
type: 'local'
}
break
case 'remote':
clientOptions.webVersionCache = {
type: 'remote',
remotePath: 'https://raw.githubusercontent.com/wppconnect-team/wa-version/main/html/' + webVersion + '.html'
}
break
default:
clientOptions.webVersionCache = {
type: 'none'
}
}
}
const client = new Client(clientOptions)
client.initialize().catch(err => console.log('Initialize error:', err.message))
initializeEvents(client, sessionId)
// Save the session to the Map
sessions.set(sessionId, client)
return { success: true, message: 'Session initiated successfully', client }
} catch (error) {
return { success: false, message: error.message, client: null }
}
}
const initializeEvents = (client, sessionId) => {
// check if the session webhook is overridden
const sessionWebhook = process.env[sessionId.toUpperCase() + '_WEBHOOK_URL'] || baseWebhookURL
if (recoverSessions) {
waitForNestedObject(client, 'pupPage').then(() => {
const restartSession = async (sessionId) => {
sessions.delete(sessionId)
await client.destroy().catch(e => { })
setupSession(sessionId)
}
client.pupPage.once('close', function () {
// emitted when the page closes
console.log(`Browser page closed for ${sessionId}. Restoring`)
restartSession(sessionId)
})
client.pupPage.once('error', function () {
// emitted when the page crashes
console.log(`Error occurred on browser page for ${sessionId}. Restoring`)
restartSession(sessionId)
})
}).catch(e => { })
}
checkIfEventisEnabled('auth_failure')
.then(_ => {
client.on('auth_failure', (msg) => {
triggerWebhook(sessionWebhook, sessionId, 'status', { msg })
})
})
checkIfEventisEnabled('authenticated')
.then(_ => {
client.on('authenticated', () => {
triggerWebhook(sessionWebhook, sessionId, 'authenticated')
})
})
checkIfEventisEnabled('call')
.then(_ => {
client.on('call', async (call) => {
triggerWebhook(sessionWebhook, sessionId, 'call', { call })
})
})
checkIfEventisEnabled('change_state')
.then(_ => {
client.on('change_state', state => {
triggerWebhook(sessionWebhook, sessionId, 'change_state', { state })
})
})
checkIfEventisEnabled('disconnected')
.then(_ => {
client.on('disconnected', (reason) => {
triggerWebhook(sessionWebhook, sessionId, 'disconnected', { reason })
})
})
checkIfEventisEnabled('group_join')
.then(_ => {
client.on('group_join', (notification) => {
triggerWebhook(sessionWebhook, sessionId, 'group_join', { notification })
})
})
checkIfEventisEnabled('group_leave')
.then(_ => {
client.on('group_leave', (notification) => {
triggerWebhook(sessionWebhook, sessionId, 'group_leave', { notification })
})
})
checkIfEventisEnabled('group_update')
.then(_ => {
client.on('group_update', (notification) => {
triggerWebhook(sessionWebhook, sessionId, 'group_update', { notification })
})
})
checkIfEventisEnabled('loading_screen')
.then(_ => {
client.on('loading_screen', (percent, message) => {
triggerWebhook(sessionWebhook, sessionId, 'loading_screen', { percent, message })
})
})
checkIfEventisEnabled('media_uploaded')
.then(_ => {
client.on('media_uploaded', (message) => {
triggerWebhook(sessionWebhook, sessionId, 'media_uploaded', { message })
})
})
checkIfEventisEnabled('message')
.then(_ => {
client.on('message', async (message) => {
triggerWebhook(sessionWebhook, sessionId, 'message', { message })
if (message.hasMedia && message._data?.size < maxAttachmentSize) {
// custom service event
checkIfEventisEnabled('media').then(_ => {
message.downloadMedia().then(messageMedia => {
triggerWebhook(sessionWebhook, sessionId, 'media', { messageMedia, message })
}).catch(e => {
console.log('Download media error:', e.message)
})
})
}
if (setMessagesAsSeen) {
const chat = await message.getChat()
chat.sendSeen()
}
})
})
checkIfEventisEnabled('message_ack')
.then(_ => {
client.on('message_ack', async (message, ack) => {
triggerWebhook(sessionWebhook, sessionId, 'message_ack', { message, ack })
if (setMessagesAsSeen) {
const chat = await message.getChat()
chat.sendSeen()
}
})
})
checkIfEventisEnabled('message_create')
.then(_ => {
client.on('message_create', async (message) => {
triggerWebhook(sessionWebhook, sessionId, 'message_create', { message })
if (setMessagesAsSeen) {
const chat = await message.getChat()
chat.sendSeen()
}
})
})
checkIfEventisEnabled('message_reaction')
.then(_ => {
client.on('message_reaction', (reaction) => {
triggerWebhook(sessionWebhook, sessionId, 'message_reaction', { reaction })
})
})
checkIfEventisEnabled('message_edit')
.then(_ => {
client.on('message_edit', (message, newBody, prevBody) => {
triggerWebhook(sessionWebhook, sessionId, 'message_edit', { message, newBody, prevBody })
})
})
checkIfEventisEnabled('message_ciphertext')
.then(_ => {
client.on('message_ciphertext', (message) => {
triggerWebhook(sessionWebhook, sessionId, 'message_ciphertext', { message })
})
})
checkIfEventisEnabled('message_revoke_everyone')
.then(_ => {
// eslint-disable-next-line camelcase
client.on('message_revoke_everyone', async (message) => {
// eslint-disable-next-line camelcase
triggerWebhook(sessionWebhook, sessionId, 'message_revoke_everyone', { message })
})
})
checkIfEventisEnabled('message_revoke_me')
.then(_ => {
client.on('message_revoke_me', async (message) => {
triggerWebhook(sessionWebhook, sessionId, 'message_revoke_me', { message })
})
})
client.on('qr', (qr) => {
// inject qr code into session
client.qr = qr
checkIfEventisEnabled('qr')
.then(_ => {
triggerWebhook(sessionWebhook, sessionId, 'qr', { qr })
})
})
checkIfEventisEnabled('ready')
.then(_ => {
client.on('ready', () => {
triggerWebhook(sessionWebhook, sessionId, 'ready')
})
})
checkIfEventisEnabled('contact_changed')
.then(_ => {
client.on('contact_changed', async (message, oldId, newId, isContact) => {
triggerWebhook(sessionWebhook, sessionId, 'contact_changed', { message, oldId, newId, isContact })
})
})
checkIfEventisEnabled('chat_removed')
.then(_ => {
client.on('chat_removed', async (chat) => {
triggerWebhook(sessionWebhook, sessionId, 'chat_removed', { chat })
})
})
checkIfEventisEnabled('chat_archived')
.then(_ => {
client.on('chat_archived', async (chat, currState, prevState) => {
triggerWebhook(sessionWebhook, sessionId, 'chat_archived', { chat, currState, prevState })
})
})
checkIfEventisEnabled('unread_count')
.then(_ => {
client.on('unread_count', async (chat) => {
triggerWebhook(sessionWebhook, sessionId, 'unread_count', { chat })
})
})
}
// Function to delete client session folder
const deleteSessionFolder = async (sessionId) => {
try {
const targetDirPath = path.join(sessionFolderPath, `session-${sessionId}`)
const resolvedTargetDirPath = await fs.promises.realpath(targetDirPath)
const resolvedSessionPath = await fs.promises.realpath(sessionFolderPath)
// Ensure the target directory path ends with a path separator
const safeSessionPath = `${resolvedSessionPath}${path.sep}`
// Validate the resolved target directory path is a subdirectory of the session folder path
if (!resolvedTargetDirPath.startsWith(safeSessionPath)) {
throw new Error('Invalid path: Directory traversal detected')
}
await fs.promises.rm(resolvedTargetDirPath, { recursive: true, force: true })
} catch (error) {
console.log('Folder deletion error', error)
throw error
}
}
// Function to reload client session without removing browser cache
const reloadSession = async (sessionId) => {
try {
const client = sessions.get(sessionId)
if (!client) {
return
}
client.pupPage?.removeAllListeners('close')
client.pupPage?.removeAllListeners('error')
try {
const pages = await client.pupBrowser.pages()
await Promise.all(pages.map((page) => page.close()))
await Promise.race([
client.pupBrowser.close(),
new Promise(resolve => setTimeout(resolve, 5000))
])
} catch (e) {
const childProcess = client.pupBrowser.process()
if (childProcess) {
childProcess.kill(9)
}
}
sessions.delete(sessionId)
setupSession(sessionId)
} catch (error) {
console.log(error)
throw error
}
}
const deleteSession = async (sessionId, validation) => {
try {
const client = sessions.get(sessionId)
if (!client) {
return
}
client.pupPage?.removeAllListeners('close')
client.pupPage?.removeAllListeners('error')
if (validation.success) {
// Client Connected, request logout
console.log(`Logging out session ${sessionId}`)
await client.logout()
} else if (validation.message === 'session_not_connected') {
// Client not Connected, request destroy
console.log(`Destroying session ${sessionId}`)
await client.destroy()
}
// Wait 10 secs for client.pupBrowser to be disconnected before deleting the folder
let maxDelay = 0
while (client.pupBrowser.isConnected() && (maxDelay < 10)) {
await new Promise(resolve => setTimeout(resolve, 1000))
maxDelay++
}
await deleteSessionFolder(sessionId)
sessions.delete(sessionId)
} catch (error) {
console.log(error)
throw error
}
}
// Function to handle session flush
const flushSessions = async (deleteOnlyInactive) => {
try {
// Read the contents of the sessions folder
const files = await fs.promises.readdir(sessionFolderPath)
// Iterate through the files in the parent folder
for (const file of files) {
// Use regular expression to extract the string from the folder name
const match = file.match(/^session-(.+)$/)
if (match) {
const sessionId = match[1]
const validation = await validateSession(sessionId)
if (!deleteOnlyInactive || !validation.success) {
await deleteSession(sessionId, validation)
}
}
}
} catch (error) {
console.log(error)
throw error
}
}
module.exports = {
sessions,
setupSession,
restoreSessions,
validateSession,
deleteSession,
reloadSession,
flushSessions
}

46
src/utils.js Normal file
View file

@ -0,0 +1,46 @@
const axios = require('axios')
const { globalApiKey, disabledCallbacks } = require('./config')
// Trigger webhook endpoint
const triggerWebhook = (webhookURL, sessionId, dataType, data) => {
axios.post(webhookURL, { dataType, data, sessionId }, { headers: { 'x-api-key': globalApiKey } })
.catch(error => console.error('Failed to send new message webhook:', sessionId, dataType, error.message, data || ''))
}
// Function to send a response with error status and message
const sendErrorResponse = (res, status, message) => {
res.status(status).json({ success: false, error: message })
}
// Function to wait for a specific item not to be null
const waitForNestedObject = (rootObj, nestedPath, maxWaitTime = 10000, interval = 100) => {
const start = Date.now()
return new Promise((resolve, reject) => {
const checkObject = () => {
const nestedObj = nestedPath.split('.').reduce((obj, key) => obj ? obj[key] : undefined, rootObj)
if (nestedObj) {
// Nested object exists, resolve the promise
resolve()
} else if (Date.now() - start > maxWaitTime) {
// Maximum wait time exceeded, reject the promise
console.log('Timed out waiting for nested object')
reject(new Error('Timeout waiting for nested object'))
} else {
// Nested object not yet created, continue waiting
setTimeout(checkObject, interval)
}
}
checkObject()
})
}
const checkIfEventisEnabled = (event) => {
return new Promise((resolve, reject) => { if (!disabledCallbacks.includes(event)) { resolve() } })
}
module.exports = {
triggerWebhook,
sendErrorResponse,
waitForNestedObject,
checkIfEventisEnabled
}

80
swagger.js Normal file
View file

@ -0,0 +1,80 @@
const swaggerAutogen = require('swagger-autogen')({ openapi: '3.0.0', autoBody: false })
const outputFile = './swagger.json'
const endpointsFiles = ['./src/routes.js']
const doc = {
info: {
title: 'WWebJS API',
description: 'API wrapper for WhatsAppWebJS'
},
servers: [
{
url: '',
description: ''
},
{
url: 'http://localhost:3000',
description: 'localhost'
}
],
securityDefinitions: {
apiKeyAuth: {
type: 'apiKey',
in: 'header',
name: 'x-api-key'
}
},
produces: ['application/json'],
tags: [
{
name: 'Session',
description: 'Handling multiple sessions logic, creation and deletion'
},
{
name: 'Client',
description: 'All functions related to the client'
},
{
name: 'Message',
description: 'May fail if the message is too old (Only from the last 100 Messages of the given chat)'
}
],
definitions: {
StartSessionResponse: {
success: true,
message: 'Session initiated successfully'
},
StatusSessionResponse: {
success: true,
state: 'CONNECTED',
message: 'session_connected'
},
RestartSessionResponse: {
success: true,
message: 'Restarted successfully'
},
TerminateSessionResponse: {
success: true,
message: 'Logged out successfully'
},
TerminateSessionsResponse: {
success: true,
message: 'Flush completed successfully'
},
ErrorResponse: {
success: false,
error: 'Some server error'
},
NotFoundResponse: {
success: false,
error: 'Some server error'
},
ForbiddenResponse: {
success: false,
error: 'Invalid API key'
}
}
}
swaggerAutogen(outputFile, endpointsFiles, doc)

7643
swagger.json Normal file

File diff suppressed because it is too large Load diff

143
tests/api.test.js Normal file
View file

@ -0,0 +1,143 @@
const request = require('supertest')
const fs = require('fs')
// Mock your application's environment variables
process.env.API_KEY = 'test_api_key'
process.env.SESSIONS_PATH = './sessions_test'
process.env.ENABLE_LOCAL_CALLBACK_EXAMPLE = 'TRUE'
process.env.BASE_WEBHOOK_URL = 'http://localhost:3000/localCallbackExample'
const app = require('../src/app')
jest.mock('qrcode-terminal')
let server
beforeAll(() => {
server = app.listen(3000)
})
beforeEach(async () => {
if (fs.existsSync('./sessions_test/message_log.txt')) {
fs.writeFileSync('./sessions_test/message_log.txt', '')
}
})
afterAll(() => {
server.close()
fs.rmSync('./sessions_test', { recursive: true, force: true })
})
// Define test cases
describe('API health checks', () => {
it('should return valid healthcheck', async () => {
const response = await request(app).get('/ping')
expect(response.status).toBe(200)
expect(response.body).toEqual({ message: 'pong', success: true })
})
it('should return a valid callback', async () => {
const response = await request(app).post('/localCallbackExample')
.set('x-api-key', 'test_api_key')
.send({ sessionId: '1', dataType: 'testDataType', data: 'testData' })
expect(response.status).toBe(200)
expect(response.body).toEqual({ success: true })
expect(fs.existsSync('./sessions_test/message_log.txt')).toBe(true)
expect(fs.readFileSync('./sessions_test/message_log.txt', 'utf-8')).toEqual('{"sessionId":"1","dataType":"testDataType","data":"testData"}\r\n')
})
})
describe('API Authentication Tests', () => {
it('should return 403 Forbidden for invalid API key', async () => {
const response = await request(app).get('/session/start/1')
expect(response.status).toBe(403)
expect(response.body).toEqual({ success: false, error: 'Invalid API key' })
})
it('should fail invalid sessionId', async () => {
const response = await request(app).get('/session/start/ABCD1@').set('x-api-key', 'test_api_key')
expect(response.status).toBe(422)
expect(response.body).toEqual({ success: false, error: 'Session should be alphanumerical or -' })
})
it('should setup and terminate a client session', async () => {
const response = await request(app).get('/session/start/1').set('x-api-key', 'test_api_key')
expect(response.status).toBe(200)
expect(response.body).toEqual({ success: true, message: 'Session initiated successfully' })
expect(fs.existsSync('./sessions_test/session-1')).toBe(true)
const response2 = await request(app).get('/session/terminate/1').set('x-api-key', 'test_api_key')
expect(response2.status).toBe(200)
expect(response2.body).toEqual({ success: true, message: 'Logged out successfully' })
expect(fs.existsSync('./sessions_test/session-1')).toBe(false)
}, 10000)
it('should setup and flush multiple client sessions', async () => {
const response = await request(app).get('/session/start/2').set('x-api-key', 'test_api_key')
expect(response.status).toBe(200)
expect(response.body).toEqual({ success: true, message: 'Session initiated successfully' })
expect(fs.existsSync('./sessions_test/session-2')).toBe(true)
const response2 = await request(app).get('/session/start/3').set('x-api-key', 'test_api_key')
expect(response2.status).toBe(200)
expect(response2.body).toEqual({ success: true, message: 'Session initiated successfully' })
expect(fs.existsSync('./sessions_test/session-3')).toBe(true)
const response3 = await request(app).get('/session/terminateInactive').set('x-api-key', 'test_api_key')
expect(response3.status).toBe(200)
expect(response3.body).toEqual({ success: true, message: 'Flush completed successfully' })
expect(fs.existsSync('./sessions_test/session-2')).toBe(false)
expect(fs.existsSync('./sessions_test/session-3')).toBe(false)
}, 10000)
})
describe('API Action Tests', () => {
it('should setup, create at least a QR, and terminate a client session', async () => {
const response = await request(app).get('/session/start/4').set('x-api-key', 'test_api_key')
expect(response.status).toBe(200)
expect(response.body).toEqual({ success: true, message: 'Session initiated successfully' })
expect(fs.existsSync('./sessions_test/session-4')).toBe(true)
// Wait for message_log.txt to not be empty
const result = await waitForFileNotToBeEmpty('./sessions_test/message_log.txt')
.then(() => { return true })
.catch(() => { return false })
expect(result).toBe(true)
// Verify the message content
const expectedMessage = {
dataType: 'qr',
data: expect.objectContaining({ qr: expect.any(String) }),
sessionId: '4'
}
expect(JSON.parse(fs.readFileSync('./sessions_test/message_log.txt', 'utf-8'))).toEqual(expectedMessage)
const response2 = await request(app).get('/session/terminate/4').set('x-api-key', 'test_api_key')
expect(response2.status).toBe(200)
expect(response2.body).toEqual({ success: true, message: 'Logged out successfully' })
expect(fs.existsSync('./sessions_test/session-4')).toBe(false)
}, 15000)
})
// Function to wait for a specific item to be equal a specific value
const waitForFileNotToBeEmpty = (filePath, maxWaitTime = 10000, interval = 100) => {
const start = Date.now()
return new Promise((resolve, reject) => {
const checkObject = () => {
const filecontent = fs.readFileSync(filePath, 'utf-8')
if (filecontent !== '') {
// Nested object exists, resolve the promise
resolve()
} else if (Date.now() - start > maxWaitTime) {
// Maximum wait time exceeded, reject the promise
console.log('Timed out waiting for nested object')
reject(new Error('Timeout waiting for nested object'))
} else {
// Nested object not yet created, continue waiting
setTimeout(checkObject, interval)
}
}
checkObject()
})
}