├── .github ├── FUNDING.yml └── workflows │ ├── pull_request.yml │ └── push.yml ├── assets └── basic_start.gif ├── .eslintrc.js ├── .gitignore ├── src ├── logger.js ├── app.js ├── websocket.js ├── config.js ├── controllers │ ├── healthController.js │ ├── contactController.js │ ├── sessionController.js │ ├── chatController.js │ ├── groupChatController.js │ └── messageController.js ├── utils.js ├── middleware.js ├── sessions.js └── routes.js ├── .dockerignore ├── docker-compose.yml ├── LICENSE ├── server.js ├── Dockerfile ├── LICENSE.md ├── package.json ├── CONTRIBUTING.md ├── swagger.js ├── .env.example ├── REVERSE_PROXY_SETUP.md ├── tests └── api.test.js └── README.md /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: [avoylenko] 2 | -------------------------------------------------------------------------------- /assets/basic_start.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/avoylenko/wwebjs-api/HEAD/assets/basic_start.gif -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | browser: true, 4 | es2021: true, 5 | jest: true 6 | }, 7 | extends: 'standard', 8 | overrides: [ 9 | ], 10 | parserOptions: { 11 | ecmaVersion: 'latest', 12 | sourceType: 'module' 13 | }, 14 | rules: { 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Ignore node_modules 2 | node_modules 3 | 4 | # Ignore dotenv files 5 | .env 6 | 7 | # Ignore sessions 8 | sessions 9 | sessions_test 10 | .wwebjs_cache 11 | 12 | # Ignore logs 13 | logs 14 | 15 | # Ignore test coverage reports 16 | coverage 17 | 18 | # Ignore other unnecessary files 19 | .DS_Store 20 | .vscode -------------------------------------------------------------------------------- /src/logger.js: -------------------------------------------------------------------------------- 1 | const { logLevel } = require('./config') 2 | const pino = require('pino') 3 | 4 | const logger = pino({ 5 | level: logLevel 6 | }, pino.destination(1, { sync: false })) 7 | 8 | logger.on('level-change', (lvl, val, prevLvl, prevVal) => { 9 | logger.info('%s (%d) was changed to %s (%d)', prevLvl, prevVal, lvl, val) 10 | }) 11 | 12 | module.exports = { logger } 13 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | # Ignore node_modules 2 | node_modules 3 | 4 | # Ignore dotenv files 5 | .env 6 | .env.example 7 | 8 | # Ignore logs 9 | logs 10 | 11 | # Ignore test files 12 | tests 13 | 14 | # Ignore session files 15 | sessions 16 | sessions_test 17 | 18 | # Ignore git related files 19 | .git 20 | .gitignore 21 | 22 | # Ignore other unnecessary files 23 | README.md 24 | CONTRIBUTING.md 25 | LICENSE.md 26 | Dockerfile 27 | docker-compose.yml 28 | swagger.yml 29 | .github 30 | assets -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | # The version property is now optional in modern Docker Compose 2 | # version: '3.8' 3 | 4 | name: wwebjs-api 5 | 6 | services: 7 | api: 8 | image: avoylenko/wwebjs-api:latest 9 | container_name: wwebjs-api 10 | restart: always 11 | ports: 12 | - "3000:3000" 13 | env_file: .env 14 | volumes: 15 | - ./sessions:/usr/src/app/sessions 16 | # Optional healthcheck 17 | # healthcheck: 18 | # test: ["CMD", "curl", "-f", "http://localhost:3000/health"] 19 | # interval: 30s 20 | # timeout: 10s 21 | # retries: 3 22 | # start_period: 40s 23 | -------------------------------------------------------------------------------- /src/app.js: -------------------------------------------------------------------------------- 1 | require('./routes') 2 | const express = require('express') 3 | const { routes } = require('./routes') 4 | const { maxAttachmentSize, basePath, trustProxy } = require('./config') 5 | 6 | const app = express() 7 | 8 | // Initialize Express app 9 | app.disable('x-powered-by') 10 | 11 | // Configure trust proxy for reverse proxy compatibility 12 | if (trustProxy) { 13 | app.set('trust proxy', true) 14 | } 15 | 16 | app.use(express.json({ limit: maxAttachmentSize + 1000000 })) 17 | app.use(express.urlencoded({ limit: maxAttachmentSize + 1000000, extended: true })) 18 | 19 | // Mount routes with configurable base path 20 | app.use(basePath, routes) 21 | 22 | module.exports = app 23 | -------------------------------------------------------------------------------- /.github/workflows/pull_request.yml: -------------------------------------------------------------------------------- 1 | name: CI/CD Pipeline for Pull Requests to Main 2 | 3 | on: 4 | pull_request: 5 | branches: 6 | - main 7 | jobs: 8 | test: 9 | runs-on: ubuntu-latest 10 | strategy: 11 | matrix: 12 | node-version: 13 | - 22.x 14 | steps: 15 | - name: Checkout repository 16 | uses: actions/checkout@v4 17 | - name: 'Use Node.js ${{ matrix.node-version }}' 18 | uses: actions/setup-node@v4 19 | with: 20 | node-version: '${{ matrix.node-version }}' 21 | - name: Install dependencies 22 | run: npm ci 23 | - name: Run tests 24 | run: npm test 25 | timeout-minutes: 5 -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Anton Voylenko 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /server.js: -------------------------------------------------------------------------------- 1 | const app = require('./src/app') 2 | const { servicePort, baseWebhookURL, enableWebHook, enableWebSocket, autoStartSessions } = require('./src/config') 3 | const { logger } = require('./src/logger') 4 | const { handleUpgrade } = require('./src/websocket') 5 | const { restoreSessions } = require('./src/sessions') 6 | 7 | // Check if BASE_WEBHOOK_URL environment variable is available when WebHook is enabled 8 | if (!baseWebhookURL && enableWebHook) { 9 | logger.error('BASE_WEBHOOK_URL environment variable is not set. Exiting...') 10 | process.exit(1) // Terminate the application with an error code 11 | } 12 | 13 | const server = app.listen(servicePort, () => { 14 | logger.info(`Server running on port ${servicePort}`) 15 | logger.debug({ configuration: require('./src/config') }, 'Service configuration') 16 | if (autoStartSessions) { 17 | logger.info('Starting all sessions') 18 | restoreSessions() 19 | } 20 | }) 21 | 22 | if (enableWebSocket) { 23 | server.on('upgrade', (request, socket, head) => { 24 | handleUpgrade(request, socket, head) 25 | }) 26 | } 27 | 28 | // puppeteer uses subscriptions to SIGINT, SIGTERM, and SIGHUP to know when to close browser instances 29 | // this disables the warnings when you starts more than 10 browser instances 30 | process.setMaxListeners(0) 31 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # Use the official Node.js Debian image as the base image 2 | FROM node:22-bookworm-slim AS base 3 | 4 | ENV CHROME_BIN="/usr/bin/chromium" \ 5 | PUPPETEER_SKIP_CHROMIUM_DOWNLOAD="true" \ 6 | NODE_ENV="production" 7 | 8 | WORKDIR /usr/src/app 9 | 10 | FROM base AS deps 11 | 12 | ARG USE_EDGE=false 13 | 14 | COPY package*.json ./ 15 | 16 | RUN if [ "$USE_EDGE" = "true" ]; then \ 17 | apt-get update && apt-get install -y --no-install-recommends git ca-certificates && \ 18 | npm ci --only=production --ignore-scripts && \ 19 | npm install --save-exact git+https://github.com/pedroslopez/whatsapp-web.js.git#main && \ 20 | apt-get purge -y git ca-certificates && apt-get autoremove -y && rm -rf /var/lib/apt/lists/*; \ 21 | else \ 22 | npm ci --only=production --ignore-scripts; \ 23 | fi 24 | 25 | # Create the final stage 26 | FROM base 27 | 28 | # Install system dependencies 29 | RUN apt-get update && \ 30 | apt-get install -y --no-install-recommends \ 31 | fonts-freefont-ttf \ 32 | chromium \ 33 | ffmpeg && \ 34 | apt-get clean && \ 35 | rm -rf /var/lib/apt/lists/* 36 | 37 | # Copy only production dependencies from deps stage 38 | COPY --from=deps /usr/src/app/node_modules ./node_modules 39 | COPY --from=deps /usr/src/app/package*.json ./ 40 | 41 | # Copy application code 42 | COPY server.js ./ 43 | COPY LICENSE ./ 44 | COPY swagger.json ./ 45 | COPY src/ ./src/ 46 | 47 | EXPOSE 3000 48 | 49 | CMD ["npm", "start"] 50 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | # MIT License 2 | 3 | [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) 4 | 5 | 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. 6 | 7 | ## License 8 | 9 | MIT License 10 | 11 | ``` 12 | MIT License 13 | 14 | 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: 15 | 16 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 17 | 18 | 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. 19 | ``` -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "wwebjs-api", 3 | "version": "1.0.0", 4 | "description": "REST API wrapper for whatsapp-web.js", 5 | "main": "server.js", 6 | "scripts": { 7 | "start": "node server.js", 8 | "test": "jest --runInBand", 9 | "swagger": "node swagger.js" 10 | }, 11 | "dependencies": { 12 | "axios": "^1.13.2", 13 | "dotenv": "^16.6.1", 14 | "express": "^4.21.2", 15 | "express-rate-limit": "^7.5.1", 16 | "pino": "^9.14.0", 17 | "qr-image": "^3.2.0", 18 | "qrcode-terminal": "^0.12.0", 19 | "swagger-ui-express": "^5.0.1", 20 | "whatsapp-web.js": "^1.34.2", 21 | "ws": "^8.18.3" 22 | }, 23 | "devDependencies": { 24 | "eslint": "^8.38.0", 25 | "eslint-config-standard": "^17.0.0", 26 | "eslint-plugin-import": "^2.27.5", 27 | "eslint-plugin-n": "^15.7.0", 28 | "eslint-plugin-promise": "^6.1.1", 29 | "jest": "^29.7.0", 30 | "supertest": "^6.3.3", 31 | "swagger-autogen": "^2.23.7" 32 | }, 33 | "keywords": [ 34 | "whatsapp", 35 | "whatsapp-web", 36 | "wwebjs", 37 | "api", 38 | "wrapper", 39 | "rest", 40 | "express", 41 | "axios" 42 | ], 43 | "author": { 44 | "name": "Anton Voylenko", 45 | "email": "anton.voylenko@gmail.com" 46 | }, 47 | "license": "MIT", 48 | "engines": { 49 | "node": ">=18" 50 | }, 51 | "repository": { 52 | "type": "git", 53 | "url": "https://github.com/avoylenko/wwebjs-api.git" 54 | }, 55 | "bugs": { 56 | "url": "https://github.com/avoylenko/wwebjs-api/issues" 57 | }, 58 | "homepage": "https://github.com/avoylenko/wwebjs-api" 59 | } 60 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to WWebJS REST API Wrapper 2 | 3 | Welcome to WWebJS API Wrapper! We appreciate your interest in contributing to this project. Please follow the guidelines below to contribute effectively. 4 | 5 | ## Getting Started 6 | 7 | 1. Fork the repository. 8 | 2. Clone your forked repository to your local machine. 9 | 3. Install the necessary dependencies by running `npm install`. 10 | 4. Create a new branch for your contribution. 11 | 12 | ## Code Style 13 | 14 | - Follow the existing code style and conventions in the project. 15 | - Use meaningful variable and function names. 16 | - Add comments to your code, especially for complex or tricky parts. 17 | 18 | ## Pull Requests 19 | 20 | - Create a pull request from your branch to the `main` branch of this repository. 21 | - Provide a clear and descriptive title for your pull request. 22 | - Include a detailed description of the changes you made in the pull request. 23 | - Reference any related issues in your pull request description using the `#` symbol followed by the issue number. 24 | 25 | ## Testing 26 | 27 | - Write appropriate unit tests for your code. 28 | - Make sure all existing tests pass. 29 | - Provide instructions for testing your changes, if necessary. 30 | 31 | ## Documentation 32 | 33 | - Update the README.md file with any relevant information about your contribution, including installation instructions, usage examples, and API documentation. 34 | 35 | ## Code Block Example 36 | 37 | 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: 38 | 39 | \``` 40 | // Example code block 41 | const hello = "Hello, world!"; 42 | console.log(hello); 43 | \``` 44 | 45 | This will render as: 46 | 47 | ``` 48 | // Example code block 49 | const hello = "Hello, world!"; 50 | console.log(hello); 51 | ``` 52 | 53 | ## Contact Us 54 | 55 | 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. 56 | 57 | Thank you for your contribution! -------------------------------------------------------------------------------- /.github/workflows/push.yml: -------------------------------------------------------------------------------- 1 | name: CI/CD Pipeline for Docker Tag Push 2 | 3 | on: 4 | push: 5 | tags: 6 | - "v*" 7 | - "edge" 8 | 9 | jobs: 10 | test: 11 | runs-on: ubuntu-latest 12 | strategy: 13 | matrix: 14 | node-version: 15 | - 22.x 16 | steps: 17 | - name: Checkout repository 18 | uses: actions/checkout@v4 19 | - name: "Use Node.js ${{ matrix.node-version }}" 20 | uses: actions/setup-node@v4 21 | with: 22 | node-version: "${{ matrix.node-version }}" 23 | - name: Install dependencies 24 | run: npm ci 25 | - name: Install edge version of whatsapp-web.js 26 | if: github.ref_name == 'edge' 27 | run: | 28 | npm install --save-exact github:pedroslopez/whatsapp-web.js#main 29 | - name: Run tests 30 | run: npm test 31 | timeout-minutes: 5 32 | 33 | docker: 34 | needs: test 35 | runs-on: ubuntu-latest 36 | steps: 37 | - name: Checkout repository 38 | uses: actions/checkout@v3 39 | - name: Set up QEMU 40 | uses: docker/setup-qemu-action@v2 41 | - name: Set up Docker Buildx 42 | uses: docker/setup-buildx-action@v2 43 | - name: Login to Docker Hub 44 | uses: docker/login-action@v2 45 | with: 46 | username: "${{ secrets.DOCKER_HUB_USERNAME }}" 47 | password: "${{ secrets.DOCKER_HUB_TOKEN }}" 48 | - name: Extract metadata 49 | id: meta 50 | run: | 51 | if [[ "${{ github.ref_name }}" == "edge" ]]; then 52 | echo "tags=avoylenko/wwebjs-api:edge" >> $GITHUB_OUTPUT 53 | else 54 | echo "tags=avoylenko/wwebjs-api:${{ github.ref_name }},avoylenko/wwebjs-api:latest" >> $GITHUB_OUTPUT 55 | fi 56 | - name: Build and push with dynamic tag 57 | uses: docker/build-push-action@v5 58 | with: 59 | platforms: linux/amd64,linux/arm64,linux/arm/v7 60 | push: true 61 | tags: ${{ steps.meta.outputs.tags }} 62 | build-args: | 63 | USE_EDGE=${{ github.ref_name == 'edge' }} 64 | no-cache: ${{ github.ref_name == 'edge' }} 65 | -------------------------------------------------------------------------------- /swagger.js: -------------------------------------------------------------------------------- 1 | const swaggerAutogen = require('swagger-autogen')({ openapi: '3.0.0', autoBody: false }) 2 | 3 | const outputFile = './swagger.json' 4 | const endpointsFiles = ['./src/routes.js'] 5 | 6 | const doc = { 7 | info: { 8 | title: 'WWebJS API', 9 | description: 'API wrapper for WhatsAppWebJS' 10 | }, 11 | servers: [ 12 | { 13 | url: '/', 14 | description: 'default server' 15 | }, 16 | { 17 | url: 'http://localhost:3000', 18 | description: 'localhost server' 19 | } 20 | ], 21 | securityDefinitions: { 22 | apiKeyAuth: { 23 | type: 'apiKey', 24 | in: 'header', 25 | name: 'x-api-key' 26 | } 27 | }, 28 | produces: ['application/json'], 29 | tags: [ 30 | { 31 | name: 'Session', 32 | description: 'Handling multiple sessions logic, creation and deletion' 33 | }, 34 | { 35 | name: 'Client', 36 | description: 'All functions related to the client' 37 | }, 38 | { 39 | name: 'Message' 40 | } 41 | ], 42 | definitions: { 43 | StartSessionResponse: { 44 | success: true, 45 | message: 'Session initiated successfully' 46 | }, 47 | StopSessionResponse: { 48 | success: true, 49 | message: 'Session stopped successfully' 50 | }, 51 | StatusSessionResponse: { 52 | success: true, 53 | state: 'CONNECTED', 54 | message: 'session_connected' 55 | }, 56 | RestartSessionResponse: { 57 | success: true, 58 | message: 'Restarted successfully' 59 | }, 60 | TerminateSessionResponse: { 61 | success: true, 62 | message: 'Logged out successfully' 63 | }, 64 | TerminateSessionsResponse: { 65 | success: true, 66 | message: 'Flush completed successfully' 67 | }, 68 | ErrorResponse: { 69 | success: false, 70 | error: 'Some server error' 71 | }, 72 | NotFoundResponse: { 73 | success: false, 74 | error: 'Not found error' 75 | }, 76 | ForbiddenResponse: { 77 | success: false, 78 | error: 'Invalid API key' 79 | }, 80 | GetSessionsResponse: { 81 | success: true, 82 | result: ['session1', 'session2'] 83 | } 84 | } 85 | } 86 | 87 | swaggerAutogen(outputFile, endpointsFiles, doc) 88 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | ## Application ## 2 | # OPTIONAL, DEFAULT 3000 3 | PORT=3000 4 | # OPTIONAL, IF SET, ALL REQUESTS MUST INCLUDE THIS IN THE 'x-api-key' HEADER 5 | API_KEY=SET_YOUR_API_KEY_HERE 6 | # MANDATORY 7 | BASE_WEBHOOK_URL=http://localhost:3000/localCallbackExample 8 | # OPTIONAL, DISABLE FOR PRODUCTION 9 | ENABLE_LOCAL_CALLBACK_EXAMPLE=TRUE 10 | # OPTIONAL, THE MAXIMUM NUMBER OF CONNECTIONS TO ALLOW PER TIME FRAME 11 | RATE_LIMIT_MAX=1000 12 | # OPTIONAL, TIME FRAME FOR WHICH REQUESTS ARE CHECKED IN MS 13 | RATE_LIMIT_WINDOW_MS=1000 14 | 15 | ## Client ## 16 | # IF REACHED, MEDIA ATTACHMENT BODY WILL BE NULL 17 | MAX_ATTACHMENT_SIZE=10000000 18 | # WILL MARK THE MESSAGES AS READ AUTOMATICALLY 19 | SET_MESSAGES_AS_SEEN=TRUE 20 | # PREVENT SENDING CERTAIN TYPES OF CALLBACKS BACK TO THE WEBHOOK 21 | # 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 22 | DISABLED_CALLBACKS=message_ack|message_reaction|unread_count|message_edit|message_ciphertext 23 | # OPTIONAL, THE VERSION OF WHATSAPP WEB TO USE 24 | WEB_VERSION='2.2328.5' 25 | # OPTIONAL, DETERMINES WHERE TO GET THE WHATSAPP WEB VERSION(local, remote or none), DEFAULT 'none' 26 | WEB_VERSION_CACHE_TYPE=none 27 | # OPTIONAL, SHOULD WE RECOVER THE SESSION IN CASE OF PAGE FAILURES 28 | RECOVER_SESSIONS=TRUE 29 | # OPTIONAL, PATH TO CHROME BINARY 30 | CHROME_BIN= 31 | # OPTIONAL, RUN CHROME IN HEADLESS MODE 32 | HEADLESS=TRUE 33 | # OPTIONAL, RELEASE THE BROWSER LOCK ON SESSION INITIALIZATION 34 | RELEASE_BROWSER_LOCK=TRUE 35 | # OPTIONAL, SET THE LOG LEVEL 36 | LOG_LEVEL=info 37 | # OPTIONAL, ENABLE WEBHOOK FOR REALTIME UPDATES(TRUE BY DEFAULT) 38 | ENABLE_WEBHOOK=TRUE 39 | # OPTIONAL, ENABLE WEBSOCKET FOR REALTIME UPDATES(FALSE BY DEFAULT) 40 | ENABLE_WEBSOCKET=FALSE 41 | # OPTIONAL, AUTO START SESSIONS ON SERVER STARTUP(TRUE BY DEFAULT) 42 | AUTO_START_SESSIONS=TRUE 43 | 44 | ## Session File Storage ## 45 | # OPTIONAL 46 | SESSIONS_PATH=./sessions 47 | # OPTIONAL, ENABLE SWAGGER ENDPOINT FOR API DOCUMENTATION 48 | ENABLE_SWAGGER_ENDPOINT=TRUE 49 | 50 | ## Reverse Proxy / Load Balancer ## 51 | # OPTIONAL, BASE PATH FOR MOUNTING ROUTES (e.g., /api/v1/whatsapp) 52 | BASE_PATH= 53 | # OPTIONAL, ENABLE WHEN BEHIND REVERSE PROXY/LOAD BALANCER 54 | TRUST_PROXY=FALSE -------------------------------------------------------------------------------- /src/websocket.js: -------------------------------------------------------------------------------- 1 | const { WebSocketServer } = require('ws') 2 | const { enableWebSocket, basePath } = require('./config') 3 | const { logger } = require('./logger') 4 | const wssMap = new Map() 5 | 6 | // Function to initialize the WebSocket server if enabled 7 | const initWebSocketServer = (sessionId) => { 8 | if (enableWebSocket) { 9 | const server = wssMap.get(sessionId) 10 | if (server) { 11 | // happens on session restart 12 | return 13 | } 14 | // init websocket server 15 | const wss = new WebSocketServer({ noServer: true }) 16 | wssMap.set(sessionId, wss) 17 | wss.on('connection', (ws) => { 18 | logger.debug({ sessionId }, 'WebSocket connection established') 19 | ws.on('close', () => { 20 | logger.debug({ sessionId }, 'WebSocket connection closed') 21 | }) 22 | ws.on('error', () => { 23 | logger.error({ sessionId }, 'WebSocket connection error') 24 | }) 25 | }) 26 | } 27 | } 28 | 29 | // Function to initialize the WebSocket server 30 | const terminateWebSocketServer = async (sessionId) => { 31 | const server = wssMap.get(sessionId) 32 | if (!server) { 33 | return Promise.resolve() 34 | } 35 | const closeEventSignal = new Promise((resolve, reject) => 36 | server.close(err => (err ? reject(err) : resolve(undefined))) 37 | ) 38 | for (const ws of server.clients) { 39 | ws.terminate() 40 | } 41 | wssMap.delete(sessionId) 42 | await closeEventSignal 43 | } 44 | 45 | const triggerWebSocket = (sessionId, dataType, data) => { 46 | const server = wssMap.get(sessionId) 47 | if (server) { 48 | for (const ws of server.clients) { 49 | ws.send(JSON.stringify({ dataType, data, sessionId })) 50 | } 51 | } 52 | } 53 | 54 | const handleUpgrade = (request, socket, head) => { 55 | const host = request.headers['x-forwarded-host'] || request.headers.host 56 | const baseUrl = 'ws://' + host + '/' 57 | const { pathname } = new URL(request.url, baseUrl) 58 | 59 | // Handle base path for WebSocket connections 60 | const wsPath = basePath ? `${basePath}/ws/` : '/ws/' 61 | if (pathname.startsWith(wsPath)) { 62 | const pathParts = pathname.split('/') 63 | const sessionId = basePath ? pathParts[3] : pathParts[2] 64 | const server = wssMap.get(sessionId) 65 | if (server) { 66 | server.handleUpgrade(request, socket, head, (ws) => { 67 | server.emit('connection', ws, request) 68 | }) 69 | return 70 | } 71 | } 72 | socket.destroy() 73 | } 74 | 75 | module.exports = { initWebSocketServer, terminateWebSocketServer, handleUpgrade, triggerWebSocket } 76 | -------------------------------------------------------------------------------- /src/config.js: -------------------------------------------------------------------------------- 1 | // Load environment variables from .env file 2 | require('dotenv').config({ path: process.env.ENV_PATH || '.env' }) 3 | 4 | // setup global const 5 | const servicePort = process.env.PORT || 3000 6 | const sessionFolderPath = process.env.SESSIONS_PATH || './sessions' 7 | const enableLocalCallbackExample = (process.env.ENABLE_LOCAL_CALLBACK_EXAMPLE || '').toLowerCase() === 'true' 8 | const globalApiKey = process.env.API_KEY 9 | const baseWebhookURL = process.env.BASE_WEBHOOK_URL 10 | const maxAttachmentSize = parseInt(process.env.MAX_ATTACHMENT_SIZE) || 10000000 11 | const setMessagesAsSeen = (process.env.SET_MESSAGES_AS_SEEN || '').toLowerCase() === 'true' 12 | const disabledCallbacks = process.env.DISABLED_CALLBACKS ? process.env.DISABLED_CALLBACKS.split('|') : [] 13 | const enableSwaggerEndpoint = (process.env.ENABLE_SWAGGER_ENDPOINT || '').toLowerCase() === 'true' 14 | const webVersion = process.env.WEB_VERSION 15 | const webVersionCacheType = process.env.WEB_VERSION_CACHE_TYPE || 'none' 16 | const rateLimitMax = parseInt(process.env.RATE_LIMIT_MAX) || 1000 17 | const rateLimitWindowMs = parseInt(process.env.RATE_LIMIT_WINDOW_MS) || 1000 18 | const recoverSessions = (process.env.RECOVER_SESSIONS || '').toLowerCase() === 'true' 19 | const chromeBin = process.env.CHROME_BIN || null 20 | const headless = process.env.HEADLESS ? (process.env.HEADLESS).toLowerCase() === 'true' : true 21 | const releaseBrowserLock = process.env.RELEASE_BROWSER_LOCK ? (process.env.RELEASE_BROWSER_LOCK).toLowerCase() === 'true' : true 22 | const logLevel = process.env.LOG_LEVEL || 'info' 23 | const enableWebHook = process.env.ENABLE_WEBHOOK ? (process.env.ENABLE_WEBHOOK).toLowerCase() === 'true' : true 24 | const enableWebSocket = process.env.ENABLE_WEBSOCKET ? (process.env.ENABLE_WEBSOCKET).toLowerCase() === 'true' : false 25 | const autoStartSessions = process.env.AUTO_START_SESSIONS ? (process.env.AUTO_START_SESSIONS).toLowerCase() === 'true' : true 26 | const basePath = process.env.BASE_PATH || '/' 27 | const trustProxy = process.env.TRUST_PROXY ? (process.env.TRUST_PROXY).toLowerCase() === 'true' : false 28 | 29 | module.exports = { 30 | servicePort, 31 | sessionFolderPath, 32 | enableLocalCallbackExample, 33 | globalApiKey, 34 | baseWebhookURL, 35 | maxAttachmentSize, 36 | setMessagesAsSeen, 37 | disabledCallbacks, 38 | enableSwaggerEndpoint, 39 | webVersion, 40 | webVersionCacheType, 41 | rateLimitMax, 42 | rateLimitWindowMs, 43 | recoverSessions, 44 | chromeBin, 45 | headless, 46 | releaseBrowserLock, 47 | logLevel, 48 | enableWebHook, 49 | enableWebSocket, 50 | autoStartSessions, 51 | basePath, 52 | trustProxy 53 | } 54 | -------------------------------------------------------------------------------- /src/controllers/healthController.js: -------------------------------------------------------------------------------- 1 | const fsp = require('fs').promises 2 | const qrcode = require('qrcode-terminal') 3 | const { sessionFolderPath } = require('../config') 4 | const { sendErrorResponse } = require('../utils') 5 | const { logger } = require('../logger') 6 | 7 | /** 8 | * Responds to request with 'pong' 9 | * 10 | * @function ping 11 | * @async 12 | * @param {Object} req - Express request object 13 | * @param {Object} res - Express response object 14 | * @returns {Promise} - Promise that resolves once response is sent 15 | * @throws {Object} - Throws error if response fails 16 | */ 17 | const ping = async (req, res) => { 18 | /* 19 | #swagger.tags = ['Various'] 20 | #swagger.summary = 'Health check' 21 | #swagger.description = 'Responds to request with "pong" message' 22 | #swagger.responses[200] = { 23 | description: "Response message", 24 | content: { 25 | "application/json": { 26 | example: { 27 | success: true, 28 | message: "pong" 29 | } 30 | } 31 | } 32 | } 33 | */ 34 | res.json({ success: true, message: 'pong' }) 35 | } 36 | 37 | /** 38 | * Example local callback that generates a QR code and writes a log file 39 | * 40 | * @function localCallbackExample 41 | * @async 42 | * @param {Object} req - Express request object containing a body object with dataType and data 43 | * @param {string} req.body.dataType - Type of data (in this case, 'qr') 44 | * @param {Object} req.body.data - Data to generate a QR code from 45 | * @param {Object} res - Express response object 46 | * @returns {Promise} - Promise that resolves once response is sent 47 | * @throws {Object} - Throws error if response fails 48 | */ 49 | const localCallbackExample = async (req, res) => { 50 | /* 51 | #swagger.tags = ['Various'] 52 | #swagger.summary = 'Local callback' 53 | #swagger.description = 'Used to generate a QR code and writes a log file. ONLY FOR DEVELOPMENT/TEST PURPOSES.' 54 | #swagger.responses[200] = { 55 | description: "Response message", 56 | content: { 57 | "application/json": { 58 | example: { 59 | success: true 60 | } 61 | } 62 | } 63 | } 64 | */ 65 | try { 66 | const { dataType, data } = req.body 67 | if (dataType === 'qr') { qrcode.generate(data.qr, { small: true }) } 68 | await fsp.mkdir(sessionFolderPath, { recursive: true }) 69 | await fsp.writeFile(`${sessionFolderPath}/message_log.txt`, `${JSON.stringify(req.body)}\r\n`, { flag: 'a+' }) 70 | res.json({ success: true }) 71 | } catch (error) { 72 | /* #swagger.responses[500] = { 73 | description: "Server Failure.", 74 | content: { 75 | "application/json": { 76 | schema: { "$ref": "#/definitions/ErrorResponse" } 77 | } 78 | } 79 | } 80 | */ 81 | logger.error(error, 'Failed to handle local callback') 82 | sendErrorResponse(res, 500, error.message) 83 | } 84 | } 85 | 86 | module.exports = { ping, localCallbackExample } 87 | -------------------------------------------------------------------------------- /REVERSE_PROXY_SETUP.md: -------------------------------------------------------------------------------- 1 | # Reverse Proxy / Kong Setup Guide 2 | 3 | This document provides configuration examples for deploying WWebJS API behind reverse proxies like Kong, Nginx, or other load balancers. 4 | 5 | ## Environment Variables 6 | 7 | The following environment variables have been added to support reverse proxy deployments: 8 | 9 | ```bash 10 | # Base path for mounting all routes (optional) 11 | BASE_PATH=/api/v1/whatsapp 12 | 13 | # Enable trust proxy for proper IP forwarding (required for reverse proxy) 14 | TRUST_PROXY=true 15 | ``` 16 | 17 | ## Kong Configuration 18 | 19 | ### Basic Kong Route Setup 20 | 21 | ```yaml 22 | # Kong route configuration 23 | routes: 24 | - name: wwebjs-api 25 | paths: ["/api/v1/whatsapp"] 26 | strip_path: true # Important: removes the prefix before forwarding 27 | preserve_host: false 28 | protocols: ["http", "https"] 29 | service: wwebjs-service 30 | 31 | services: 32 | - name: wwebjs-service 33 | url: http://wwebjs-api:3000 34 | connect_timeout: 60000 35 | write_timeout: 60000 36 | read_timeout: 60000 37 | ``` 38 | 39 | ### Kong with WebSocket Support 40 | 41 | ```yaml 42 | # Kong route for WebSocket connections 43 | routes: 44 | - name: wwebjs-websocket 45 | paths: ["/api/v1/whatsapp/ws"] 46 | strip_path: true 47 | protocols: ["ws", "wss"] 48 | service: wwebjs-websocket-service 49 | 50 | services: 51 | - name: wwebjs-websocket-service 52 | url: http://wwebjs-api:3000 53 | ``` 54 | 55 | ## Nginx Configuration 56 | 57 | ```nginx 58 | upstream wwebjs_backend { 59 | server wwebjs-api:3000; 60 | } 61 | 62 | server { 63 | listen 80; 64 | server_name api.yourdomain.com; 65 | 66 | location /api/v1/whatsapp/ { 67 | proxy_pass http://wwebjs_backend/; 68 | proxy_set_header Host $host; 69 | proxy_set_header X-Real-IP $remote_addr; 70 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 71 | proxy_set_header X-Forwarded-Proto $scheme; 72 | proxy_set_header X-Forwarded-Host $host; 73 | 74 | # WebSocket support 75 | proxy_http_version 1.1; 76 | proxy_set_header Upgrade $http_upgrade; 77 | proxy_set_header Connection "upgrade"; 78 | 79 | # Timeouts for long-running operations 80 | proxy_connect_timeout 60s; 81 | proxy_send_timeout 60s; 82 | proxy_read_timeout 60s; 83 | } 84 | } 85 | ``` 86 | 87 | ## Docker Compose with Reverse Proxy 88 | 89 | ```yaml 90 | version: '3.8' 91 | 92 | services: 93 | wwebjs-api: 94 | image: avoylenko/wwebjs-api:latest 95 | container_name: wwebjs-api 96 | restart: always 97 | environment: 98 | # Reverse proxy configuration 99 | - BASE_PATH=/api/v1/whatsapp 100 | - TRUST_PROXY=true 101 | 102 | # Other configurations 103 | - BASE_WEBHOOK_URL=https://api.yourdomain.com/api/v1/whatsapp/localCallbackExample 104 | - API_KEY=your_secure_api_key 105 | - ENABLE_LOCAL_CALLBACK_EXAMPLE=false 106 | - ENABLE_SWAGGER_ENDPOINT=true 107 | volumes: 108 | - ./sessions:/usr/src/app/sessions 109 | networks: 110 | - api-network 111 | 112 | nginx: 113 | image: nginx:alpine 114 | container_name: nginx-proxy 115 | ports: 116 | - "80:80" 117 | - "443:443" 118 | volumes: 119 | - ./nginx.conf:/etc/nginx/nginx.conf 120 | depends_on: 121 | - wwebjs-api 122 | networks: 123 | - api-network 124 | 125 | networks: 126 | api-network: 127 | driver: bridge 128 | ``` 129 | 130 | ## API Endpoint Examples 131 | 132 | With `BASE_PATH=/api/v1/whatsapp` configured: 133 | 134 | ### Original endpoints: 135 | - `GET /session/start/ABCD` 136 | - `GET /client/getContacts/ABCD` 137 | - `WebSocket: ws://localhost:3000/ws/ABCD` 138 | 139 | ### Behind reverse proxy: 140 | - `GET https://api.yourdomain.com/api/v1/whatsapp/session/start/ABCD` 141 | - `GET https://api.yourdomain.com/api/v1/whatsapp/client/getContacts/ABCD` 142 | - `WebSocket: wss://api.yourdomain.com/api/v1/whatsapp/ws/ABCD` 143 | 144 | ## Important Notes 145 | 146 | 1. **Strip Path**: Always configure your reverse proxy to strip the base path before forwarding to the application 147 | 2. **Trust Proxy**: Set `TRUST_PROXY=true` to ensure proper IP detection for rate limiting 148 | 3. **WebSocket Headers**: Ensure `X-Forwarded-Host` header is properly forwarded for WebSocket connections 149 | 4. **Timeouts**: Configure appropriate timeouts for WhatsApp operations which can take time 150 | 5. **HTTPS**: Use HTTPS in production and update `BASE_WEBHOOK_URL` accordingly 151 | 152 | ## Troubleshooting 153 | 154 | ### Common Issues: 155 | 156 | 1. **404 Errors**: Check if `strip_path` is enabled in your reverse proxy 157 | 2. **WebSocket Connection Failed**: Ensure WebSocket upgrade headers are properly forwarded 158 | 3. **Rate Limiting Issues**: Verify `TRUST_PROXY=true` is set and `X-Forwarded-For` header is forwarded 159 | 4. **Webhook Callbacks**: Update `BASE_WEBHOOK_URL` to use the external domain with base path 160 | 161 | ### Testing: 162 | 163 | ```bash 164 | # Test API endpoint 165 | curl https://api.yourdomain.com/api/v1/whatsapp/ping 166 | 167 | # Test WebSocket connection 168 | wscat -c wss://api.yourdomain.com/api/v1/whatsapp/ws/test 169 | ``` 170 | -------------------------------------------------------------------------------- /src/utils.js: -------------------------------------------------------------------------------- 1 | const axios = require('axios') 2 | const { globalApiKey, disabledCallbacks, enableWebHook } = require('./config') 3 | const { logger } = require('./logger') 4 | const ChatFactory = require('whatsapp-web.js/src/factories/ChatFactory') 5 | const Client = require('whatsapp-web.js').Client 6 | const { Chat, Message } = require('whatsapp-web.js/src/structures') 7 | 8 | // Trigger webhook endpoint 9 | const triggerWebhook = (webhookURL, sessionId, dataType, data) => { 10 | if (enableWebHook) { 11 | axios.post(webhookURL, { dataType, data, sessionId }, { headers: { 'x-api-key': globalApiKey } }) 12 | .then(() => logger.debug({ sessionId, dataType, data: data || '' }, `Webhook message sent to ${webhookURL}`)) 13 | .catch(error => logger.error({ sessionId, dataType, err: error, data: data || '' }, `Failed to send webhook message to ${webhookURL}`)) 14 | } 15 | } 16 | 17 | // Function to send a response with error status and message 18 | const sendErrorResponse = (res, status, message) => { 19 | res.status(status).json({ success: false, error: message }) 20 | } 21 | 22 | // Function to wait for a specific item not to be null 23 | const waitForNestedObject = (rootObj, nestedPath, maxWaitTime = 10000, interval = 100) => { 24 | const start = Date.now() 25 | return new Promise((resolve, reject) => { 26 | const checkObject = () => { 27 | const nestedObj = nestedPath.split('.').reduce((obj, key) => obj ? obj[key] : undefined, rootObj) 28 | if (nestedObj) { 29 | // Nested object exists, resolve the promise 30 | resolve() 31 | } else if (Date.now() - start > maxWaitTime) { 32 | // Maximum wait time exceeded, reject the promise 33 | logger.error('Timed out waiting for nested object') 34 | reject(new Error('Timeout waiting for nested object')) 35 | } else { 36 | // Nested object not yet created, continue waiting 37 | setTimeout(checkObject, interval) 38 | } 39 | } 40 | checkObject() 41 | }) 42 | } 43 | 44 | const isEventEnabled = (event) => { 45 | return !disabledCallbacks.includes(event) 46 | } 47 | 48 | const sendMessageSeenStatus = async (message) => { 49 | try { 50 | const chat = await message.getChat() 51 | await chat.sendSeen() 52 | } catch (error) { 53 | logger.error(error, 'Failed to send seen status') 54 | } 55 | } 56 | 57 | const decodeBase64 = function * (base64String) { 58 | const chunkSize = 1024 59 | for (let i = 0; i < base64String.length; i += chunkSize) { 60 | const chunk = base64String.slice(i, i + chunkSize) 61 | yield Buffer.from(chunk, 'base64') 62 | } 63 | } 64 | 65 | const sleep = function (ms) { 66 | return new Promise(resolve => setTimeout(resolve, ms)) 67 | } 68 | 69 | const exposeFunctionIfAbsent = async (page, name, fn) => { 70 | const exist = await page.evaluate((name) => { 71 | return !!window[name] 72 | }, name) 73 | if (exist) { 74 | return 75 | } 76 | await page.exposeFunction(name, fn) 77 | } 78 | 79 | const patchWWebLibrary = async (client) => { 80 | // MUST be run after the 'ready' event fired 81 | Client.prototype.getChats = async function (searchOptions = {}) { 82 | const chats = await this.pupPage.evaluate(async (searchOptions) => { 83 | return await window.WWebJS.getChats({ ...searchOptions }) 84 | }, searchOptions) 85 | 86 | return chats.map(chat => ChatFactory.create(this, chat)) 87 | } 88 | 89 | Chat.prototype.fetchMessages = async function (searchOptions) { 90 | const messages = await this.client.pupPage.evaluate(async (chatId, searchOptions) => { 91 | const msgFilter = (m) => { 92 | if (m.isNotification) { 93 | return false 94 | } 95 | if (searchOptions && searchOptions.fromMe !== undefined && m.id.fromMe !== searchOptions.fromMe) { 96 | return false 97 | } 98 | if (searchOptions && searchOptions.since !== undefined && Number.isFinite(searchOptions.since) && m.t < searchOptions.since) { 99 | return false 100 | } 101 | return true 102 | } 103 | 104 | const chat = await window.WWebJS.getChat(chatId, { getAsModel: false }) 105 | let msgs = chat.msgs.getModelsArray().filter(msgFilter) 106 | 107 | if (searchOptions && searchOptions.limit > 0) { 108 | while (msgs.length < searchOptions.limit) { 109 | const loadedMessages = await window.Store.ConversationMsgs.loadEarlierMsgs(chat) 110 | if (!loadedMessages || !loadedMessages.length) break 111 | msgs = [...loadedMessages.filter(msgFilter), ...msgs] 112 | } 113 | 114 | if (msgs.length > searchOptions.limit) { 115 | msgs.sort((a, b) => (a.t > b.t) ? 1 : -1) 116 | msgs = msgs.splice(msgs.length - searchOptions.limit) 117 | } 118 | } 119 | 120 | return msgs.map(m => window.WWebJS.getMessageModel(m)) 121 | }, this.id._serialized, searchOptions) 122 | 123 | return messages.map(m => new Message(this.client, m)) 124 | } 125 | 126 | await client.pupPage.evaluate(() => { 127 | // hotfix for https://github.com/pedroslopez/whatsapp-web.js/pull/3643 128 | window.WWebJS.getChats = async (searchOptions = {}) => { 129 | const chatFilter = (c) => { 130 | if (searchOptions && searchOptions.unread === true && c.unreadCount === 0) { 131 | return false 132 | } 133 | if (searchOptions && searchOptions.since !== undefined && Number.isFinite(searchOptions.since) && c.t < searchOptions.since) { 134 | return false 135 | } 136 | return true 137 | } 138 | 139 | const allChats = window.Store.Chat.getModelsArray() 140 | 141 | const filteredChats = allChats.filter(chatFilter) 142 | 143 | return await Promise.all( 144 | filteredChats.map(chat => window.WWebJS.getChatModel(chat)) 145 | ) 146 | } 147 | }) 148 | } 149 | 150 | module.exports = { 151 | triggerWebhook, 152 | sendErrorResponse, 153 | waitForNestedObject, 154 | isEventEnabled, 155 | sendMessageSeenStatus, 156 | decodeBase64, 157 | sleep, 158 | exposeFunctionIfAbsent, 159 | patchWWebLibrary 160 | } 161 | -------------------------------------------------------------------------------- /tests/api.test.js: -------------------------------------------------------------------------------- 1 | const request = require('supertest') 2 | const fs = require('fs') 3 | 4 | // Mock your application's environment variables 5 | process.env.API_KEY = 'test_api_key' 6 | process.env.SESSIONS_PATH = './sessions_test' 7 | process.env.ENABLE_LOCAL_CALLBACK_EXAMPLE = 'TRUE' 8 | process.env.BASE_WEBHOOK_URL = 'http://localhost:3000/localCallbackExample' 9 | 10 | const app = require('../src/app') 11 | jest.mock('qrcode-terminal') 12 | 13 | jest.setTimeout(5 * 60 * 1000) 14 | 15 | let server 16 | beforeAll(() => { 17 | fs.rmSync(process.env.SESSIONS_PATH, { recursive: true, force: true }) 18 | server = app.listen(3000) 19 | }) 20 | 21 | beforeEach(() => { 22 | if (fs.existsSync('./sessions_test/message_log.txt')) { 23 | fs.writeFileSync('./sessions_test/message_log.txt', '') 24 | } 25 | }) 26 | 27 | afterAll(() => { 28 | server.close() 29 | fs.rmSync(process.env.SESSIONS_PATH, { recursive: true, force: true }) 30 | }) 31 | 32 | // Define test cases 33 | describe('API health checks', () => { 34 | it('should return valid health check', async () => { 35 | const response = await request(app).get('/ping') 36 | expect(response.status).toBe(200) 37 | expect(response.body).toEqual({ message: 'pong', success: true }) 38 | }) 39 | 40 | it('should return a valid callback status', async () => { 41 | const response = await request(app).post('/localCallbackExample') 42 | .set('x-api-key', 'test_api_key') 43 | .send({ sessionId: '1', dataType: 'testDataType', data: 'testData' }) 44 | expect(response.status).toBe(200) 45 | expect(response.body).toEqual({ success: true }) 46 | 47 | expect(fs.existsSync('./sessions_test/message_log.txt')).toBe(true) 48 | expect(fs.readFileSync('./sessions_test/message_log.txt', 'utf-8')).toEqual('{"sessionId":"1","dataType":"testDataType","data":"testData"}\r\n') 49 | }) 50 | }) 51 | 52 | describe('API Authentication Tests', () => { 53 | it('should return 403 Forbidden for invalid API key', async () => { 54 | const response = await request(app).get('/session/start/1') 55 | expect(response.status).toBe(403) 56 | expect(response.body).toEqual({ success: false, error: 'Invalid API key' }) 57 | }) 58 | 59 | it('should fail invalid sessionId', async () => { 60 | const response = await request(app).get('/session/start/ABCD1@').set('x-api-key', 'test_api_key') 61 | expect(response.status).toBe(422) 62 | expect(response.body).toEqual({ success: false, error: 'Session should be alphanumerical or -' }) 63 | }) 64 | 65 | it('should setup and terminate a client session', async () => { 66 | const response = await request(app).get('/session/start/1').set('x-api-key', 'test_api_key') 67 | expect(response.status).toBe(200) 68 | expect(response.body).toEqual({ success: true, message: 'Session initiated successfully' }) 69 | expect(fs.existsSync('./sessions_test/session-1')).toBe(true) 70 | 71 | const response2 = await request(app).get('/session/terminate/1').set('x-api-key', 'test_api_key') 72 | expect(response2.status).toBe(200) 73 | expect(response2.body).toEqual({ success: true, message: 'Logged out successfully' }) 74 | 75 | expect(fs.existsSync('./sessions_test/session-1')).toBe(false) 76 | }) 77 | 78 | it('should setup and flush multiple client sessions', async () => { 79 | const response = await request(app).get('/session/start/2').set('x-api-key', 'test_api_key') 80 | expect(response.status).toBe(200) 81 | expect(response.body).toEqual({ success: true, message: 'Session initiated successfully' }) 82 | expect(fs.existsSync('./sessions_test/session-2')).toBe(true) 83 | 84 | const response2 = await request(app).get('/session/start/3').set('x-api-key', 'test_api_key') 85 | expect(response2.status).toBe(200) 86 | expect(response2.body).toEqual({ success: true, message: 'Session initiated successfully' }) 87 | expect(fs.existsSync('./sessions_test/session-3')).toBe(true) 88 | 89 | const response3 = await request(app).get('/session/terminateInactive').set('x-api-key', 'test_api_key') 90 | expect(response3.status).toBe(200) 91 | expect(response3.body).toEqual({ success: true, message: 'Flush completed successfully' }) 92 | 93 | expect(fs.existsSync('./sessions_test/session-2')).toBe(false) 94 | expect(fs.existsSync('./sessions_test/session-3')).toBe(false) 95 | }) 96 | }) 97 | 98 | describe('API Action Tests', () => { 99 | it('should setup, create at least a QR, and terminate a client session', async () => { 100 | const response = await request(app).get('/session/start/4').set('x-api-key', 'test_api_key') 101 | expect(response.status).toBe(200) 102 | expect(response.body).toEqual({ success: true, message: 'Session initiated successfully' }) 103 | expect(fs.existsSync('./sessions_test/session-4')).toBe(true) 104 | 105 | // Wait for message_log.txt to not be empty 106 | const result = await waitForFileNotToBeEmpty('./sessions_test/message_log.txt', 120_000, 1000) 107 | .then(() => { return true }) 108 | .catch(() => { return false }) 109 | expect(result).toBe(true) 110 | 111 | // Verify the message content 112 | const expectedMessage = { 113 | dataType: 'qr', 114 | data: expect.objectContaining({ qr: expect.any(String) }), 115 | sessionId: '4' 116 | } 117 | expect(JSON.parse(fs.readFileSync('./sessions_test/message_log.txt', 'utf-8'))).toEqual(expectedMessage) 118 | 119 | const response2 = await request(app).get('/session/terminate/4').set('x-api-key', 'test_api_key') 120 | expect(response2.status).toBe(200) 121 | expect(response2.body).toEqual({ success: true, message: 'Logged out successfully' }) 122 | expect(fs.existsSync('./sessions_test/session-4')).toBe(false) 123 | }) 124 | }) 125 | 126 | // Function to wait for a specific item to be equal a specific value 127 | const waitForFileNotToBeEmpty = (filePath, maxWaitTime = 10000, interval = 100) => { 128 | const start = Date.now() 129 | return new Promise((resolve, reject) => { 130 | const checkObject = async () => { 131 | try { 132 | const filecontent = await fs.promises.readFile(filePath, 'utf-8') 133 | if (filecontent !== '') { 134 | // Nested object exists, resolve the promise 135 | resolve() 136 | } else if (Date.now() - start > maxWaitTime) { 137 | // Maximum wait time exceeded, reject the promise 138 | console.log('Timed out waiting for nested object') 139 | reject(new Error('Timeout waiting for nested object')) 140 | } else { 141 | // Nested object not yet created, continue waiting 142 | setTimeout(checkObject, interval) 143 | } 144 | } catch (ignore) { 145 | // continue waiting 146 | setTimeout(checkObject, interval) 147 | } 148 | } 149 | checkObject() 150 | }) 151 | } 152 | -------------------------------------------------------------------------------- /src/middleware.js: -------------------------------------------------------------------------------- 1 | const { globalApiKey, rateLimitMax, rateLimitWindowMs } = require('./config') 2 | const { sendErrorResponse } = require('./utils') 3 | const { validateSession } = require('./sessions') 4 | const rateLimiting = require('express-rate-limit') 5 | 6 | const apikey = async (req, res, next) => { 7 | /* 8 | #swagger.security = [{ 9 | "apiKeyAuth": [] 10 | }] 11 | */ 12 | /* #swagger.responses[403] = { 13 | description: "Forbidden.", 14 | content: { 15 | "application/json": { 16 | schema: { "$ref": "#/definitions/ForbiddenResponse" } 17 | } 18 | } 19 | } 20 | */ 21 | if (globalApiKey) { 22 | const apiKey = req.headers['x-api-key'] 23 | if (!apiKey || apiKey !== globalApiKey) { 24 | return sendErrorResponse(res, 403, 'Invalid API key') 25 | } 26 | } 27 | next() 28 | } 29 | 30 | const sessionNameValidation = async (req, res, next) => { 31 | /* 32 | #swagger.parameters['sessionId'] = { 33 | in: 'path', 34 | description: 'Unique identifier for the session (alphanumeric and - allowed)', 35 | required: true, 36 | type: 'string', 37 | example: 'f8377d8d-a589-4242-9ba6-9486a04ef80c' 38 | } 39 | */ 40 | if ((!/^[\w-]+$/.test(req.params.sessionId))) { 41 | /* #swagger.responses[422] = { 42 | description: "Unprocessable Entity.", 43 | content: { 44 | "application/json": { 45 | schema: { "$ref": "#/definitions/ErrorResponse" } 46 | } 47 | } 48 | } 49 | */ 50 | return sendErrorResponse(res, 422, 'Session should be alphanumerical or -') 51 | } 52 | next() 53 | } 54 | 55 | const sessionValidation = async (req, res, next) => { 56 | const validation = await validateSession(req.params.sessionId) 57 | if (validation.success !== true) { 58 | /* #swagger.responses[404] = { 59 | description: "Not Found.", 60 | content: { 61 | "application/json": { 62 | schema: { "$ref": "#/definitions/NotFoundResponse" } 63 | } 64 | } 65 | } 66 | */ 67 | return sendErrorResponse(res, 404, validation.message) 68 | } 69 | next() 70 | } 71 | 72 | const rateLimiter = rateLimiting({ 73 | limit: rateLimitMax, 74 | windowMs: rateLimitWindowMs, 75 | message: "You can't make any more requests at the moment. Try again later", 76 | // Use real client IP when behind reverse proxy 77 | keyGenerator: (req) => { 78 | return req.ip || req.connection.remoteAddress 79 | } 80 | }) 81 | 82 | const sessionSwagger = async (req, res, next) => { 83 | /* 84 | #swagger.tags = ['Session'] 85 | #swagger.responses[500] = { 86 | description: "Server failure.", 87 | content: { 88 | "application/json": { 89 | schema: { "$ref": "#/definitions/ErrorResponse" } 90 | } 91 | } 92 | } 93 | */ 94 | next() 95 | } 96 | 97 | const clientSwagger = async (req, res, next) => { 98 | /* 99 | #swagger.tags = ['Client'] 100 | #swagger.responses[500] = { 101 | description: "Server failure.", 102 | content: { 103 | "application/json": { 104 | schema: { "$ref": "#/definitions/ErrorResponse" } 105 | } 106 | } 107 | } 108 | */ 109 | next() 110 | } 111 | 112 | const contactSwagger = async (req, res, next) => { 113 | /* 114 | #swagger.tags = ['Contact'] 115 | #swagger.requestBody = { 116 | required: true, 117 | schema: { 118 | type: 'object', 119 | properties: { 120 | contactId: { 121 | type: 'string', 122 | description: 'Unique WhatsApp ID for the contact', 123 | example: '6281288888888@c.us' 124 | } 125 | } 126 | } 127 | } 128 | #swagger.responses[500] = { 129 | description: "Server failure.", 130 | content: { 131 | "application/json": { 132 | schema: { "$ref": "#/definitions/ErrorResponse" } 133 | } 134 | } 135 | } 136 | */ 137 | next() 138 | } 139 | 140 | const messageSwagger = async (req, res, next) => { 141 | /* 142 | #swagger.tags = ['Message'] 143 | #swagger.requestBody = { 144 | required: true, 145 | schema: { 146 | type: 'object', 147 | properties: { 148 | chatId: { 149 | type: 'string', 150 | description: 'The chat id which contains the message', 151 | example: '6281288888888@c.us' 152 | }, 153 | messageId: { 154 | type: 'string', 155 | description: 'Unique WhatsApp ID for the message', 156 | example: 'ABCDEF999999999' 157 | } 158 | } 159 | } 160 | } 161 | */ 162 | next() 163 | } 164 | 165 | const chatSwagger = async (req, res, next) => { 166 | /* 167 | #swagger.tags = ['Chat'] 168 | #swagger.requestBody = { 169 | required: true, 170 | schema: { 171 | type: 'object', 172 | properties: { 173 | chatId: { 174 | type: 'string', 175 | description: 'Unique WhatsApp ID for the given chat (either group or personal)', 176 | example: '6281288888888@c.us' 177 | } 178 | } 179 | } 180 | } 181 | #swagger.responses[500] = { 182 | description: "Server failure.", 183 | content: { 184 | "application/json": { 185 | schema: { "$ref": "#/definitions/ErrorResponse" } 186 | } 187 | } 188 | } 189 | */ 190 | next() 191 | } 192 | 193 | const groupChatSwagger = async (req, res, next) => { 194 | /* 195 | #swagger.tags = ['Group Chat'] 196 | #swagger.requestBody = { 197 | required: true, 198 | schema: { 199 | type: 'object', 200 | properties: { 201 | chatId: { 202 | type: 'string', 203 | description: 'Unique WhatsApp id for the given chat group', 204 | example: 'XXXXXXXXXX@g.us' 205 | } 206 | } 207 | } 208 | } 209 | #swagger.responses[500] = { 210 | description: "Server failure.", 211 | content: { 212 | "application/json": { 213 | schema: { "$ref": "#/definitions/ErrorResponse" } 214 | } 215 | } 216 | } 217 | */ 218 | next() 219 | } 220 | 221 | const channelSwagger = async (req, res, next) => { 222 | /* 223 | #swagger.tags = ['Channel Chat'] 224 | #swagger.responses[500] = { 225 | description: "Server failure.", 226 | content: { 227 | "application/json": { 228 | schema: { "$ref": "#/definitions/ErrorResponse" } 229 | } 230 | } 231 | } 232 | */ 233 | next() 234 | } 235 | 236 | module.exports = { 237 | sessionValidation, 238 | apikey, 239 | sessionNameValidation, 240 | sessionSwagger, 241 | clientSwagger, 242 | contactSwagger, 243 | messageSwagger, 244 | chatSwagger, 245 | groupChatSwagger, 246 | channelSwagger, 247 | rateLimiter 248 | } 249 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # WWebJS REST API 2 | 3 | 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. 4 | It is designed to be used as a docker container, scalable, secure, and easy to integrate with other non-NodeJS projects. 5 | 6 | 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. 7 | 8 | The project is a work in progress: star it, create issues, features or pull requests ❣️ 9 | 10 | **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. 11 | 12 | ## Table of Contents 13 | 14 | [1. Quick Start with Docker](#quick-start-with-docker) 15 | 16 | [2. Features](#features) 17 | 18 | [3. Run Locally](#run-locally) 19 | 20 | [4. Testing](#testing) 21 | 22 | [5. Documentation](#documentation) 23 | 24 | [6. Deploy to Production](#deploy-to-production) 25 | 26 | [7. Contributing](#contributing) 27 | 28 | [8. License](#license) 29 | 30 | [9. Star History](#star-history) 31 | 32 | ## Quick Start with Docker 33 | 34 | [![dockeri.co](https://dockerico.blankenship.io/image/avoylenko/wwebjs-api)](https://hub.docker.com/r/avoylenko/wwebjs-api) 35 | 36 | 1. Clone the repository: 37 | 38 | ```bash 39 | git clone https://github.com/avoylenko/wwebjs-api.git 40 | cd wwebjs-api 41 | ``` 42 | 43 | 3. Run the Docker Compose: 44 | 45 | ```bash 46 | docker compose pull && docker compose up 47 | ``` 48 | 4. Visit http://localhost:3000/session/start/ABCD 49 | 50 | 5. Scan the QR on your console using WhatsApp mobile app -> Linked Device -> Link a Device (it may take time to setup the session) 51 | 52 | 6. Visit http://localhost:3000/client/getContacts/ABCD 53 | 54 | 7. EXTRA: Look at all the callbacks data in `./session/message_log.txt` 55 | 56 | ![Quick Start](./assets/basic_start.gif) 57 | 58 | ## Features 59 | 60 | 1. API and Callbacks 61 | 62 | | Actions | Status | Sessions | Status | Callbacks | Status | 63 | | ----------------------------| ------| ----------------------------------------| ------| ----------------------------------------------| ------| 64 | | Send Image Message | ✅ | Initiate session | ✅ | Callback QR code | ✅ | 65 | | Send Video Message(requires Google Chrome) | ✅ | Terminate session | ✅ | Callback new message | ✅ | 66 | | Send Audio Message | ✅ | Terminate inactive sessions | ✅ | Callback status change | ✅ | 67 | | Send Document Message | ✅ | Terminate all sessions | ✅ | Callback message media attachment | ✅ | 68 | | Send File URL | ✅ | Restart session | ✅ | | | 69 | | Send Contact Message | ✅ | Get session status | ✅ | | | 70 | | Send Poll Message | ✅ | Health Check | ✅ | | | 71 | | Edit Message | ✅ | | | | | 72 | | Set Status | ✅ | | | | | 73 | | Is On Whatsapp? | ✅ | | | | | 74 | | Download Profile Picture | ✅ | | | | | 75 | | User Status | ✅ | | | | | 76 | | Block/Unblock User | ✅ | | | | | 77 | | Update Profile Picture | ✅ | | | | | 78 | | Create Group | ✅ | | | | | 79 | | Leave Group | ✅ | | | | | 80 | | All Groups | ✅ | | | | | 81 | | Invite User | ✅ | | | | | 82 | | Make Admin | ✅ | | | | | 83 | | Demote Admin | ✅ | | | | | 84 | | Group Invite Code | ✅ | | | | | 85 | | Update Group Participants | ✅ | | | | | 86 | | Update Group Setting | ✅ | | | | | 87 | | Update Group Subject | ✅ | | | | | 88 | | Update Group Description | ✅ | | | | | 89 | 90 | 1. Handle multiple client sessions (session data saved locally), identified by unique id 91 | 92 | 2. All endpoints may be secured by a global API key 93 | 94 | 3. On server start, all existing sessions are restored 95 | 96 | 4. Set messages automatically as read 97 | 98 | 5. Disable any of the callbacks 99 | 100 | ## Run Locally 101 | 102 | 1. Clone the repository: 103 | 104 | ```bash 105 | git clone https://github.com/avoylenko/wwebjs-api.git 106 | cd wwebjs-api 107 | ``` 108 | 109 | 2. Install the dependencies: 110 | 111 | ```bash 112 | npm install 113 | ``` 114 | 115 | **Note:** To use the latest edge version of whatsapp-web.js directly from GitHub (with the latest features and fixes), modify the `package.json` dependency: 116 | 117 | ```json 118 | "whatsapp-web.js": "github:pedroslopez/whatsapp-web.js#main" 119 | ``` 120 | 121 | Then run `npm install` again. Be aware that edge versions may contain unstable features. 122 | 123 | 3. Copy the `.env.example` file to `.env` and update the required environment variables: 124 | 125 | ```bash 126 | cp .env.example .env 127 | ``` 128 | 129 | 4. Run the application: 130 | 131 | ```bash 132 | npm run start 133 | ``` 134 | 135 | 5. Access the API at `http://localhost:3000` 136 | 137 | ## Testing 138 | 139 | Run the test suite with the following command: 140 | 141 | ```bash 142 | npm run test 143 | ``` 144 | 145 | ## Documentation 146 | 147 | API documentation can be found in the [`swagger.json`](https://raw.githubusercontent.com/avoylenko/wwebjs-api/main/swagger.json) file. See this file directly into [Swagger Editor](https://editor.swagger.io/?url=https://raw.githubusercontent.com/avoylenko/wwebjs-api/main/swagger.json) or any other OpenAPI-compatible tool to view and interact with the API documentation. 148 | 149 | This documentation is straightforward if you are familiar with whatsapp-web.js library (https://docs.wwebjs.dev/) 150 | If you are still confused - open an issue and I'll improve it. 151 | 152 | 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. 153 | 154 | By default, all callback events are delivered to the webhook defined with the `BASE_WEBHOOK_URL` environment variable. 155 | This can be overridden by setting the `*_WEBHOOK_URL` environment variable, where `*` is your sessionId. 156 | For example, if you have the sessionId defined as `DEMO`, the environment variable must be `DEMO_WEBHOOK_URL`. 157 | 158 | By setting the `DISABLED_CALLBACKS` environment variable you can specify what events you are **not** willing to receive on your webhook. 159 | 160 | By setting the `ENABLE_WEBHOOK` environment to `FALSE` you can disable webhook dispatching. This will help you if you want to switch to websocket method(see below). 161 | 162 | ### Scanning QR code 163 | 164 | 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). 165 | 166 | ### WebSocket mode 167 | The service can dispatch realtime events through websocket connection. By default, the websocket is not activated, so you need manually set the `ENABLE_WEBSOCKET` environment variable to activate it. The server activates a new websocket instance per each active session. The websocket path is `/ws/:sessionId`, where sessionId is your configured session name. The websocket supports ping/pong scheme to keep the socket running. 168 | The below example shows how to receive the events for **test** session. 169 | ``` 170 | const ws = new WebSocket('ws://127.0.0.1:3000/ws/test'); 171 | 172 | ws.on('message', (data) => { 173 | // consume the events 174 | }); 175 | ``` 176 | 177 | ## Deploy to Production 178 | 179 | - Load the docker image in docker-compose, or your Kubernetes environment 180 | - Disable the `ENABLE_LOCAL_CALLBACK_EXAMPLE` environment variable 181 | - Set the `API_KEY` environment variable to protect the REST endpoints 182 | - 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) 183 | 184 | ## Contributing 185 | 186 | Please read [CONTRIBUTING.md](./CONTRIBUTING.md) for details on our code of conduct, and the process for submitting pull requests to us. 187 | 188 | ## Disclaimer 189 | 190 | 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. 191 | 192 | ## License 193 | 194 | This project is licensed under the MIT License - see the [LICENSE.md](./LICENSE.md) file for details. 195 | 196 | ## Star History 197 | 198 | [![Star History Chart](https://api.star-history.com/svg?repos=avoylenko/wwebjs-api&type=Date)](https://star-history.com/#avoylenko/wwebjs-api&Date) 199 | -------------------------------------------------------------------------------- /src/controllers/contactController.js: -------------------------------------------------------------------------------- 1 | const { sessions } = require('../sessions') 2 | const { sendErrorResponse } = require('../utils') 3 | 4 | /** 5 | * Retrieves information about a WhatsApp contact by ID. 6 | * 7 | * @async 8 | * @function 9 | * @param {Object} req - The request object. 10 | * @param {Object} res - The response object. 11 | * @param {string} req.params.sessionId - The ID of the current session. 12 | * @param {string} req.body.contactId - The ID of the contact to retrieve information for. 13 | * @throws {Error} If there is an error retrieving the contact information. 14 | * @returns {Object} The contact information object. 15 | */ 16 | const getClassInfo = async (req, res) => { 17 | /* 18 | #swagger.summary = 'Get the contact' 19 | */ 20 | try { 21 | const { contactId } = req.body 22 | const client = sessions.get(req.params.sessionId) 23 | const contact = await client.getContactById(contactId) 24 | if (!contact) { 25 | throw new Error('Contact not found') 26 | } 27 | res.json({ success: true, contact }) 28 | } catch (error) { 29 | sendErrorResponse(res, 500, error.message) 30 | } 31 | } 32 | 33 | /** 34 | * Blocks a WhatsApp contact by ID. 35 | * 36 | * @async 37 | * @function 38 | * @param {Object} req - The request object. 39 | * @param {Object} res - The response object. 40 | * @param {string} req.params.sessionId - The ID of the current session. 41 | * @param {string} req.body.contactId - The ID of the contact to block. 42 | * @throws {Error} If there is an error blocking the contact. 43 | * @returns {Object} The result of the blocking operation. 44 | */ 45 | const block = async (req, res) => { 46 | /* 47 | #swagger.summary = 'Block contact' 48 | */ 49 | try { 50 | const { contactId } = req.body 51 | const client = sessions.get(req.params.sessionId) 52 | const contact = await client.getContactById(contactId) 53 | if (!contact) { 54 | throw new Error('Contact not found') 55 | } 56 | const result = await contact.block() 57 | res.json({ success: true, result }) 58 | } catch (error) { 59 | sendErrorResponse(res, 500, error.message) 60 | } 61 | } 62 | 63 | /** 64 | * Retrieves the 'About' information of a WhatsApp contact by ID. 65 | * 66 | * @async 67 | * @function 68 | * @param {Object} req - The request object. 69 | * @param {Object} res - The response object. 70 | * @param {string} req.params.sessionId - The ID of the current session. 71 | * @param {string} req.body.contactId - The ID of the contact to retrieve 'About' information for. 72 | * @throws {Error} If there is an error retrieving the contact information. 73 | * @returns {Object} The 'About' information of the contact. 74 | */ 75 | const getAbout = async (req, res) => { 76 | /* 77 | #swagger.summary = "Get the contact's current info" 78 | #swagger.description = "Get the Contact's current 'about' info. Returns null if you don't have permission to read their status." 79 | */ 80 | try { 81 | const { contactId } = req.body 82 | const client = sessions.get(req.params.sessionId) 83 | const contact = await client.getContactById(contactId) 84 | if (!contact) { 85 | throw new Error('Contact not found') 86 | } 87 | const result = await contact.getAbout() 88 | res.json({ success: true, result }) 89 | } catch (error) { 90 | sendErrorResponse(res, 500, error.message) 91 | } 92 | } 93 | 94 | /** 95 | * Retrieves the chat information of a contact with a given contactId. 96 | * 97 | * @async 98 | * @function getChat 99 | * @param {Object} req - The request object. 100 | * @param {Object} res - The response object. 101 | * @param {string} req.params.sessionId - The session ID. 102 | * @param {string} req.body.contactId - The ID of the client whose chat information is being retrieved. 103 | * @throws {Error} If the contact with the given contactId is not found or if there is an error retrieving the chat information. 104 | * @returns {Promise} A promise that resolves with the chat information of the contact. 105 | */ 106 | const getChat = async (req, res) => { 107 | /* 108 | #swagger.summary = 'Get the chat' 109 | #swagger.description = 'Get the chat that corresponds to the contact. Will return null when getting chat for currently logged in user.' 110 | */ 111 | try { 112 | const { contactId } = req.body 113 | const client = sessions.get(req.params.sessionId) 114 | const contact = await client.getContactById(contactId) 115 | if (!contact) { 116 | throw new Error('Contact not found') 117 | } 118 | const result = await contact.getChat() 119 | res.json({ success: true, result }) 120 | } catch (error) { 121 | sendErrorResponse(res, 500, error.message) 122 | } 123 | } 124 | 125 | /** 126 | * Retrieves the formatted number of a contact with a given contactId. 127 | * 128 | * @async 129 | * @function getFormattedNumber 130 | * @param {Object} req - The request object. 131 | * @param {Object} res - The response object. 132 | * @param {string} req.params.sessionId - The session ID. 133 | * @param {string} req.body.contactId - The ID of the client whose chat information is being retrieved. 134 | * @throws {Error} If the contact with the given contactId is not found or if there is an error retrieving the chat information. 135 | * @returns {Promise} A promise that resolves with the formatted number of the contact. 136 | */ 137 | const getFormattedNumber = async (req, res) => { 138 | /* 139 | #swagger.summary = 'Get the formatted phone number' 140 | #swagger.description = "Returns the contact's formatted phone number, (12345678901@c.us) => (+1 (234) 5678-901)." 141 | */ 142 | try { 143 | const { contactId } = req.body 144 | const client = sessions.get(req.params.sessionId) 145 | const contact = await client.getContactById(contactId) 146 | if (!contact) { 147 | throw new Error('Contact not found') 148 | } 149 | const result = await contact.getFormattedNumber() 150 | res.json({ success: true, result }) 151 | } catch (error) { 152 | sendErrorResponse(res, 500, error.message) 153 | } 154 | } 155 | 156 | /** 157 | * Retrieves the country code of a contact with a given contactId. 158 | * 159 | * @async 160 | * @function getCountryCode 161 | * @param {Object} req - The request object. 162 | * @param {Object} res - The response object. 163 | * @param {string} req.params.sessionId - The session ID. 164 | * @param {string} req.body.contactId - The ID of the client whose chat information is being retrieved. 165 | * @throws {Error} If the contact with the given contactId is not found or if there is an error retrieving the chat information. 166 | * @returns {Promise} A promise that resolves with the country code of the contact. 167 | */ 168 | const getCountryCode = async (req, res) => { 169 | /* 170 | #swagger.summary = 'Get the country code' 171 | #swagger.description = "Returns the contact's country code, (1541859685@c.us) => (1)." 172 | */ 173 | try { 174 | const { contactId } = req.body 175 | const client = sessions.get(req.params.sessionId) 176 | const contact = await client.getContactById(contactId) 177 | if (!contact) { 178 | throw new Error('Contact not found') 179 | } 180 | const result = await contact.getCountryCode() 181 | res.json({ success: true, result }) 182 | } catch (error) { 183 | sendErrorResponse(res, 500, error.message) 184 | } 185 | } 186 | 187 | /** 188 | * Retrieves the profile picture url of a contact with a given contactId. 189 | * 190 | * @async 191 | * @function getProfilePicUrl 192 | * @param {Object} req - The request object. 193 | * @param {Object} res - The response object. 194 | * @param {string} req.params.sessionId - The session ID. 195 | * @param {string} req.body.contactId - The ID of the client whose chat information is being retrieved. 196 | * @throws {Error} If the contact with the given contactId is not found or if there is an error retrieving the chat information. 197 | * @returns {Promise} A promise that resolves with the profile picture url of the contact. 198 | */ 199 | const getProfilePicUrl = async (req, res) => { 200 | /* 201 | #swagger.summary = 'Get the profile picture URL' 202 | #swagger.description = "Get the contact's profile picture URL, if privacy settings allow it." 203 | */ 204 | try { 205 | const { contactId } = req.body 206 | const client = sessions.get(req.params.sessionId) 207 | const contact = await client.getContactById(contactId) 208 | if (!contact) { 209 | throw new Error('Contact not found') 210 | } 211 | const result = await contact.getProfilePicUrl() || null 212 | res.json({ success: true, result }) 213 | } catch (error) { 214 | sendErrorResponse(res, 500, error.message) 215 | } 216 | } 217 | 218 | /** 219 | * Unblocks the contact with a given contactId. 220 | * 221 | * @async 222 | * @function unblock 223 | * @param {Object} req - The request object. 224 | * @param {Object} res - The response object. 225 | * @param {string} req.params.sessionId - The session ID. 226 | * @param {string} req.body.contactId - The ID of the client whose contact is being unblocked. 227 | * @throws {Error} If the contact with the given contactId is not found or if there is an error unblocking the contact. 228 | * @returns {Promise} A promise that resolves with the result of unblocking the contact. 229 | */ 230 | const unblock = async (req, res) => { 231 | /* 232 | #swagger.summary = 'Unblock the contact' 233 | #swagger.description = "Unblock the contact from WhatsApp." 234 | */ 235 | try { 236 | const { contactId } = req.body 237 | const client = sessions.get(req.params.sessionId) 238 | const contact = await client.getContactById(contactId) 239 | if (!contact) { 240 | throw new Error('Contact not found') 241 | } 242 | const result = await contact.unblock() 243 | res.json({ success: true, result }) 244 | } catch (error) { 245 | sendErrorResponse(res, 500, error.message) 246 | } 247 | } 248 | 249 | /** 250 | * Gets the contact's common groups with you. 251 | * 252 | * @async 253 | * @function unblock 254 | * @param {Object} req - The request object. 255 | * @param {Object} res - The response object. 256 | * @param {string} req.params.sessionId - The session ID. 257 | * @param {string} req.body.contactId - The ID of the client whose contact is being unblocked. 258 | * @throws {Error} If the contact with the given contactId is not found or if there is an error unblocking the contact. 259 | * @returns {Promise} A promise that resolves with the result of unblocking the contact. 260 | */ 261 | const getCommonGroups = async (req, res) => { 262 | /* 263 | #swagger.summary = "Get the contact's common groups" 264 | #swagger.description = "Get the contact's common groups with you. Returns empty array if you don't have any common group." 265 | */ 266 | try { 267 | const { contactId } = req.body 268 | const client = sessions.get(req.params.sessionId) 269 | const contact = await client.getContactById(contactId) 270 | if (!contact) { 271 | throw new Error('Contact not found') 272 | } 273 | const result = await contact.getCommonGroups() 274 | res.json({ success: true, result }) 275 | } catch (error) { 276 | sendErrorResponse(res, 500, error.message) 277 | } 278 | } 279 | 280 | module.exports = { 281 | getClassInfo, 282 | block, 283 | getAbout, 284 | getChat, 285 | unblock, 286 | getFormattedNumber, 287 | getCountryCode, 288 | getProfilePicUrl, 289 | getCommonGroups 290 | } 291 | -------------------------------------------------------------------------------- /src/controllers/sessionController.js: -------------------------------------------------------------------------------- 1 | const qr = require('qr-image') 2 | const { setupSession, deleteSession, reloadSession, validateSession, flushSessions, destroySession, sessions } = require('../sessions') 3 | const { sendErrorResponse, waitForNestedObject, exposeFunctionIfAbsent } = require('../utils') 4 | const { logger } = require('../logger') 5 | 6 | /** 7 | * Starts a session for the given session ID. 8 | * 9 | * @function 10 | * @async 11 | * @param {Object} req - The HTTP request object. 12 | * @param {Object} res - The HTTP response object. 13 | * @param {string} req.params.sessionId - The session ID to start. 14 | * @returns {Promise} 15 | * @throws {Error} If there was an error starting the session. 16 | */ 17 | const startSession = async (req, res) => { 18 | // #swagger.summary = 'Start new session' 19 | // #swagger.description = 'Starts a session for the given session ID.' 20 | const sessionId = req.params.sessionId 21 | try { 22 | const setupSessionReturn = await setupSession(sessionId) 23 | if (!setupSessionReturn.success) { 24 | /* #swagger.responses[422] = { 25 | description: "Unprocessable Entity.", 26 | content: { 27 | "application/json": { 28 | schema: { "$ref": "#/definitions/ErrorResponse" } 29 | } 30 | } 31 | } 32 | */ 33 | sendErrorResponse(res, 422, setupSessionReturn.message) 34 | return 35 | } 36 | /* #swagger.responses[200] = { 37 | description: "Status of the initiated session.", 38 | content: { 39 | "application/json": { 40 | schema: { "$ref": "#/definitions/StartSessionResponse" } 41 | } 42 | } 43 | } 44 | */ 45 | // wait until the client is created 46 | await waitForNestedObject(setupSessionReturn.client, 'pupPage') 47 | res.json({ success: true, message: setupSessionReturn.message }) 48 | } catch (error) { 49 | logger.error({ sessionId, err: error }, 'Failed to start session') 50 | sendErrorResponse(res, 500, error.message) 51 | } 52 | } 53 | 54 | /** 55 | * Stops a session for the given session ID. 56 | * 57 | * @function 58 | * @async 59 | * @param {Object} req - The HTTP request object. 60 | * @param {Object} res - The HTTP response object. 61 | * @param {string} req.params.sessionId - The session ID to stop. 62 | * @returns {Promise} 63 | * @throws {Error} If there was an error stopping the session. 64 | */ 65 | const stopSession = async (req, res) => { 66 | // #swagger.summary = 'Stop session' 67 | // #swagger.description = 'Stops a session for the given session ID.' 68 | const sessionId = req.params.sessionId 69 | try { 70 | await destroySession(sessionId) 71 | /* #swagger.responses[200] = { 72 | description: "Status of the stopped session.", 73 | content: { 74 | "application/json": { 75 | schema: { "$ref": "#/definitions/StopSessionResponse" } 76 | } 77 | } 78 | } 79 | */ 80 | res.json({ success: true, message: 'Session stopped successfully' }) 81 | } catch (error) { 82 | logger.error({ sessionId, err: error }, 'Failed to stop session') 83 | sendErrorResponse(res, 500, error.message) 84 | } 85 | } 86 | 87 | /** 88 | * Status of the session with the given session ID. 89 | * 90 | * @function 91 | * @async 92 | * @param {Object} req - The HTTP request object. 93 | * @param {Object} res - The HTTP response object. 94 | * @param {string} req.params.sessionId - The session ID to start. 95 | * @returns {Promise} 96 | * @throws {Error} If there was an error getting status of the session. 97 | */ 98 | const statusSession = async (req, res) => { 99 | // #swagger.summary = 'Get session status' 100 | // #swagger.description = 'Status of the session with the given session ID.' 101 | const sessionId = req.params.sessionId 102 | try { 103 | const sessionData = await validateSession(sessionId) 104 | /* #swagger.responses[200] = { 105 | description: "Status of the session.", 106 | content: { 107 | "application/json": { 108 | schema: { "$ref": "#/definitions/StatusSessionResponse" } 109 | } 110 | } 111 | } 112 | */ 113 | res.json(sessionData) 114 | } catch (error) { 115 | logger.error({ sessionId, err: error }, 'Failed to get session status') 116 | sendErrorResponse(res, 500, error.message) 117 | } 118 | } 119 | 120 | /** 121 | * QR code of the session with the given session ID. 122 | * 123 | * @function 124 | * @async 125 | * @param {Object} req - The HTTP request object. 126 | * @param {Object} res - The HTTP response object. 127 | * @param {string} req.params.sessionId - The session ID to start. 128 | * @returns {Promise} 129 | * @throws {Error} If there was an error getting status of the session. 130 | */ 131 | const sessionQrCode = async (req, res) => { 132 | // #swagger.summary = 'Get session QR code' 133 | // #swagger.description = 'QR code of the session with the given session ID.' 134 | const sessionId = req.params.sessionId 135 | try { 136 | const session = sessions.get(sessionId) 137 | if (!session) { 138 | return res.json({ success: false, message: 'session_not_found' }) 139 | } 140 | if (session.qr) { 141 | res.setHeader('Cache-Control', 'no-cache, no-store, must-revalidate') 142 | res.setHeader('Expires', 0) 143 | return res.json({ success: true, qr: session.qr }) 144 | } 145 | return res.json({ success: false, message: 'qr code not ready or already scanned' }) 146 | } catch (error) { 147 | logger.error({ sessionId, err: error }, 'Failed to get session qr code') 148 | sendErrorResponse(res, 500, error.message) 149 | } 150 | } 151 | 152 | /** 153 | * QR code as image of the session with the given session ID. 154 | * 155 | * @function 156 | * @async 157 | * @param {Object} req - The HTTP request object. 158 | * @param {Object} res - The HTTP response object. 159 | * @param {string} req.params.sessionId - The session ID to start. 160 | * @returns {Promise} 161 | * @throws {Error} If there was an error getting status of the session. 162 | */ 163 | const sessionQrCodeImage = async (req, res) => { 164 | // #swagger.summary = 'Get session QR code as image' 165 | // #swagger.description = 'QR code as image of the session with the given session ID.' 166 | const sessionId = req.params.sessionId 167 | try { 168 | const session = sessions.get(sessionId) 169 | if (!session) { 170 | return res.json({ success: false, message: 'session_not_found' }) 171 | } 172 | if (session.qr) { 173 | const qrImage = qr.image(session.qr) 174 | /* #swagger.responses[200] = { 175 | description: "QR image.", 176 | content: { 177 | "image/png": {} 178 | } 179 | } 180 | */ 181 | res.writeHead(200, { 182 | 'Cache-Control': 'no-cache, no-store, must-revalidate', 183 | Expires: 0, 184 | 'Content-Type': 'image/png' 185 | }) 186 | return qrImage.pipe(res) 187 | } 188 | return res.json({ success: false, message: 'qr code not ready or already scanned' }) 189 | } catch (error) { 190 | logger.error({ sessionId, err: error }, 'Failed to get session qr code image') 191 | sendErrorResponse(res, 500, error.message) 192 | } 193 | } 194 | 195 | /** 196 | * Restarts the session with the given session ID. 197 | * 198 | * @function 199 | * @async 200 | * @param {Object} req - The HTTP request object. 201 | * @param {Object} res - The HTTP response object. 202 | * @param {string} req.params.sessionId - The session ID to terminate. 203 | * @returns {Promise} 204 | * @throws {Error} If there was an error terminating the session. 205 | */ 206 | const restartSession = async (req, res) => { 207 | // #swagger.summary = 'Restart session' 208 | // #swagger.description = 'Restarts the session with the given session ID.' 209 | const sessionId = req.params.sessionId 210 | try { 211 | const validation = await validateSession(sessionId) 212 | if (validation.message === 'session_not_found') { 213 | return res.json(validation) 214 | } 215 | await reloadSession(sessionId) 216 | /* #swagger.responses[200] = { 217 | description: "Sessions restarted.", 218 | content: { 219 | "application/json": { 220 | schema: { "$ref": "#/definitions/RestartSessionResponse" } 221 | } 222 | } 223 | } 224 | */ 225 | res.json({ success: true, message: 'Restarted successfully' }) 226 | } catch (error) { 227 | logger.error({ sessionId, err: error }, 'Failed to restart session') 228 | sendErrorResponse(res, 500, error.message) 229 | } 230 | } 231 | 232 | /** 233 | * Terminates the session with the given session ID. 234 | * 235 | * @function 236 | * @async 237 | * @param {Object} req - The HTTP request object. 238 | * @param {Object} res - The HTTP response object. 239 | * @param {string} req.params.sessionId - The session ID to terminate. 240 | * @returns {Promise} 241 | * @throws {Error} If there was an error terminating the session. 242 | */ 243 | const terminateSession = async (req, res) => { 244 | // #swagger.summary = 'Terminate session' 245 | // #swagger.description = 'Terminates the session with the given session ID.' 246 | const sessionId = req.params.sessionId 247 | try { 248 | const validation = await validateSession(sessionId) 249 | if (validation.message === 'session_not_found') { 250 | return res.json(validation) 251 | } 252 | await deleteSession(sessionId, validation) 253 | /* #swagger.responses[200] = { 254 | description: "Sessions terminated.", 255 | content: { 256 | "application/json": { 257 | schema: { "$ref": "#/definitions/TerminateSessionResponse" } 258 | } 259 | } 260 | } 261 | */ 262 | res.json({ success: true, message: 'Logged out successfully' }) 263 | } catch (error) { 264 | logger.error({ sessionId, err: error }, 'Failed to terminate session') 265 | sendErrorResponse(res, 500, error.message) 266 | } 267 | } 268 | 269 | /** 270 | * Terminates all inactive sessions. 271 | * 272 | * @function 273 | * @async 274 | * @param {Object} req - The HTTP request object. 275 | * @param {Object} res - The HTTP response object. 276 | * @returns {Promise} 277 | * @throws {Error} If there was an error terminating the sessions. 278 | */ 279 | const terminateInactiveSessions = async (req, res) => { 280 | // #swagger.summary = 'Terminate inactive sessions' 281 | // #swagger.description = 'Terminates all inactive sessions.' 282 | try { 283 | await flushSessions(true) 284 | /* #swagger.responses[200] = { 285 | description: "Sessions terminated.", 286 | content: { 287 | "application/json": { 288 | schema: { "$ref": "#/definitions/TerminateSessionsResponse" } 289 | } 290 | } 291 | } 292 | */ 293 | res.json({ success: true, message: 'Flush completed successfully' }) 294 | } catch (error) { 295 | logger.error(error, 'Failed to terminate inactive sessions') 296 | sendErrorResponse(res, 500, error.message) 297 | } 298 | } 299 | 300 | /** 301 | * Terminates all sessions. 302 | * 303 | * @function 304 | * @async 305 | * @param {Object} req - The HTTP request object. 306 | * @param {Object} res - The HTTP response object. 307 | * @returns {Promise} 308 | * @throws {Error} If there was an error terminating the sessions. 309 | */ 310 | const terminateAllSessions = async (req, res) => { 311 | // #swagger.summary = 'Terminate all sessions' 312 | // #swagger.description = 'Terminates all sessions.' 313 | try { 314 | await flushSessions(false) 315 | /* #swagger.responses[200] = { 316 | description: "Sessions terminated.", 317 | content: { 318 | "application/json": { 319 | schema: { "$ref": "#/definitions/TerminateSessionsResponse" } 320 | } 321 | } 322 | } 323 | */ 324 | res.json({ success: true, message: 'Flush completed successfully' }) 325 | } catch (error) { 326 | logger.error(error, 'Failed to terminate all sessions') 327 | sendErrorResponse(res, 500, error.message) 328 | } 329 | } 330 | 331 | /** 332 | * Request authentication via pairing code instead of QR code. 333 | * 334 | * @async 335 | * @function 336 | * @param {Object} req - The HTTP request object containing the chatId and sessionId. 337 | * @param {string} req.body.phoneNumber - The phone number in international, symbol-free format (e.g. 12025550108 for US, 551155501234 for Brazil). 338 | * @param {boolean} req.body.showNotification - Show notification to pair on phone number. 339 | * @param {string} req.params.sessionId - The unique identifier of the session associated with the client to use. 340 | * @param {Object} res - The HTTP response object. 341 | * @returns {Promise} - A Promise that resolves with a JSON object containing a success flag and the result of the operation. 342 | * @throws {Error} - If an error occurs during the operation, it is thrown and handled by the catch block. 343 | */ 344 | const requestPairingCode = async (req, res) => { 345 | /* 346 | #swagger.summary = 'Request authentication via pairing code' 347 | #swagger.requestBody = { 348 | required: true, 349 | schema: { 350 | type: 'object', 351 | properties: { 352 | phoneNumber: { 353 | type: 'string', 354 | description: 'Phone number in international, symbol-free format', 355 | example: '12025550108' 356 | }, 357 | showNotification: { 358 | type: 'boolean', 359 | description: 'Show notification to pair on phone number', 360 | example: true 361 | }, 362 | } 363 | }, 364 | } 365 | */ 366 | try { 367 | const { phoneNumber, showNotification = true } = req.body 368 | const client = sessions.get(req.params.sessionId) 369 | if (!client) { 370 | return res.json({ success: false, message: 'session_not_found' }) 371 | } 372 | // hotfix https://github.com/pedroslopez/whatsapp-web.js/pull/3706 373 | await exposeFunctionIfAbsent(client.pupPage, 'onCodeReceivedEvent', async (code) => { 374 | client.emit('code', code) 375 | return code 376 | }) 377 | const result = await client.requestPairingCode(phoneNumber, showNotification) 378 | res.json({ success: true, result }) 379 | } catch (error) { 380 | sendErrorResponse(res, 500, error.message) 381 | } 382 | } 383 | 384 | /** 385 | * Get all sessions. 386 | * 387 | * @function 388 | * @async 389 | * @param {Object} req - The HTTP request object. 390 | * @param {Object} res - The HTTP response object. 391 | * @returns {} 392 | */ 393 | const getSessions = async (req, res) => { 394 | // #swagger.summary = 'Get all sessions' 395 | // #swagger.description = 'Get all sessions.' 396 | /* #swagger.responses[200] = { 397 | description: "Retrieved all sessions.", 398 | content: { 399 | "application/json": { 400 | schema: { "$ref": "#/definitions/GetSessionsResponse" } 401 | } 402 | } 403 | } 404 | */ 405 | return res.json({ success: true, result: Array.from(sessions.keys()) }) 406 | } 407 | 408 | /** 409 | * Get pupPage screenshot image 410 | * 411 | * @function 412 | * @async 413 | * @param {Object} req - The request object. 414 | * @param {Object} res - The response object. 415 | * @returns {Promise} - A Promise that resolves with a JSON object containing a success flag and the result of the operation. 416 | * @throws {Error} If there is an issue setting the profile picture, an error will be thrown. 417 | */ 418 | const getPageScreenshot = async (req, res) => { 419 | // #swagger.summary = 'Get page screenshot' 420 | // #swagger.description = 'Screenshot of the client with the given session ID.' 421 | const sessionId = req.params.sessionId 422 | try { 423 | const session = sessions.get(sessionId) 424 | if (!session) { 425 | return res.json({ success: false, message: 'session_not_found' }) 426 | } 427 | 428 | if (!session.pupPage) { 429 | return res.json({ success: false, message: 'page_not_ready' }) 430 | } 431 | 432 | const pngBase64String = await session.pupPage.screenshot({ 433 | fullPage: true, 434 | encoding: 'base64', 435 | type: 'png' 436 | }) 437 | 438 | /* #swagger.responses[200] = { 439 | description: "Screenshot image.", 440 | content: { 441 | "image/png": {} 442 | } 443 | } 444 | */ 445 | res.writeHead(200, { 446 | 'Content-Type': 'image/png' 447 | }) 448 | res.write(Buffer.from(pngBase64String, 'base64')) 449 | res.end() 450 | } catch (error) { 451 | logger.error({ sessionId, err: error }, 'Failed to get page screenshot') 452 | sendErrorResponse(res, 500, error.message) 453 | } 454 | } 455 | 456 | module.exports = { 457 | startSession, 458 | stopSession, 459 | statusSession, 460 | sessionQrCode, 461 | sessionQrCodeImage, 462 | requestPairingCode, 463 | restartSession, 464 | terminateSession, 465 | terminateInactiveSessions, 466 | terminateAllSessions, 467 | getSessions, 468 | getPageScreenshot 469 | } 470 | -------------------------------------------------------------------------------- /src/controllers/chatController.js: -------------------------------------------------------------------------------- 1 | const { sessions } = require('../sessions') 2 | const { sendErrorResponse } = require('../utils') 3 | 4 | /** 5 | * @function 6 | * @async 7 | * @name getClassInfo 8 | * @description Gets information about a chat using the chatId and sessionId 9 | * @param {Object} req - Request object 10 | * @param {Object} res - Response object 11 | * @param {string} req.body.chatId - The ID of the chat to get information for 12 | * @param {string} req.params.sessionId - The ID of the session to use 13 | * @returns {Object} - Returns a JSON object with the success status and chat information 14 | * @throws {Error} - Throws an error if chat is not found or if there is a server error 15 | */ 16 | const getClassInfo = async (req, res) => { 17 | /* 18 | #swagger.summary = 'Get the chat' 19 | */ 20 | try { 21 | const { chatId } = req.body 22 | const client = sessions.get(req.params.sessionId) 23 | const chat = await client.getChatById(chatId) 24 | if (!chat) { 25 | sendErrorResponse(res, 404, 'Chat not Found') 26 | return 27 | } 28 | res.json({ success: true, chat }) 29 | } catch (error) { 30 | sendErrorResponse(res, 500, error.message) 31 | } 32 | } 33 | 34 | /** 35 | * Clears all messages in a chat. 36 | * 37 | * @function 38 | * @async 39 | * @param {Object} req - The request object. 40 | * @param {Object} res - The response object. 41 | * @param {string} req.params.sessionId - The ID of the session. 42 | * @param {string} req.body.chatId - The ID of the chat to clear messages from. 43 | * @throws {Error} If the chat is not found or there is an internal server error. 44 | * @returns {Object} The success status and the cleared messages. 45 | */ 46 | const clearMessages = async (req, res) => { 47 | /* 48 | #swagger.summary = 'Clear all messages from the chat' 49 | */ 50 | try { 51 | const { chatId } = req.body 52 | const client = sessions.get(req.params.sessionId) 53 | const chat = await client.getChatById(chatId) 54 | if (!chat) { 55 | sendErrorResponse(res, 404, 'Chat not Found') 56 | return 57 | } 58 | const result = await chat.clearMessages() 59 | res.json({ success: true, result }) 60 | } catch (error) { 61 | sendErrorResponse(res, 500, error.message) 62 | } 63 | } 64 | 65 | /** 66 | * Stops typing or recording in chat immediately. 67 | * 68 | * @function 69 | * @async 70 | * @param {Object} req - Request object. 71 | * @param {Object} res - Response object. 72 | * @param {string} req.body.chatId - ID of the chat to clear the state for. 73 | * @param {string} req.params.sessionId - ID of the session the chat belongs to. 74 | * @returns {Promise} - A Promise that resolves with a JSON object containing a success flag and the result of clearing the state. 75 | * @throws {Error} - If there was an error while clearing the state. 76 | */ 77 | const clearState = async (req, res) => { 78 | /* 79 | #swagger.summary = 'Stop typing or recording in chat immediately' 80 | */ 81 | try { 82 | const { chatId } = req.body 83 | const client = sessions.get(req.params.sessionId) 84 | const chat = await client.getChatById(chatId) 85 | if (!chat) { 86 | sendErrorResponse(res, 404, 'Chat not Found') 87 | return 88 | } 89 | const result = await chat.clearState() 90 | res.json({ success: true, result }) 91 | } catch (error) { 92 | sendErrorResponse(res, 500, error.message) 93 | } 94 | } 95 | 96 | /** 97 | * Delete a chat. 98 | * 99 | * @async 100 | * @function 101 | * @param {Object} req - The request object. 102 | * @param {Object} res - The response object. 103 | * @param {string} req.params.sessionId - The session ID. 104 | * @param {string} req.body.chatId - The ID of the chat to be deleted. 105 | * @returns {Object} A JSON response indicating whether the chat was deleted successfully. 106 | * @throws {Object} If there is an error while deleting the chat, an error response is sent with a status code of 500. 107 | * @throws {Object} If the chat is not found, an error response is sent with a status code of 404. 108 | */ 109 | const deleteChat = async (req, res) => { 110 | /* 111 | #swagger.summary = 'Delete the chat' 112 | */ 113 | try { 114 | const { chatId } = req.body 115 | const client = sessions.get(req.params.sessionId) 116 | const chat = await client.getChatById(chatId) 117 | if (!chat) { 118 | sendErrorResponse(res, 404, 'Chat not Found') 119 | return 120 | } 121 | const result = await chat.delete() 122 | res.json({ success: true, result }) 123 | } catch (error) { 124 | sendErrorResponse(res, 500, error.message) 125 | } 126 | } 127 | 128 | /** 129 | * Fetches messages from a specified chat. 130 | * 131 | * @function 132 | * @async 133 | * 134 | * @param {Object} req - The request object containing sessionId, chatId, and searchOptions. 135 | * @param {string} req.params.sessionId - The ID of the session associated with the chat. 136 | * @param {Object} req.body - The body of the request containing chatId and searchOptions. 137 | * @param {string} req.body.chatId - The ID of the chat from which to fetch messages. 138 | * @param {Object} req.body.searchOptions - The search options to use when fetching messages. 139 | * 140 | * @param {Object} res - The response object to send the fetched messages. 141 | * @returns {Promise} A JSON object containing the success status and fetched messages. 142 | * 143 | * @throws {Error} If the chat is not found or there is an error fetching messages. 144 | */ 145 | const fetchMessages = async (req, res) => { 146 | try { 147 | /* 148 | #swagger.summary = 'Load chat messages' 149 | #swagger.description = 'Messages sorted from earliest to latest' 150 | #swagger.requestBody = { 151 | required: true, 152 | schema: { 153 | type: 'object', 154 | properties: { 155 | chatId: { 156 | type: 'string', 157 | description: 'Unique WhatsApp identifier for the given Chat (either group or personal)', 158 | example: '6281288888888@c.us' 159 | }, 160 | searchOptions: { 161 | type: 'object', 162 | description: 'Search options for fetching messages', 163 | example: { limit: 10, fromMe: true } 164 | } 165 | } 166 | } 167 | } 168 | */ 169 | const { chatId, searchOptions = {} } = req.body 170 | const client = sessions.get(req.params.sessionId) 171 | const chat = await client.getChatById(chatId) 172 | if (!chat) { 173 | sendErrorResponse(res, 404, 'Chat not Found') 174 | return 175 | } 176 | const messages = Object.keys(searchOptions).length ? await chat.fetchMessages(searchOptions) : await chat.fetchMessages() 177 | res.json({ success: true, messages }) 178 | } catch (error) { 179 | sendErrorResponse(res, 500, error.message) 180 | } 181 | } 182 | 183 | /** 184 | * Gets the contact for a chat 185 | * @async 186 | * @function 187 | * @param {Object} req - The HTTP request object 188 | * @param {Object} res - The HTTP response object 189 | * @param {string} req.params.sessionId - The ID of the current session 190 | * @param {string} req.body.chatId - The ID of the chat to get the contact for 191 | * @returns {Promise} - Promise that resolves with the chat's contact information 192 | * @throws {Error} - Throws an error if chat is not found or if there is an error getting the contact information 193 | */ 194 | const getContact = async (req, res) => { 195 | /* 196 | #swagger.summary = 'Get the contact' 197 | */ 198 | try { 199 | const { chatId } = req.body 200 | const client = sessions.get(req.params.sessionId) 201 | const chat = await client.getChatById(chatId) 202 | if (!chat) { 203 | sendErrorResponse(res, 404, 'Chat not Found') 204 | return 205 | } 206 | const contact = await chat.getContact() 207 | res.json({ success: true, contact }) 208 | } catch (error) { 209 | sendErrorResponse(res, 500, error.message) 210 | } 211 | } 212 | 213 | /** 214 | * Send a recording state to a WhatsApp chat. 215 | * @async 216 | * @function 217 | * @param {object} req - The request object. 218 | * @param {object} res - The response object. 219 | * @param {string} req.params.sessionId - The session ID. 220 | * @param {object} req.body - The request body. 221 | * @param {string} req.body.chatId - The ID of the chat to send the recording state to. 222 | * @returns {object} - An object containing a success message and the result of the sendStateRecording method. 223 | * @throws {object} - An error object containing a status code and error message if an error occurs. 224 | */ 225 | const sendStateRecording = async (req, res) => { 226 | /* 227 | #swagger.summary = 'Simulate recording audio' 228 | #swagger.description = 'Simulate recording audio in chat. This will last for 25 seconds' 229 | */ 230 | try { 231 | const { chatId } = req.body 232 | const client = sessions.get(req.params.sessionId) 233 | const chat = await client.getChatById(chatId) 234 | if (!chat) { 235 | sendErrorResponse(res, 404, 'Chat not Found') 236 | return 237 | } 238 | const result = await chat.sendStateRecording() 239 | res.json({ success: true, result }) 240 | } catch (error) { 241 | sendErrorResponse(res, 500, error.message) 242 | } 243 | } 244 | 245 | /** 246 | * Send a typing state to a WhatsApp chat. 247 | * @async 248 | * @function 249 | * @param {object} req - The request object. 250 | * @param {object} res - The response object. 251 | * @param {string} req.params.sessionId - The session ID. 252 | * @param {object} req.body - The request body. 253 | * @param {string} req.body.chatId - The ID of the chat to send the typing state to. 254 | * @returns {object} - An object containing a success message and the result of the sendStateTyping method. 255 | * @throws {object} - An error object containing a status code and error message if an error occurs. 256 | */ 257 | const sendStateTyping = async (req, res) => { 258 | /* 259 | #swagger.summary = 'Simulate typing in chat' 260 | #swagger.description = 'Simulate typing in chat. This will last for 25 seconds.' 261 | */ 262 | try { 263 | const { chatId } = req.body 264 | const client = sessions.get(req.params.sessionId) 265 | const chat = await client.getChatById(chatId) 266 | if (!chat) { 267 | sendErrorResponse(res, 404, 'Chat not Found') 268 | return 269 | } 270 | const result = await chat.sendStateTyping() 271 | res.json({ success: true, result }) 272 | } catch (error) { 273 | sendErrorResponse(res, 500, error.message) 274 | } 275 | } 276 | 277 | /** 278 | * Send a seen state to a WhatsApp chat. 279 | * @async 280 | * @function 281 | * @param {object} req - The request object. 282 | * @param {object} res - The response object. 283 | * @param {string} req.params.sessionId - The session ID. 284 | * @param {object} req.body - The request body. 285 | * @param {string} req.body.chatId - The ID of the chat to send the typing state to. 286 | * @returns {object} - An object containing a success message and the result of the sendStateTyping method. 287 | * @throws {object} - An error object containing a status code and error message if an error occurs. 288 | */ 289 | const sendSeen = async (req, res) => { 290 | /* 291 | #swagger.summary = 'Set the message as seen' 292 | */ 293 | try { 294 | const { chatId } = req.body 295 | const client = sessions.get(req.params.sessionId) 296 | const chat = await client.getChatById(chatId) 297 | if (!chat) { 298 | sendErrorResponse(res, 404, 'Chat not Found') 299 | return 300 | } 301 | const result = await chat.sendSeen() 302 | res.json({ success: true, result }) 303 | } catch (error) { 304 | sendErrorResponse(res, 500, error.message) 305 | } 306 | } 307 | 308 | /** 309 | * Mark this chat as unread. 310 | * @async 311 | * @function 312 | * @param {object} req - The request object. 313 | * @param {object} res - The response object. 314 | * @param {string} req.params.sessionId - The session ID. 315 | * @param {object} req.body - The request body. 316 | * @param {string} req.body.chatId - The ID of the chat to send the typing state to. 317 | * @returns {object} - An object containing a success message and the result of the sendStateTyping method. 318 | * @throws {object} - An error object containing a status code and error message if an error occurs. 319 | */ 320 | const markUnread = async (req, res) => { 321 | /* 322 | #swagger.summary = 'Mark this chat as unread' 323 | */ 324 | try { 325 | const { chatId } = req.body 326 | const client = sessions.get(req.params.sessionId) 327 | const chat = await client.getChatById(chatId) 328 | if (!chat) { 329 | sendErrorResponse(res, 404, 'Chat not Found') 330 | return 331 | } 332 | const result = await chat.markUnread() 333 | res.json({ success: true, result }) 334 | } catch (error) { 335 | sendErrorResponse(res, 500, error.message) 336 | } 337 | } 338 | 339 | /** 340 | * Sync history. 341 | * @async 342 | * @function 343 | * @param {object} req - The request object. 344 | * @param {object} res - The response object. 345 | * @param {string} req.params.sessionId - The session ID. 346 | * @param {object} req.body - The request body. 347 | * @param {string} req.body.chatId - The ID of the chat to send the typing state to. 348 | * @returns {object} - An object containing a success message and the result of the sendStateTyping method. 349 | * @throws {object} - An error object containing a status code and error message if an error occurs. 350 | */ 351 | const syncHistory = async (req, res) => { 352 | /* 353 | #swagger.summary = 'Sync chat history' 354 | */ 355 | try { 356 | const { chatId } = req.body 357 | const client = sessions.get(req.params.sessionId) 358 | const chat = await client.getChatById(chatId) 359 | if (!chat) { 360 | sendErrorResponse(res, 404, 'Chat not Found') 361 | return 362 | } 363 | const result = await chat.syncHistory() 364 | res.json({ success: true, result }) 365 | } catch (error) { 366 | sendErrorResponse(res, 500, error.message) 367 | } 368 | } 369 | 370 | /** 371 | * Return array of all labels. 372 | * @async 373 | * @function 374 | * @param {object} req - The request object. 375 | * @param {object} res - The response object. 376 | * @param {string} req.params.sessionId - The session ID. 377 | * @param {object} req.body - The request body. 378 | * @param {string} req.body.chatId - The ID of the chat to send the typing state to. 379 | * @returns {object} - An object containing a success message and the result of the sendStateTyping method. 380 | * @throws {object} - An error object containing a status code and error message if an error occurs. 381 | */ 382 | const getLabels = async (req, res) => { 383 | /* 384 | #swagger.summary = 'Return all labels' 385 | #swagger.description = 'Return array of all labels assigned to this chat' 386 | */ 387 | try { 388 | const { chatId } = req.body 389 | const client = sessions.get(req.params.sessionId) 390 | const chat = await client.getChatById(chatId) 391 | if (!chat) { 392 | sendErrorResponse(res, 404, 'Chat not Found') 393 | return 394 | } 395 | const labels = await chat.getLabels() 396 | res.json({ success: true, labels }) 397 | } catch (error) { 398 | sendErrorResponse(res, 500, error.message) 399 | } 400 | } 401 | 402 | /** 403 | * Add or remove labels. 404 | * @async 405 | * @function 406 | * @param {object} req - The request object. 407 | * @param {object} res - The response object. 408 | * @param {string} req.params.sessionId - The session ID. 409 | * @param {object} req.body - The request body. 410 | * @param {string} req.body.chatId - The ID of the chat to send the typing state to. 411 | * @returns {object} - An object containing a success message and the result of the sendStateTyping method. 412 | * @throws {object} - An error object containing a status code and error message if an error occurs. 413 | */ 414 | const changeLabels = async (req, res) => { 415 | /* 416 | #swagger.summary = 'Add or remove labels' 417 | #swagger.requestBody = { 418 | required: true, 419 | schema: { 420 | type: 'object', 421 | properties: { 422 | chatId: { 423 | type: 'string', 424 | description: 'Unique WhatsApp identifier for the given Chat (either group or personal)', 425 | example: '6281288888888@c.us' 426 | }, 427 | labelIds: { 428 | type: 'array', 429 | description: 'Array of (number or string)', 430 | example: [0, 1] 431 | } 432 | } 433 | } 434 | } 435 | */ 436 | try { 437 | const { chatId, labelIds } = req.body 438 | const client = sessions.get(req.params.sessionId) 439 | const chat = await client.getChatById(chatId) 440 | if (!chat) { 441 | sendErrorResponse(res, 404, 'Chat not Found') 442 | return 443 | } 444 | await chat.changeLabels(labelIds) 445 | res.json({ success: true }) 446 | } catch (error) { 447 | sendErrorResponse(res, 500, error.message) 448 | } 449 | } 450 | 451 | /** 452 | * Executes a method on the chat associated with the given sessionId. 453 | * 454 | * @async 455 | * @function 456 | * @param {Object} req - The HTTP request object containing the chatId and sessionId. 457 | * @param {string} req.body.chatId - The unique identifier of the chat. 458 | * @param {string} req.params.sessionId - The unique identifier of the session associated with the client to use. 459 | * @param {Object} res - The HTTP response object. 460 | * @returns {Promise} - A Promise that resolves with a JSON object containing a success flag and the result of the operation. 461 | * @throws {Error} - If an error occurs during the operation, it is thrown and handled by the catch block. 462 | */ 463 | const runMethod = async (req, res) => { 464 | /* 465 | #swagger.summary = 'Execute a method on the chat' 466 | #swagger.description = 'Execute a method on the chat and return the result' 467 | #swagger.requestBody = { 468 | required: true, 469 | schema: { 470 | type: 'object', 471 | properties: { 472 | chatId: { 473 | type: 'string', 474 | description: 'The chat id which contains the message', 475 | example: '6281288888888@c.us' 476 | }, 477 | method: { 478 | type: 'string', 479 | description: 'The name of the method to execute', 480 | example: 'getLabels' 481 | }, 482 | options: { 483 | anyOf: [ 484 | { type: 'object' }, 485 | { type: 'string' } 486 | ], 487 | description: 'The options to pass to the method', 488 | } 489 | } 490 | }, 491 | } 492 | */ 493 | try { 494 | const { chatId, method, options } = req.body 495 | const client = sessions.get(req.params.sessionId) 496 | const chat = await client.getChatById(chatId) 497 | if (!chat) { 498 | sendErrorResponse(res, 404, 'Chat not Found') 499 | return 500 | } 501 | if (typeof chat[method] !== 'function') { 502 | throw new Error('Method is not implemented') 503 | } 504 | const result = options ? await chat[method](options) : await chat[method]() 505 | res.json({ success: true, data: result }) 506 | } catch (error) { 507 | sendErrorResponse(res, 500, error.message) 508 | } 509 | } 510 | 511 | module.exports = { 512 | getClassInfo, 513 | clearMessages, 514 | clearState, 515 | deleteChat, 516 | fetchMessages, 517 | getContact, 518 | sendStateRecording, 519 | sendStateTyping, 520 | sendSeen, 521 | markUnread, 522 | syncHistory, 523 | getLabels, 524 | changeLabels, 525 | runMethod 526 | } 527 | -------------------------------------------------------------------------------- /src/sessions.js: -------------------------------------------------------------------------------- 1 | const { Client, LocalAuth } = require('whatsapp-web.js') 2 | const fs = require('fs') 3 | const path = require('path') 4 | const sessions = new Map() 5 | const { baseWebhookURL, sessionFolderPath, maxAttachmentSize, setMessagesAsSeen, webVersion, webVersionCacheType, recoverSessions, chromeBin, headless, releaseBrowserLock } = require('./config') 6 | const { triggerWebhook, waitForNestedObject, isEventEnabled, sendMessageSeenStatus, sleep, patchWWebLibrary } = require('./utils') 7 | const { logger } = require('./logger') 8 | const { initWebSocketServer, terminateWebSocketServer, triggerWebSocket } = require('./websocket') 9 | 10 | // Function to validate if the session is ready 11 | const validateSession = async (sessionId) => { 12 | try { 13 | const returnData = { success: false, state: null, message: '' } 14 | 15 | // Session not Connected 😢 16 | if (!sessions.has(sessionId) || !sessions.get(sessionId)) { 17 | returnData.message = 'session_not_found' 18 | return returnData 19 | } 20 | 21 | const client = sessions.get(sessionId) 22 | // wait until the client is created 23 | await waitForNestedObject(client, 'pupPage') 24 | .catch((err) => { return { success: false, state: null, message: err.message } }) 25 | 26 | // Wait for client.pupPage to be evaluable 27 | let maxRetry = 0 28 | while (true) { 29 | try { 30 | if (client.pupPage.isClosed()) { 31 | return { success: false, state: null, message: 'browser tab closed' } 32 | } 33 | await Promise.race([ 34 | client.pupPage.evaluate('1'), 35 | new Promise(resolve => setTimeout(resolve, 1000)) 36 | ]) 37 | break 38 | } catch (error) { 39 | if (maxRetry === 2) { 40 | return { success: false, state: null, message: 'session closed' } 41 | } 42 | maxRetry++ 43 | } 44 | } 45 | 46 | const state = await client.getState() 47 | returnData.state = state 48 | if (state !== 'CONNECTED') { 49 | returnData.message = 'session_not_connected' 50 | return returnData 51 | } 52 | 53 | // Session Connected 🎉 54 | returnData.success = true 55 | returnData.message = 'session_connected' 56 | return returnData 57 | } catch (error) { 58 | logger.error({ sessionId, err: error }, 'Failed to validate session') 59 | return { success: false, state: null, message: error.message } 60 | } 61 | } 62 | 63 | // Function to handle client session restoration 64 | const restoreSessions = () => { 65 | try { 66 | if (!fs.existsSync(sessionFolderPath)) { 67 | fs.mkdirSync(sessionFolderPath) // Create the session directory if it doesn't exist 68 | } 69 | // Read the contents of the folder 70 | fs.readdir(sessionFolderPath, async (_, files) => { 71 | // Iterate through the files in the parent folder 72 | for (const file of files) { 73 | // Use regular expression to extract the string from the folder name 74 | const match = file.match(/^session-(.+)$/) 75 | if (match) { 76 | const sessionId = match[1] 77 | logger.warn({ sessionId }, 'Existing session detected') 78 | await setupSession(sessionId) 79 | } 80 | } 81 | }) 82 | } catch (error) { 83 | logger.error(error, 'Failed to restore sessions') 84 | } 85 | } 86 | 87 | // Setup Session 88 | const setupSession = async (sessionId) => { 89 | try { 90 | if (sessions.has(sessionId)) { 91 | return { success: false, message: `Session already exists for: ${sessionId}`, client: sessions.get(sessionId) } 92 | } 93 | logger.info({ sessionId }, 'Session is being initiated') 94 | // Disable the delete folder from the logout function (will be handled separately) 95 | const localAuth = new LocalAuth({ clientId: sessionId, dataPath: sessionFolderPath }) 96 | delete localAuth.logout 97 | localAuth.logout = () => { } 98 | 99 | const clientOptions = { 100 | puppeteer: { 101 | executablePath: chromeBin, 102 | headless, 103 | args: [ 104 | '--autoplay-policy=user-gesture-required', 105 | '--disable-background-networking', 106 | '--disable-background-timer-throttling', 107 | '--disable-backgrounding-occluded-windows', 108 | '--disable-breakpad', 109 | '--disable-client-side-phishing-detection', 110 | '--disable-component-update', 111 | '--disable-default-apps', 112 | '--disable-dev-shm-usage', 113 | '--disable-domain-reliability', 114 | '--disable-extensions', 115 | '--disable-features=AudioServiceOutOfProcess', 116 | '--disable-hang-monitor', 117 | '--disable-ipc-flooding-protection', 118 | '--disable-notifications', 119 | '--disable-offer-store-unmasked-wallet-cards', 120 | '--disable-popup-blocking', 121 | '--disable-print-preview', 122 | '--disable-prompt-on-repost', 123 | '--disable-renderer-backgrounding', 124 | '--disable-speech-api', 125 | '--disable-sync', 126 | '--disable-gpu', 127 | '--disable-accelerated-2d-canvas', 128 | '--hide-scrollbars', 129 | '--ignore-gpu-blacklist', 130 | '--metrics-recording-only', 131 | '--mute-audio', 132 | '--no-default-browser-check', 133 | '--no-first-run', 134 | '--no-pings', 135 | '--no-zygote', 136 | '--password-store=basic', 137 | '--use-gl=swiftshader', 138 | '--use-mock-keychain', 139 | '--disable-setuid-sandbox', 140 | '--no-sandbox', 141 | '--disable-blink-features=AutomationControlled' 142 | ] 143 | }, 144 | authStrategy: localAuth 145 | } 146 | 147 | if (webVersion) { 148 | clientOptions.webVersion = webVersion 149 | switch (webVersionCacheType.toLowerCase()) { 150 | case 'local': 151 | clientOptions.webVersionCache = { 152 | type: 'local' 153 | } 154 | break 155 | case 'remote': 156 | clientOptions.webVersionCache = { 157 | type: 'remote', 158 | remotePath: 'https://raw.githubusercontent.com/wppconnect-team/wa-version/main/html/' + webVersion + '.html' 159 | } 160 | break 161 | default: 162 | clientOptions.webVersionCache = { 163 | type: 'none' 164 | } 165 | } 166 | } 167 | 168 | const client = new Client(clientOptions) 169 | if (releaseBrowserLock) { 170 | // See https://github.com/puppeteer/puppeteer/issues/4860 171 | const singletonLockPath = path.resolve(path.join(sessionFolderPath, `session-${sessionId}`, 'SingletonLock')) 172 | const singletonLockExists = await fs.promises.lstat(singletonLockPath).then(() => true).catch(() => false) 173 | if (singletonLockExists) { 174 | logger.warn({ sessionId }, 'Browser lock file exists, removing') 175 | await fs.promises.unlink(singletonLockPath) 176 | } 177 | } 178 | 179 | try { 180 | client.once('ready', () => { 181 | patchWWebLibrary(client).catch((err) => { 182 | logger.error({ sessionId, err }, 'Failed to patch WWebJS library') 183 | }) 184 | }) 185 | initWebSocketServer(sessionId) 186 | initializeEvents(client, sessionId) 187 | await client.initialize() 188 | } catch (error) { 189 | logger.error({ sessionId, err: error }, 'Initialize error') 190 | throw error 191 | } 192 | 193 | // Save the session to the Map 194 | sessions.set(sessionId, client) 195 | return { success: true, message: 'Session initiated successfully', client } 196 | } catch (error) { 197 | return { success: false, message: error.message, client: null } 198 | } 199 | } 200 | 201 | const initializeEvents = (client, sessionId) => { 202 | // check if the session webhook is overridden 203 | const sessionWebhook = process.env[sessionId.toUpperCase() + '_WEBHOOK_URL'] || baseWebhookURL 204 | 205 | if (recoverSessions) { 206 | waitForNestedObject(client, 'pupPage').then(() => { 207 | const restartSession = async (sessionId) => { 208 | sessions.delete(sessionId) 209 | await client.destroy().catch(e => { }) 210 | await setupSession(sessionId) 211 | } 212 | client.pupPage.once('close', function () { 213 | // emitted when the page closes 214 | logger.warn({ sessionId }, 'Browser page closed. Restoring') 215 | restartSession(sessionId) 216 | }) 217 | client.pupPage.once('error', function () { 218 | // emitted when the page crashes 219 | logger.warn({ sessionId }, 'Error occurred on browser page. Restoring') 220 | restartSession(sessionId) 221 | }) 222 | client.pupPage 223 | .on('console', message => { 224 | const type = message.type().substr(0, 3).toUpperCase() 225 | logger.debug({ sessionId, type }, `Page console log: ${message.text()}`) 226 | }) 227 | .on('requestfailed', request => { 228 | const failure = request.failure() 229 | if (failure) { 230 | logger.error({ sessionId, url: request.url() }, `Page request failed: ${failure.errorText}`) 231 | } else { 232 | logger.error({ sessionId, url: request.url() }, 'Page request failed but no failure reason provided') 233 | } 234 | }) 235 | .on('pageerror', ({ message }) => logger.error({ sessionId, message }, 'Page error occurred')) 236 | }).catch(e => { }) 237 | } 238 | 239 | if (isEventEnabled('auth_failure')) { 240 | client.on('auth_failure', (msg) => { 241 | triggerWebhook(sessionWebhook, sessionId, 'status', { msg }) 242 | triggerWebSocket(sessionId, 'status', { msg }) 243 | }) 244 | } 245 | 246 | client.on('authenticated', () => { 247 | client.qr = null 248 | if (isEventEnabled('authenticated')) { 249 | triggerWebhook(sessionWebhook, sessionId, 'authenticated') 250 | triggerWebSocket(sessionId, 'authenticated') 251 | } 252 | }) 253 | 254 | if (isEventEnabled('call')) { 255 | client.on('call', (call) => { 256 | triggerWebhook(sessionWebhook, sessionId, 'call', { call }) 257 | triggerWebSocket(sessionId, 'call', { call }) 258 | }) 259 | } 260 | 261 | if (isEventEnabled('change_state')) { 262 | client.on('change_state', state => { 263 | triggerWebhook(sessionWebhook, sessionId, 'change_state', { state }) 264 | triggerWebSocket(sessionId, 'change_state', { state }) 265 | }) 266 | } 267 | 268 | if (isEventEnabled('disconnected')) { 269 | client.on('disconnected', (reason) => { 270 | triggerWebhook(sessionWebhook, sessionId, 'disconnected', { reason }) 271 | triggerWebSocket(sessionId, 'disconnected', { reason }) 272 | }) 273 | } 274 | 275 | if (isEventEnabled('group_join')) { 276 | client.on('group_join', (notification) => { 277 | triggerWebhook(sessionWebhook, sessionId, 'group_join', { notification }) 278 | triggerWebSocket(sessionId, 'group_join', { notification }) 279 | }) 280 | } 281 | 282 | if (isEventEnabled('group_leave')) { 283 | client.on('group_leave', (notification) => { 284 | triggerWebhook(sessionWebhook, sessionId, 'group_leave', { notification }) 285 | triggerWebSocket(sessionId, 'group_leave', { notification }) 286 | }) 287 | } 288 | 289 | if (isEventEnabled('group_admin_changed')) { 290 | client.on('group_admin_changed', (notification) => { 291 | triggerWebhook(sessionWebhook, sessionId, 'group_admin_changed', { notification }) 292 | triggerWebSocket(sessionId, 'group_admin_changed', { notification }) 293 | }) 294 | } 295 | 296 | if (isEventEnabled('group_membership_request')) { 297 | client.on('group_membership_request', (notification) => { 298 | triggerWebhook(sessionWebhook, sessionId, 'group_membership_request', { notification }) 299 | triggerWebSocket(sessionId, 'group_membership_request', { notification }) 300 | }) 301 | } 302 | 303 | if (isEventEnabled('group_update')) { 304 | client.on('group_update', (notification) => { 305 | triggerWebhook(sessionWebhook, sessionId, 'group_update', { notification }) 306 | triggerWebSocket(sessionId, 'group_update', { notification }) 307 | }) 308 | } 309 | 310 | if (isEventEnabled('loading_screen')) { 311 | client.on('loading_screen', (percent, message) => { 312 | triggerWebhook(sessionWebhook, sessionId, 'loading_screen', { percent, message }) 313 | triggerWebSocket(sessionId, 'loading_screen', { percent, message }) 314 | }) 315 | } 316 | 317 | if (isEventEnabled('media_uploaded')) { 318 | client.on('media_uploaded', (message) => { 319 | triggerWebhook(sessionWebhook, sessionId, 'media_uploaded', { message }) 320 | triggerWebSocket(sessionId, 'media_uploaded', { message }) 321 | }) 322 | } 323 | 324 | client.on('message', async (message) => { 325 | if (isEventEnabled('message')) { 326 | triggerWebhook(sessionWebhook, sessionId, 'message', { message }) 327 | triggerWebSocket(sessionId, 'message', { message }) 328 | if (message.hasMedia && message._data?.size < maxAttachmentSize) { 329 | // custom service event 330 | if (isEventEnabled('media')) { 331 | message.downloadMedia().then(messageMedia => { 332 | triggerWebhook(sessionWebhook, sessionId, 'media', { messageMedia, message }) 333 | triggerWebSocket(sessionId, 'media', { messageMedia, message }) 334 | }).catch(error => { 335 | logger.error({ sessionId, err: error }, 'Failed to download media') 336 | }) 337 | } 338 | } 339 | } 340 | if (setMessagesAsSeen) { 341 | // small delay to ensure the message is processed before sending seen status 342 | await sleep(1000) 343 | sendMessageSeenStatus(message) 344 | } 345 | }) 346 | 347 | if (isEventEnabled('message_ack')) { 348 | client.on('message_ack', (message, ack) => { 349 | triggerWebhook(sessionWebhook, sessionId, 'message_ack', { message, ack }) 350 | triggerWebSocket(sessionId, 'message_ack', { message, ack }) 351 | }) 352 | } 353 | 354 | if (isEventEnabled('message_create')) { 355 | client.on('message_create', (message) => { 356 | triggerWebhook(sessionWebhook, sessionId, 'message_create', { message }) 357 | triggerWebSocket(sessionId, 'message_create', { message }) 358 | }) 359 | } 360 | 361 | if (isEventEnabled('message_reaction')) { 362 | client.on('message_reaction', (reaction) => { 363 | triggerWebhook(sessionWebhook, sessionId, 'message_reaction', { reaction }) 364 | triggerWebSocket(sessionId, 'message_reaction', { reaction }) 365 | }) 366 | } 367 | 368 | if (isEventEnabled('message_edit')) { 369 | client.on('message_edit', (message, newBody, prevBody) => { 370 | triggerWebhook(sessionWebhook, sessionId, 'message_edit', { message, newBody, prevBody }) 371 | triggerWebSocket(sessionId, 'message_edit', { message, newBody, prevBody }) 372 | }) 373 | } 374 | 375 | if (isEventEnabled('message_ciphertext')) { 376 | client.on('message_ciphertext', (message) => { 377 | triggerWebhook(sessionWebhook, sessionId, 'message_ciphertext', { message }) 378 | triggerWebSocket(sessionId, 'message_ciphertext', { message }) 379 | }) 380 | } 381 | 382 | if (isEventEnabled('message_revoke_everyone')) { 383 | client.on('message_revoke_everyone', (message) => { 384 | triggerWebhook(sessionWebhook, sessionId, 'message_revoke_everyone', { message }) 385 | triggerWebSocket(sessionId, 'message_revoke_everyone', { message }) 386 | }) 387 | } 388 | 389 | if (isEventEnabled('message_revoke_me')) { 390 | client.on('message_revoke_me', (message, revokedMsg) => { 391 | triggerWebhook(sessionWebhook, sessionId, 'message_revoke_me', { message, revokedMsg }) 392 | triggerWebSocket(sessionId, 'message_revoke_me', { message, revokedMsg }) 393 | }) 394 | } 395 | 396 | client.on('qr', (qr) => { 397 | // inject qr code into session 398 | client.qr = qr 399 | if (isEventEnabled('qr')) { 400 | triggerWebhook(sessionWebhook, sessionId, 'qr', { qr }) 401 | triggerWebSocket(sessionId, 'qr', { qr }) 402 | } 403 | }) 404 | 405 | if (isEventEnabled('ready')) { 406 | client.on('ready', () => { 407 | triggerWebhook(sessionWebhook, sessionId, 'ready') 408 | triggerWebSocket(sessionId, 'ready') 409 | }) 410 | } 411 | 412 | if (isEventEnabled('contact_changed')) { 413 | client.on('contact_changed', (message, oldId, newId, isContact) => { 414 | triggerWebhook(sessionWebhook, sessionId, 'contact_changed', { message, oldId, newId, isContact }) 415 | triggerWebSocket(sessionId, 'contact_changed', { message, oldId, newId, isContact }) 416 | }) 417 | } 418 | 419 | if (isEventEnabled('chat_removed')) { 420 | client.on('chat_removed', (chat) => { 421 | triggerWebhook(sessionWebhook, sessionId, 'chat_removed', { chat }) 422 | triggerWebSocket(sessionId, 'chat_removed', { chat }) 423 | }) 424 | } 425 | 426 | if (isEventEnabled('chat_archived')) { 427 | client.on('chat_archived', (chat, currState, prevState) => { 428 | triggerWebhook(sessionWebhook, sessionId, 'chat_archived', { chat, currState, prevState }) 429 | triggerWebSocket(sessionId, 'chat_archived', { chat, currState, prevState }) 430 | }) 431 | } 432 | 433 | if (isEventEnabled('unread_count')) { 434 | client.on('unread_count', (chat) => { 435 | triggerWebhook(sessionWebhook, sessionId, 'unread_count', { chat }) 436 | triggerWebSocket(sessionId, 'unread_count', { chat }) 437 | }) 438 | } 439 | 440 | if (isEventEnabled('vote_update')) { 441 | client.on('vote_update', (vote) => { 442 | triggerWebhook(sessionWebhook, sessionId, 'vote_update', { vote }) 443 | triggerWebSocket(sessionId, 'vote_update', { vote }) 444 | }) 445 | } 446 | 447 | if (isEventEnabled('code')) { 448 | client.on('code', (code) => { 449 | triggerWebhook(sessionWebhook, sessionId, 'code', { code }) 450 | triggerWebSocket(sessionId, 'code', { code }) 451 | }) 452 | } 453 | } 454 | 455 | // Function to delete client session folder 456 | const deleteSessionFolder = async (sessionId) => { 457 | try { 458 | const targetDirPath = path.join(sessionFolderPath, `session-${sessionId}`) 459 | const resolvedTargetDirPath = await fs.promises.realpath(targetDirPath) 460 | const resolvedSessionPath = await fs.promises.realpath(sessionFolderPath) 461 | 462 | // Ensure the target directory path ends with a path separator 463 | const safeSessionPath = `${resolvedSessionPath}${path.sep}` 464 | 465 | // Validate the resolved target directory path is a subdirectory of the session folder path 466 | if (!resolvedTargetDirPath.startsWith(safeSessionPath)) { 467 | throw new Error('Invalid path: Directory traversal detected') 468 | } 469 | await fs.promises.rm(resolvedTargetDirPath, { recursive: true, force: true }) 470 | } catch (error) { 471 | logger.error({ sessionId, err: error }, 'Folder deletion error') 472 | throw error 473 | } 474 | } 475 | 476 | // Function to reload client session without removing browser cache 477 | const reloadSession = async (sessionId) => { 478 | try { 479 | const client = sessions.get(sessionId) 480 | if (!client) { 481 | return 482 | } 483 | client.pupPage?.removeAllListeners('close') 484 | client.pupPage?.removeAllListeners('error') 485 | try { 486 | const pages = await client.pupBrowser.pages() 487 | await Promise.all(pages.map((page) => page.close())) 488 | await Promise.race([ 489 | client.pupBrowser.close(), 490 | new Promise(resolve => setTimeout(resolve, 5000)) 491 | ]) 492 | } catch (e) { 493 | const childProcess = client.pupBrowser.process() 494 | if (childProcess) { 495 | childProcess.kill(9) 496 | } 497 | } 498 | sessions.delete(sessionId) 499 | await setupSession(sessionId) 500 | } catch (error) { 501 | logger.error({ sessionId, err: error }, 'Failed to reload session') 502 | throw error 503 | } 504 | } 505 | 506 | const destroySession = async (sessionId) => { 507 | try { 508 | const client = sessions.get(sessionId) 509 | if (!client) { 510 | return 511 | } 512 | client.pupPage?.removeAllListeners('close') 513 | client.pupPage?.removeAllListeners('error') 514 | try { 515 | await terminateWebSocketServer(sessionId) 516 | } catch (error) { 517 | logger.error({ sessionId, err: error }, 'Failed to terminate WebSocket server') 518 | } 519 | await client.destroy() 520 | // Wait 10 secs for client.pupBrowser to be disconnected 521 | let maxDelay = 0 522 | while (client.pupBrowser?.isConnected() && (maxDelay < 10)) { 523 | await new Promise(resolve => setTimeout(resolve, 1000)) 524 | maxDelay++ 525 | } 526 | sessions.delete(sessionId) 527 | } catch (error) { 528 | logger.error({ sessionId, err: error }, 'Failed to stop session') 529 | throw error 530 | } 531 | } 532 | 533 | const deleteSession = async (sessionId, validation) => { 534 | try { 535 | const client = sessions.get(sessionId) 536 | if (!client) { 537 | return 538 | } 539 | client.pupPage?.removeAllListeners('close') 540 | client.pupPage?.removeAllListeners('error') 541 | try { 542 | await terminateWebSocketServer(sessionId) 543 | } catch (error) { 544 | logger.error({ sessionId, err: error }, 'Failed to terminate WebSocket server') 545 | } 546 | if (validation.success) { 547 | // Client Connected, request logout 548 | logger.info({ sessionId }, 'Logging out session') 549 | await client.logout() 550 | } else if (validation.message === 'session_not_connected') { 551 | // Client not Connected, request destroy 552 | logger.info({ sessionId }, 'Destroying session') 553 | await client.destroy() 554 | } 555 | // Wait 10 secs for client.pupBrowser to be disconnected before deleting the folder 556 | let maxDelay = 0 557 | while (client.pupBrowser.isConnected() && (maxDelay < 10)) { 558 | await new Promise(resolve => setTimeout(resolve, 1000)) 559 | maxDelay++ 560 | } 561 | sessions.delete(sessionId) 562 | await deleteSessionFolder(sessionId) 563 | } catch (error) { 564 | logger.error({ sessionId, err: error }, 'Failed to delete session') 565 | throw error 566 | } 567 | } 568 | 569 | // Function to handle session flush 570 | const flushSessions = async (deleteOnlyInactive) => { 571 | try { 572 | // Read the contents of the sessions folder 573 | const files = await fs.promises.readdir(sessionFolderPath) 574 | // Iterate through the files in the parent folder 575 | for (const file of files) { 576 | // Use regular expression to extract the string from the folder name 577 | const match = file.match(/^session-(.+)$/) 578 | if (match) { 579 | const sessionId = match[1] 580 | const validation = await validateSession(sessionId) 581 | if (!deleteOnlyInactive || !validation.success) { 582 | await deleteSession(sessionId, validation) 583 | } 584 | } 585 | } 586 | } catch (error) { 587 | logger.error(error, 'Failed to flush sessions') 588 | throw error 589 | } 590 | } 591 | 592 | module.exports = { 593 | sessions, 594 | setupSession, 595 | restoreSessions, 596 | validateSession, 597 | deleteSession, 598 | reloadSession, 599 | flushSessions, 600 | destroySession 601 | } 602 | -------------------------------------------------------------------------------- /src/routes.js: -------------------------------------------------------------------------------- 1 | const express = require('express') 2 | const routes = express.Router() 3 | const swaggerUi = require('swagger-ui-express') 4 | const swaggerDocument = require('../swagger.json') 5 | const { enableLocalCallbackExample, enableSwaggerEndpoint } = require('./config') 6 | 7 | const middleware = require('./middleware') 8 | const healthController = require('./controllers/healthController') 9 | const sessionController = require('./controllers/sessionController') 10 | const clientController = require('./controllers/clientController') 11 | const chatController = require('./controllers/chatController') 12 | const groupChatController = require('./controllers/groupChatController') 13 | const messageController = require('./controllers/messageController') 14 | const contactController = require('./controllers/contactController') 15 | const channelController = require('./controllers/channelController') 16 | 17 | /** 18 | * ================ 19 | * HEALTH ENDPOINTS 20 | * ================ 21 | */ 22 | 23 | // API endpoint to check if server is alive 24 | routes.get('/ping', healthController.ping) 25 | // API basic callback 26 | if (enableLocalCallbackExample) { 27 | routes.post('/localCallbackExample', [middleware.apikey, middleware.rateLimiter], healthController.localCallbackExample) 28 | } 29 | 30 | /** 31 | * ================ 32 | * SESSION ENDPOINTS 33 | * ================ 34 | */ 35 | const sessionRouter = express.Router() 36 | sessionRouter.use(middleware.apikey) 37 | sessionRouter.use(middleware.sessionSwagger) 38 | routes.use('/session', sessionRouter) 39 | 40 | sessionRouter.get('/getSessions', sessionController.getSessions) 41 | sessionRouter.get('/start/:sessionId', middleware.sessionNameValidation, sessionController.startSession) 42 | sessionRouter.get('/stop/:sessionId', middleware.sessionNameValidation, sessionController.stopSession) 43 | sessionRouter.get('/status/:sessionId', middleware.sessionNameValidation, sessionController.statusSession) 44 | sessionRouter.get('/qr/:sessionId', middleware.sessionNameValidation, sessionController.sessionQrCode) 45 | sessionRouter.get('/qr/:sessionId/image', middleware.sessionNameValidation, sessionController.sessionQrCodeImage) 46 | sessionRouter.post('/requestPairingCode/:sessionId', middleware.sessionNameValidation, sessionController.requestPairingCode) 47 | sessionRouter.get('/restart/:sessionId', middleware.sessionNameValidation, sessionController.restartSession) 48 | sessionRouter.get('/terminate/:sessionId', middleware.sessionNameValidation, sessionController.terminateSession) 49 | sessionRouter.get('/terminateInactive', sessionController.terminateInactiveSessions) 50 | sessionRouter.get('/terminateAll', sessionController.terminateAllSessions) 51 | sessionRouter.get('/getPageScreenshot/:sessionId', middleware.sessionNameValidation, sessionController.getPageScreenshot) 52 | 53 | /** 54 | * ================ 55 | * CLIENT ENDPOINTS 56 | * ================ 57 | */ 58 | 59 | const clientRouter = express.Router() 60 | clientRouter.use(middleware.apikey) 61 | sessionRouter.use(middleware.clientSwagger) 62 | routes.use('/client', clientRouter) 63 | 64 | clientRouter.get('/getClassInfo/:sessionId', [middleware.sessionNameValidation, middleware.sessionValidation], clientController.getClassInfo) 65 | clientRouter.post('/acceptInvite/:sessionId', [middleware.sessionNameValidation, middleware.sessionValidation], clientController.acceptInvite) 66 | clientRouter.post('/archiveChat/:sessionId', [middleware.sessionNameValidation, middleware.sessionValidation], clientController.archiveChat) 67 | clientRouter.post('/createGroup/:sessionId', [middleware.sessionNameValidation, middleware.sessionValidation], clientController.createGroup) 68 | clientRouter.post('/getBlockedContacts/:sessionId', [middleware.sessionNameValidation, middleware.sessionValidation], clientController.getBlockedContacts) 69 | clientRouter.post('/getChatById/:sessionId', [middleware.sessionNameValidation, middleware.sessionValidation], clientController.getChatById) 70 | clientRouter.post('/getChatLabels/:sessionId', [middleware.sessionNameValidation, middleware.sessionValidation], clientController.getChatLabels) 71 | clientRouter.get('/getChats/:sessionId', [middleware.sessionNameValidation, middleware.sessionValidation], clientController.getChats) 72 | clientRouter.post('/getChats/:sessionId', [middleware.sessionNameValidation, middleware.sessionValidation], clientController.getChatsWithSearch) 73 | clientRouter.post('/getChatsByLabelId/:sessionId', [middleware.sessionNameValidation, middleware.sessionValidation], clientController.getChatsByLabelId) 74 | clientRouter.post('/getCommonGroups/:sessionId', [middleware.sessionNameValidation, middleware.sessionValidation], clientController.getCommonGroups) 75 | clientRouter.post('/getContactById/:sessionId', [middleware.sessionNameValidation, middleware.sessionValidation], clientController.getContactById) 76 | clientRouter.get('/getContacts/:sessionId', [middleware.sessionNameValidation, middleware.sessionValidation], clientController.getContacts) 77 | clientRouter.post('/getInviteInfo/:sessionId', [middleware.sessionNameValidation, middleware.sessionValidation], clientController.getInviteInfo) 78 | clientRouter.post('/getLabelById/:sessionId', [middleware.sessionNameValidation, middleware.sessionValidation], clientController.getLabelById) 79 | clientRouter.post('/getLabels/:sessionId', [middleware.sessionNameValidation, middleware.sessionValidation], clientController.getLabels) 80 | clientRouter.post('/addOrRemoveLabels/:sessionId', [middleware.sessionNameValidation, middleware.sessionValidation], clientController.addOrRemoveLabels) 81 | clientRouter.post('/getNumberId/:sessionId', [middleware.sessionNameValidation, middleware.sessionValidation], clientController.getNumberId) 82 | clientRouter.post('/isRegisteredUser/:sessionId', [middleware.sessionNameValidation, middleware.sessionValidation], clientController.isRegisteredUser) 83 | clientRouter.post('/getProfilePicUrl/:sessionId', [middleware.sessionNameValidation, middleware.sessionValidation], clientController.getProfilePictureUrl) 84 | clientRouter.get('/getState/:sessionId', [middleware.sessionNameValidation], clientController.getState) 85 | clientRouter.post('/markChatUnread/:sessionId', [middleware.sessionNameValidation, middleware.sessionValidation], clientController.markChatUnread) 86 | clientRouter.post('/muteChat/:sessionId', [middleware.sessionNameValidation, middleware.sessionValidation], clientController.muteChat) 87 | clientRouter.post('/pinChat/:sessionId', [middleware.sessionNameValidation, middleware.sessionValidation], clientController.pinChat) 88 | clientRouter.post('/searchMessages/:sessionId', [middleware.sessionNameValidation, middleware.sessionValidation], clientController.searchMessages) 89 | clientRouter.post('/sendMessage/:sessionId', [middleware.sessionNameValidation, middleware.sessionValidation], clientController.sendMessage) 90 | clientRouter.post('/sendPresenceAvailable/:sessionId', [middleware.sessionNameValidation, middleware.sessionValidation], clientController.sendPresenceAvailable) 91 | clientRouter.post('/sendPresenceUnavailable/:sessionId', [middleware.sessionNameValidation, middleware.sessionValidation], clientController.sendPresenceUnavailable) 92 | clientRouter.post('/sendSeen/:sessionId', [middleware.sessionNameValidation, middleware.sessionValidation], clientController.sendSeen) 93 | clientRouter.post('/setDisplayName/:sessionId', [middleware.sessionNameValidation, middleware.sessionValidation], clientController.setDisplayName) 94 | clientRouter.post('/setProfilePicture/:sessionId', [middleware.sessionNameValidation, middleware.sessionValidation], clientController.setProfilePicture) 95 | clientRouter.post('/setStatus/:sessionId', [middleware.sessionNameValidation, middleware.sessionValidation], clientController.setStatus) 96 | clientRouter.post('/unarchiveChat/:sessionId', [middleware.sessionNameValidation, middleware.sessionValidation], clientController.unarchiveChat) 97 | clientRouter.post('/unmuteChat/:sessionId', [middleware.sessionNameValidation, middleware.sessionValidation], clientController.unmuteChat) 98 | clientRouter.post('/unpinChat/:sessionId', [middleware.sessionNameValidation, middleware.sessionValidation], clientController.unpinChat) 99 | clientRouter.get('/getWWebVersion/:sessionId', [middleware.sessionNameValidation, middleware.sessionValidation], clientController.getWWebVersion) 100 | clientRouter.delete('/deleteProfilePicture/:sessionId', [middleware.sessionNameValidation, middleware.sessionValidation], clientController.deleteProfilePicture) 101 | clientRouter.post('/setAutoDownloadAudio/:sessionId', [middleware.sessionNameValidation, middleware.sessionValidation], clientController.setAutoDownloadAudio) 102 | clientRouter.post('/setAutoDownloadDocuments/:sessionId', [middleware.sessionNameValidation, middleware.sessionValidation], clientController.setAutoDownloadDocuments) 103 | clientRouter.post('/setAutoDownloadPhotos/:sessionId', [middleware.sessionNameValidation, middleware.sessionValidation], clientController.setAutoDownloadPhotos) 104 | clientRouter.post('/setAutoDownloadVideos/:sessionId', [middleware.sessionNameValidation, middleware.sessionValidation], clientController.setAutoDownloadVideos) 105 | clientRouter.post('/syncHistory/:sessionId', [middleware.sessionNameValidation, middleware.sessionValidation], clientController.syncHistory) 106 | clientRouter.post('/getContactDeviceCount/:sessionId', [middleware.sessionNameValidation, middleware.sessionValidation], clientController.getContactDeviceCount) 107 | clientRouter.post('/getCountryCode/:sessionId', [middleware.sessionNameValidation, middleware.sessionValidation], clientController.getCountryCode) 108 | clientRouter.post('/getFormattedNumber/:sessionId', [middleware.sessionNameValidation, middleware.sessionValidation], clientController.getFormattedNumber) 109 | clientRouter.post('/openChatWindow/:sessionId', [middleware.sessionNameValidation, middleware.sessionValidation], clientController.openChatWindow) 110 | clientRouter.post('/openChatWindowAt/:sessionId', [middleware.sessionNameValidation, middleware.sessionValidation], clientController.openChatWindowAt) 111 | clientRouter.post('/resetState/:sessionId', [middleware.sessionNameValidation, middleware.sessionValidation], clientController.resetState) 112 | clientRouter.post('/setBackgroundSync/:sessionId', [middleware.sessionNameValidation, middleware.sessionValidation], clientController.setBackgroundSync) 113 | clientRouter.post('/getContactLidAndPhone/:sessionId', [middleware.sessionNameValidation, middleware.sessionValidation], clientController.getContactLidAndPhone) 114 | clientRouter.post('/getChannelByInviteCode/:sessionId', [middleware.sessionNameValidation, middleware.sessionValidation], clientController.getChannelByInviteCode) 115 | clientRouter.get('/getChannels/:sessionId', [middleware.sessionNameValidation, middleware.sessionValidation], clientController.getChannels) 116 | clientRouter.post('/createChannel/:sessionId', [middleware.sessionNameValidation, middleware.sessionValidation], clientController.createChannel) 117 | clientRouter.post('/subscribeToChannel/:sessionId', [middleware.sessionNameValidation, middleware.sessionValidation], clientController.subscribeToChannel) 118 | clientRouter.post('/unsubscribeFromChannel/:sessionId', [middleware.sessionNameValidation, middleware.sessionValidation], clientController.unsubscribeFromChannel) 119 | clientRouter.post('/searchChannels/:sessionId', [middleware.sessionNameValidation, middleware.sessionValidation], clientController.searchChannels) 120 | clientRouter.post('/runMethod/:sessionId', [middleware.sessionNameValidation, middleware.sessionValidation], clientController.runMethod) 121 | 122 | /** 123 | * ================ 124 | * CHAT ENDPOINTS 125 | * ================ 126 | */ 127 | const chatRouter = express.Router() 128 | chatRouter.use(middleware.apikey) 129 | sessionRouter.use(middleware.chatSwagger) 130 | routes.use('/chat', chatRouter) 131 | 132 | chatRouter.post('/getClassInfo/:sessionId', [middleware.sessionNameValidation, middleware.sessionValidation], chatController.getClassInfo) 133 | chatRouter.post('/clearMessages/:sessionId', [middleware.sessionNameValidation, middleware.sessionValidation], chatController.clearMessages) 134 | chatRouter.post('/clearState/:sessionId', [middleware.sessionNameValidation, middleware.sessionValidation], chatController.clearState) 135 | chatRouter.post('/delete/:sessionId', [middleware.sessionNameValidation, middleware.sessionValidation], chatController.deleteChat) 136 | chatRouter.post('/fetchMessages/:sessionId', [middleware.sessionNameValidation, middleware.sessionValidation], chatController.fetchMessages) 137 | chatRouter.post('/getContact/:sessionId', [middleware.sessionNameValidation, middleware.sessionValidation], chatController.getContact) 138 | chatRouter.post('/sendStateRecording/:sessionId', [middleware.sessionNameValidation, middleware.sessionValidation], chatController.sendStateRecording) 139 | chatRouter.post('/sendStateTyping/:sessionId', [middleware.sessionNameValidation, middleware.sessionValidation], chatController.sendStateTyping) 140 | chatRouter.post('/sendSeen/:sessionId', [middleware.sessionNameValidation, middleware.sessionValidation], chatController.sendSeen) 141 | chatRouter.post('/markUnread/:sessionId', [middleware.sessionNameValidation, middleware.sessionValidation], chatController.markUnread) 142 | chatRouter.post('/syncHistory/:sessionId', [middleware.sessionNameValidation, middleware.sessionValidation], chatController.syncHistory) 143 | chatRouter.post('/getLabels/:sessionId', [middleware.sessionNameValidation, middleware.sessionValidation], chatController.getLabels) 144 | chatRouter.post('/changeLabels/:sessionId', [middleware.sessionNameValidation, middleware.sessionValidation], chatController.changeLabels) 145 | chatRouter.post('/runMethod/:sessionId', [middleware.sessionNameValidation, middleware.sessionValidation], chatController.runMethod) 146 | 147 | /** 148 | * ================ 149 | * GROUP CHAT ENDPOINTS 150 | * ================ 151 | */ 152 | const groupChatRouter = express.Router() 153 | groupChatRouter.use(middleware.apikey) 154 | sessionRouter.use(middleware.groupChatSwagger) 155 | routes.use('/groupChat', groupChatRouter) 156 | 157 | groupChatRouter.post('/getClassInfo/:sessionId', [middleware.sessionNameValidation, middleware.sessionValidation], groupChatController.getClassInfo) 158 | groupChatRouter.post('/addParticipants/:sessionId', [middleware.sessionNameValidation, middleware.sessionValidation], groupChatController.addParticipants) 159 | groupChatRouter.post('/demoteParticipants/:sessionId', [middleware.sessionNameValidation, middleware.sessionValidation], groupChatController.demoteParticipants) 160 | groupChatRouter.post('/getInviteCode/:sessionId', [middleware.sessionNameValidation, middleware.sessionValidation], groupChatController.getInviteCode) 161 | groupChatRouter.post('/leave/:sessionId', [middleware.sessionNameValidation, middleware.sessionValidation], groupChatController.leave) 162 | groupChatRouter.post('/promoteParticipants/:sessionId', [middleware.sessionNameValidation, middleware.sessionValidation], groupChatController.promoteParticipants) 163 | groupChatRouter.post('/removeParticipants/:sessionId', [middleware.sessionNameValidation, middleware.sessionValidation], groupChatController.removeParticipants) 164 | groupChatRouter.post('/revokeInvite/:sessionId', [middleware.sessionNameValidation, middleware.sessionValidation], groupChatController.revokeInvite) 165 | groupChatRouter.post('/setDescription/:sessionId', [middleware.sessionNameValidation, middleware.sessionValidation], groupChatController.setDescription) 166 | groupChatRouter.post('/setInfoAdminsOnly/:sessionId', [middleware.sessionNameValidation, middleware.sessionValidation], groupChatController.setInfoAdminsOnly) 167 | groupChatRouter.post('/setMessagesAdminsOnly/:sessionId', [middleware.sessionNameValidation, middleware.sessionValidation], groupChatController.setMessagesAdminsOnly) 168 | groupChatRouter.post('/setSubject/:sessionId', [middleware.sessionNameValidation, middleware.sessionValidation], groupChatController.setSubject) 169 | groupChatRouter.post('/setPicture/:sessionId', [middleware.sessionNameValidation, middleware.sessionValidation], groupChatController.setPicture) 170 | groupChatRouter.post('/deletePicture/:sessionId', [middleware.sessionNameValidation, middleware.sessionValidation], groupChatController.deletePicture) 171 | groupChatRouter.post('/getGroupMembershipRequests/:sessionId', [middleware.sessionNameValidation, middleware.sessionValidation], groupChatController.getGroupMembershipRequests) 172 | groupChatRouter.post('/approveGroupMembershipRequests/:sessionId', [middleware.sessionNameValidation, middleware.sessionValidation], groupChatController.approveGroupMembershipRequests) 173 | groupChatRouter.post('/rejectGroupMembershipRequests/:sessionId', [middleware.sessionNameValidation, middleware.sessionValidation], groupChatController.rejectGroupMembershipRequests) 174 | groupChatRouter.post('/runMethod/:sessionId', [middleware.sessionNameValidation, middleware.sessionValidation], groupChatController.runMethod) 175 | 176 | /** 177 | * ================ 178 | * MESSAGE ENDPOINTS 179 | * ================ 180 | */ 181 | const messageRouter = express.Router() 182 | messageRouter.use(middleware.apikey) 183 | sessionRouter.use(middleware.messageSwagger) 184 | routes.use('/message', messageRouter) 185 | 186 | messageRouter.post('/getClassInfo/:sessionId', [middleware.sessionNameValidation, middleware.sessionValidation], messageController.getClassInfo) 187 | messageRouter.post('/delete/:sessionId', [middleware.sessionNameValidation, middleware.sessionValidation], messageController.deleteMessage) 188 | messageRouter.post('/downloadMedia/:sessionId', [middleware.sessionNameValidation, middleware.sessionValidation], messageController.downloadMedia) 189 | messageRouter.post('/downloadMediaAsData/:sessionId', [middleware.sessionNameValidation, middleware.sessionValidation], messageController.downloadMediaAsData) 190 | messageRouter.post('/forward/:sessionId', [middleware.sessionNameValidation, middleware.sessionValidation], messageController.forward) 191 | messageRouter.post('/getInfo/:sessionId', [middleware.sessionNameValidation, middleware.sessionValidation], messageController.getInfo) 192 | messageRouter.post('/getMentions/:sessionId', [middleware.sessionNameValidation, middleware.sessionValidation], messageController.getMentions) 193 | messageRouter.post('/getOrder/:sessionId', [middleware.sessionNameValidation, middleware.sessionValidation], messageController.getOrder) 194 | messageRouter.post('/getPayment/:sessionId', [middleware.sessionNameValidation, middleware.sessionValidation], messageController.getPayment) 195 | messageRouter.post('/getQuotedMessage/:sessionId', [middleware.sessionNameValidation, middleware.sessionValidation], messageController.getQuotedMessage) 196 | messageRouter.post('/react/:sessionId', [middleware.sessionNameValidation, middleware.sessionValidation], messageController.react) 197 | messageRouter.post('/reply/:sessionId', [middleware.sessionNameValidation, middleware.sessionValidation], messageController.reply) 198 | messageRouter.post('/star/:sessionId', [middleware.sessionNameValidation, middleware.sessionValidation], messageController.star) 199 | messageRouter.post('/unstar/:sessionId', [middleware.sessionNameValidation, middleware.sessionValidation], messageController.unstar) 200 | messageRouter.post('/getReactions/:sessionId', [middleware.sessionNameValidation, middleware.sessionValidation], messageController.getReactions) 201 | messageRouter.post('/getGroupMentions/:sessionId', [middleware.sessionNameValidation, middleware.sessionValidation], messageController.getGroupMentions) 202 | messageRouter.post('/edit/:sessionId', [middleware.sessionNameValidation, middleware.sessionValidation], messageController.edit) 203 | messageRouter.post('/getContact/:sessionId', [middleware.sessionNameValidation, middleware.sessionValidation], messageController.getContact) 204 | messageRouter.post('/runMethod/:sessionId', [middleware.sessionNameValidation, middleware.sessionValidation], messageController.runMethod) 205 | 206 | /** 207 | * ================ 208 | * MESSAGE ENDPOINTS 209 | * ================ 210 | */ 211 | const contactRouter = express.Router() 212 | contactRouter.use(middleware.apikey) 213 | sessionRouter.use(middleware.contactSwagger) 214 | routes.use('/contact', contactRouter) 215 | 216 | contactRouter.post('/getClassInfo/:sessionId', [middleware.sessionNameValidation, middleware.sessionValidation], contactController.getClassInfo) 217 | contactRouter.post('/block/:sessionId', [middleware.sessionNameValidation, middleware.sessionValidation], contactController.block) 218 | contactRouter.post('/getAbout/:sessionId', [middleware.sessionNameValidation, middleware.sessionValidation], contactController.getAbout) 219 | contactRouter.post('/getChat/:sessionId', [middleware.sessionNameValidation, middleware.sessionValidation], contactController.getChat) 220 | contactRouter.post('/unblock/:sessionId', [middleware.sessionNameValidation, middleware.sessionValidation], contactController.unblock) 221 | contactRouter.post('/getFormattedNumber/:sessionId', [middleware.sessionNameValidation, middleware.sessionValidation], contactController.getFormattedNumber) 222 | contactRouter.post('/getCountryCode/:sessionId', [middleware.sessionNameValidation, middleware.sessionValidation], contactController.getCountryCode) 223 | contactRouter.post('/getProfilePicUrl/:sessionId', [middleware.sessionNameValidation, middleware.sessionValidation], contactController.getProfilePicUrl) 224 | contactRouter.post('/getCommonGroups/:sessionId', [middleware.sessionNameValidation, middleware.sessionValidation], contactController.getCommonGroups) 225 | 226 | /** 227 | * ================ 228 | * CHANNEL ENDPOINTS 229 | * ================ 230 | */ 231 | const channelRouter = express.Router() 232 | channelRouter.use(middleware.apikey) 233 | sessionRouter.use(middleware.channelSwagger) 234 | routes.use('/channel', channelRouter) 235 | 236 | channelRouter.post('/getClassInfo/:sessionId', [middleware.sessionNameValidation, middleware.sessionValidation], channelController.getClassInfo) 237 | channelRouter.post('/sendMessage/:sessionId', [middleware.sessionNameValidation, middleware.sessionValidation], channelController.sendMessage) 238 | channelRouter.post('/fetchMessages/:sessionId', [middleware.sessionNameValidation, middleware.sessionValidation], channelController.fetchMessages) 239 | channelRouter.post('/sendSeen/:sessionId', [middleware.sessionNameValidation, middleware.sessionValidation], channelController.sendSeen) 240 | channelRouter.post('/mute/:sessionId', [middleware.sessionNameValidation, middleware.sessionValidation], channelController.mute) 241 | channelRouter.post('/unmute/:sessionId', [middleware.sessionNameValidation, middleware.sessionValidation], channelController.unmute) 242 | channelRouter.post('/acceptChannelAdminInvite/:sessionId', [middleware.sessionNameValidation, middleware.sessionValidation], channelController.acceptChannelAdminInvite) 243 | channelRouter.post('/sendChannelAdminInvite/:sessionId', [middleware.sessionNameValidation, middleware.sessionValidation], channelController.sendChannelAdminInvite) 244 | channelRouter.post('/revokeChannelAdminInvite/:sessionId', [middleware.sessionNameValidation, middleware.sessionValidation], channelController.revokeChannelAdminInvite) 245 | channelRouter.post('/transferChannelOwnership/:sessionId', [middleware.sessionNameValidation, middleware.sessionValidation], channelController.transferChannelOwnership) 246 | channelRouter.post('/demoteChannelAdmin/:sessionId', [middleware.sessionNameValidation, middleware.sessionValidation], channelController.demoteChannelAdmin) 247 | channelRouter.post('/getSubscribers/:sessionId', [middleware.sessionNameValidation, middleware.sessionValidation], channelController.getSubscribers) 248 | channelRouter.post('/setProfilePicture/:sessionId', [middleware.sessionNameValidation, middleware.sessionValidation], channelController.setProfilePicture) 249 | channelRouter.post('/setDescription/:sessionId', [middleware.sessionNameValidation, middleware.sessionValidation], channelController.setDescription) 250 | channelRouter.post('/setSubject/:sessionId', [middleware.sessionNameValidation, middleware.sessionValidation], channelController.setSubject) 251 | channelRouter.post('/setReactionSetting/:sessionId', [middleware.sessionNameValidation, middleware.sessionValidation], channelController.setReactionSetting) 252 | channelRouter.post('/deleteChannel/:sessionId', [middleware.sessionNameValidation, middleware.sessionValidation], channelController.deleteChannel) 253 | 254 | /** 255 | * ================ 256 | * SWAGGER ENDPOINTS 257 | * ================ 258 | */ 259 | if (enableSwaggerEndpoint) { 260 | routes.use('/api-docs', swaggerUi.serve) 261 | routes.get('/api-docs', swaggerUi.setup(swaggerDocument) /* #swagger.ignore = true */) 262 | } 263 | 264 | module.exports = { routes } 265 | -------------------------------------------------------------------------------- /src/controllers/groupChatController.js: -------------------------------------------------------------------------------- 1 | const { MessageMedia } = require('whatsapp-web.js') 2 | const { sessions } = require('../sessions') 3 | const { sendErrorResponse } = require('../utils') 4 | 5 | /** 6 | * Retrieves information about a chat based on the provided chatId 7 | * 8 | * @async 9 | * @function getClassInfo 10 | * @param {object} req - The request object 11 | * @param {object} res - The response object 12 | * @param {string} req.body.chatId - The chatId of the chat to retrieve information about 13 | * @param {string} req.params.sessionId - The sessionId of the client making the request 14 | * @throws {Error} The chat is not a group. 15 | * @returns {Promise} - A JSON response with success true and chat object containing chat information 16 | */ 17 | const getClassInfo = async (req, res) => { 18 | /* 19 | #swagger.summary = 'Get the group' 20 | */ 21 | try { 22 | const { chatId } = req.body 23 | const client = sessions.get(req.params.sessionId) 24 | const chat = await client.getChatById(chatId) 25 | if (!chat.isGroup) { throw new Error('The chat is not a group') } 26 | res.json({ success: true, chat }) 27 | } catch (error) { 28 | sendErrorResponse(res, 500, error.message) 29 | } 30 | } 31 | 32 | /** 33 | * Adds participants to a group chat. 34 | * @async 35 | * @function 36 | * @param {Object} req - The request object containing the chatId and participantIds in the body. 37 | * @param {string} req.body.chatId - The ID of the group chat. 38 | * @param {Array} req.body.participantIds - An array of participant IDs to be added to the group. 39 | * @param {Object} res - The response object. 40 | * @returns {Object} Returns a JSON object containing a success flag and the updated participants list. 41 | * @throws {Error} Throws an error if the chat is not a group chat. 42 | */ 43 | const addParticipants = async (req, res) => { 44 | /* 45 | #swagger.summary = 'Add the participants' 46 | #swagger.description = 'Add a list of participants by ID to the group' 47 | #swagger.requestBody = { 48 | required: true, 49 | schema: { 50 | type: 'object', 51 | properties: { 52 | chatId: { 53 | type: 'string', 54 | description: 'Unique WhatsApp id for the given chat group', 55 | example: 'XXXXXXXXXX@g.us' 56 | }, 57 | participantIds: { 58 | type: 'array', 59 | description: 'Unique WhatsApp identifiers for the participants', 60 | example: ['6281288888887@c.us'] 61 | }, 62 | options: { 63 | type: 'object', 64 | description: 'Options for adding participants', 65 | example: { sleep: [250, 500], comment: '' } 66 | } 67 | } 68 | } 69 | } 70 | */ 71 | try { 72 | const { chatId, contactIds, participantIds, options = {} } = req.body 73 | const client = sessions.get(req.params.sessionId) 74 | const chat = await client.getChatById(chatId) 75 | if (!chat.isGroup) { throw new Error('The chat is not a group') } 76 | // support old property name 77 | const addParticipants = participantIds || contactIds 78 | const participantIdsArray = Array.isArray(addParticipants) ? addParticipants : [addParticipants] 79 | const result = Object.keys(options).length ? await chat.addParticipants(participantIdsArray, options) : await chat.addParticipants(participantIdsArray) 80 | res.json({ success: true, result }) 81 | } catch (error) { 82 | sendErrorResponse(res, 500, error.message) 83 | } 84 | } 85 | 86 | /** 87 | * Removes participants from a group chat 88 | * 89 | * @async 90 | * @function 91 | * @param {Object} req - Express request object 92 | * @param {Object} res - Express response object 93 | * @returns {Promise} Returns a JSON object with success flag and updated participants list 94 | * @throws {Error} If chat is not a group 95 | */ 96 | const removeParticipants = async (req, res) => { 97 | /* 98 | #swagger.summary = 'Remove the participants' 99 | #swagger.description = 'Remove a list of participants by ID to the group' 100 | #swagger.requestBody = { 101 | required: true, 102 | schema: { 103 | type: 'object', 104 | properties: { 105 | chatId: { 106 | type: 'string', 107 | description: 'Unique WhatsApp id for the given chat group', 108 | example: 'XXXXXXXXXX@g.us' 109 | }, 110 | participantIds: { 111 | type: 'array', 112 | description: 'Unique WhatsApp identifiers for the participants', 113 | example: ['6281288888887@c.us'] 114 | } 115 | } 116 | } 117 | } 118 | */ 119 | try { 120 | const { chatId, contactIds, participantIds } = req.body 121 | const client = sessions.get(req.params.sessionId) 122 | const chat = await client.getChatById(chatId) 123 | if (!chat.isGroup) { throw new Error('The chat is not a group') } 124 | // support old property name 125 | const addParticipants = participantIds || contactIds 126 | const participantIdsArray = Array.isArray(addParticipants) ? addParticipants : [addParticipants] 127 | const result = await chat.removeParticipants(participantIdsArray) 128 | res.json({ success: true, result }) 129 | } catch (error) { 130 | sendErrorResponse(res, 500, error.message) 131 | } 132 | } 133 | 134 | /** 135 | * Promotes participants in a group chat to admin 136 | * 137 | * @async 138 | * @function 139 | * @param {Object} req - Express request object 140 | * @param {Object} res - Express response object 141 | * @returns {Promise} Returns a JSON object with success flag and updated participants list 142 | * @throws {Error} If chat is not a group 143 | */ 144 | const promoteParticipants = async (req, res) => { 145 | /* 146 | #swagger.summary = 'Promote the participants' 147 | #swagger.description = 'Promote participants by ID to admins' 148 | #swagger.requestBody = { 149 | required: true, 150 | schema: { 151 | type: 'object', 152 | properties: { 153 | chatId: { 154 | type: 'string', 155 | description: 'Unique WhatsApp id for the given chat group', 156 | example: 'XXXXXXXXXX@g.us' 157 | }, 158 | participantIds: { 159 | type: 'array', 160 | description: 'Unique WhatsApp identifiers for the participants', 161 | example: ['6281288888887@c.us'] 162 | } 163 | } 164 | } 165 | } 166 | */ 167 | try { 168 | const { chatId, contactIds, participantIds } = req.body 169 | const client = sessions.get(req.params.sessionId) 170 | const chat = await client.getChatById(chatId) 171 | if (!chat.isGroup) { throw new Error('The chat is not a group') } 172 | // support old property name 173 | const addParticipants = participantIds || contactIds 174 | const participantIdsArray = Array.isArray(addParticipants) ? addParticipants : [addParticipants] 175 | const result = await chat.promoteParticipants(participantIdsArray) 176 | res.json({ success: true, result }) 177 | } catch (error) { 178 | sendErrorResponse(res, 500, error.message) 179 | } 180 | } 181 | 182 | /** 183 | * Demotes admin participants in a group chat 184 | * 185 | * @async 186 | * @function 187 | * @param {Object} req - Express request object 188 | * @param {Object} res - Express response object 189 | * @returns {Promise} Returns a JSON object with success flag and updated participants list 190 | * @throws {Error} If chat is not a group 191 | */ 192 | const demoteParticipants = async (req, res) => { 193 | /* 194 | #swagger.summary = 'Demote the participants' 195 | #swagger.description = 'Demote participants by ID to regular users' 196 | #swagger.requestBody = { 197 | required: true, 198 | schema: { 199 | type: 'object', 200 | properties: { 201 | chatId: { 202 | type: 'string', 203 | description: 'Unique WhatsApp id for the given chat group', 204 | example: 'XXXXXXXXXX@g.us' 205 | }, 206 | participantIds: { 207 | type: 'array', 208 | description: 'Unique WhatsApp identifiers for the participants', 209 | example: ['6281288888887@c.us'] 210 | } 211 | } 212 | } 213 | } 214 | */ 215 | try { 216 | const { chatId, contactIds, participantIds } = req.body 217 | const client = sessions.get(req.params.sessionId) 218 | const chat = await client.getChatById(chatId) 219 | if (!chat.isGroup) { throw new Error('The chat is not a group') } 220 | // support old property name 221 | const addParticipants = participantIds || contactIds 222 | const participantIdsArray = Array.isArray(addParticipants) ? addParticipants : [addParticipants] 223 | const result = await chat.demoteParticipants(participantIdsArray) 224 | res.json({ success: true, result }) 225 | } catch (error) { 226 | sendErrorResponse(res, 500, error.message) 227 | } 228 | } 229 | 230 | /** 231 | * Gets the invite code for a group chat 232 | * 233 | * @async 234 | * @function 235 | * @param {Object} req - Express request object 236 | * @param {Object} res - Express response object 237 | * @returns {Promise} Returns a JSON object with success flag and invite code 238 | * @throws {Error} If chat is not a group 239 | */ 240 | const getInviteCode = async (req, res) => { 241 | /* 242 | #swagger.summary = 'Get the invite code' 243 | #swagger.description = 'Get the invite code for a specific group' 244 | */ 245 | try { 246 | const { chatId } = req.body 247 | const client = sessions.get(req.params.sessionId) 248 | const chat = await client.getChatById(chatId) 249 | if (!chat.isGroup) { throw new Error('The chat is not a group') } 250 | const inviteCode = await chat.getInviteCode() 251 | res.json({ success: true, inviteCode }) 252 | } catch (error) { 253 | sendErrorResponse(res, 500, error.message) 254 | } 255 | } 256 | 257 | /** 258 | * Sets the subject of a group chat 259 | * 260 | * @async 261 | * @function 262 | * @param {Object} req - Express request object 263 | * @param {Object} res - Express response object 264 | * @returns {Promise} Returns a JSON object with success flag and updated chat object 265 | * @throws {Error} If chat is not a group 266 | */ 267 | const setSubject = async (req, res) => { 268 | /* 269 | #swagger.summary = 'Update the group subject' 270 | #swagger.requestBody = { 271 | required: true, 272 | schema: { 273 | type: 'object', 274 | properties: { 275 | chatId: { 276 | type: 'string', 277 | description: 'Unique WhatsApp id for the given chat group', 278 | example: 'XXXXXXXXXX@g.us' 279 | }, 280 | subject: { 281 | type: 'string', 282 | description: 'Group subject', 283 | example: '' 284 | } 285 | } 286 | } 287 | } 288 | */ 289 | try { 290 | const { chatId, subject } = req.body 291 | const client = sessions.get(req.params.sessionId) 292 | const chat = await client.getChatById(chatId) 293 | if (!chat.isGroup) { throw new Error('The chat is not a group') } 294 | const result = await chat.setSubject(subject) 295 | res.json({ success: true, result }) 296 | } catch (error) { 297 | sendErrorResponse(res, 500, error.message) 298 | } 299 | } 300 | 301 | /** 302 | * Sets the description of a group chat 303 | * 304 | * @async 305 | * @function 306 | * @param {Object} req - Express request object 307 | * @param {Object} res - Express response object 308 | * @returns {Promise} Returns a JSON object with success flag and updated chat object 309 | * @throws {Error} If chat is not a group 310 | */ 311 | const setDescription = async (req, res) => { 312 | /* 313 | #swagger.summary = 'Update the group description' 314 | #swagger.requestBody = { 315 | required: true, 316 | schema: { 317 | type: 'object', 318 | properties: { 319 | chatId: { 320 | type: 'string', 321 | description: 'Unique WhatsApp id for the given chat group', 322 | example: 'XXXXXXXXXX@g.us' 323 | }, 324 | description: { 325 | type: 'string', 326 | description: 'Group description', 327 | example: '' 328 | } 329 | } 330 | } 331 | } 332 | */ 333 | try { 334 | const { chatId, description } = req.body 335 | const client = sessions.get(req.params.sessionId) 336 | const chat = await client.getChatById(chatId) 337 | if (!chat.isGroup) { throw new Error('The chat is not a group') } 338 | const result = await chat.setDescription(description) 339 | res.json({ success: true, result }) 340 | } catch (error) { 341 | sendErrorResponse(res, 500, error.message) 342 | } 343 | } 344 | 345 | /** 346 | * Leaves a group chat 347 | * 348 | * @async 349 | * @function 350 | * @param {Object} req - Express request object 351 | * @param {Object} res - Express response object 352 | * @returns {Promise} Returns a JSON object with success flag and outcome of leaving the chat 353 | * @throws {Error} If chat is not a group 354 | */ 355 | const leave = async (req, res) => { 356 | /* 357 | #swagger.summary = 'Leave the group' 358 | */ 359 | try { 360 | const { chatId } = req.body 361 | const client = sessions.get(req.params.sessionId) 362 | const chat = await client.getChatById(chatId) 363 | if (!chat.isGroup) { throw new Error('The chat is not a group') } 364 | const result = await chat.leave() 365 | res.json({ success: true, result }) 366 | } catch (error) { 367 | sendErrorResponse(res, 500, error.message) 368 | } 369 | } 370 | 371 | /** 372 | * Revokes the invite link for a group chat based on the provided chatId 373 | * 374 | * @async 375 | * @function revokeInvite 376 | * @param {object} req - The request object 377 | * @param {object} res - The response object 378 | * @param {string} req.body.chatId - The chatId of the group chat to revoke the invite for 379 | * @param {string} req.params.sessionId - The sessionId of the client making the request 380 | * @throws {Error} The chat is not a group. 381 | * @returns {Promise} - A JSON response with success true and the new invite code for the group chat 382 | */ 383 | const revokeInvite = async (req, res) => { 384 | /* 385 | #swagger.summary = 'Invalidate the invite code' 386 | #swagger.description = 'Invalidate the current group invite code and generates a new one' 387 | */ 388 | try { 389 | const { chatId } = req.body 390 | const client = sessions.get(req.params.sessionId) 391 | const chat = await client.getChatById(chatId) 392 | if (!chat.isGroup) { throw new Error('The chat is not a group') } 393 | const result = await chat.revokeInvite() 394 | res.json({ success: true, result }) 395 | } catch (error) { 396 | sendErrorResponse(res, 500, error.message) 397 | } 398 | } 399 | 400 | /** 401 | * Sets admins-only status of a group chat's info or messages. 402 | * 403 | * @async 404 | * @function setInfoAdminsOnly 405 | * @param {Object} req - Request object. 406 | * @param {Object} res - Response object. 407 | * @param {string} req.params.sessionId - ID of the user's session. 408 | * @param {Object} req.body - Request body. 409 | * @param {string} req.body.chatId - ID of the group chat. 410 | * @param {boolean} req.body.adminsOnly - Desired admins-only status. 411 | * @returns {Promise} Promise representing the success or failure of the operation. 412 | * @throws {Error} If the chat is not a group. 413 | */ 414 | const setInfoAdminsOnly = async (req, res) => { 415 | /* 416 | #swagger.summary = 'Update the info group settings' 417 | #swagger.summary = 'Update the group settings to only allow admins to edit group info (title, description, photo).' 418 | #swagger.requestBody = { 419 | required: true, 420 | schema: { 421 | type: 'object', 422 | properties: { 423 | chatId: { 424 | type: 'string', 425 | description: 'Unique WhatsApp id for the given chat group', 426 | example: 'XXXXXXXXXX@g.us' 427 | }, 428 | adminsOnly: { 429 | type: 'boolean', 430 | description: 'Enable or disable this option', 431 | example: true 432 | } 433 | } 434 | } 435 | } 436 | */ 437 | try { 438 | const { chatId, adminsOnly = true } = req.body 439 | const client = sessions.get(req.params.sessionId) 440 | const chat = await client.getChatById(chatId) 441 | if (!chat.isGroup) { throw new Error('The chat is not a group') } 442 | const result = await chat.setInfoAdminsOnly(adminsOnly) 443 | res.json({ success: true, result }) 444 | } catch (error) { 445 | sendErrorResponse(res, 500, error.message) 446 | } 447 | } 448 | 449 | /** 450 | * Sets admins-only status of a group chat's messages. 451 | * 452 | * @async 453 | * @function setMessagesAdminsOnly 454 | * @param {Object} req - Request object. 455 | * @param {Object} res - Response object. 456 | * @param {string} req.params.sessionId - ID of the user's session. 457 | * @param {Object} req.body - Request body. 458 | * @param {string} req.body.chatId - ID of the group chat. 459 | * @param {boolean} req.body.adminsOnly - Desired admins-only status. 460 | * @returns {Promise} Promise representing the success or failure of the operation. 461 | * @throws {Error} If the chat is not a group. 462 | */ 463 | const setMessagesAdminsOnly = async (req, res) => { 464 | /* 465 | #swagger.summary = 'Update the message group settings' 466 | #swagger.summary = 'Update the group settings to only allow admins to send messages.' 467 | #swagger.requestBody = { 468 | required: true, 469 | schema: { 470 | type: 'object', 471 | properties: { 472 | chatId: { 473 | type: 'string', 474 | description: 'Unique WhatsApp id for the given chat group', 475 | example: 'XXXXXXXXXX@g.us' 476 | }, 477 | adminsOnly: { 478 | type: 'boolean', 479 | description: 'Enable or disable this option', 480 | example: true 481 | } 482 | } 483 | } 484 | } 485 | */ 486 | try { 487 | const { chatId, adminsOnly = true } = req.body 488 | const client = sessions.get(req.params.sessionId) 489 | const chat = await client.getChatById(chatId) 490 | if (!chat.isGroup) { throw new Error('The chat is not a group') } 491 | const result = await chat.setMessagesAdminsOnly(adminsOnly) 492 | res.json({ success: true, result }) 493 | } catch (error) { 494 | sendErrorResponse(res, 500, error.message) 495 | } 496 | } 497 | 498 | /** 499 | * Set the group Picture 500 | * @param {Object} req - The request object. 501 | * @param {Object} res - The response object. 502 | * @param {Object} req.body.pictureMimetype - The mimetype of the image. 503 | * @param {Object} req.body.pictureData - The new group picture in base64 format. 504 | * @param {Object} req.body.chatId - ID of the group chat. 505 | * @param {string} req.params.sessionId - The ID of the session for the user. 506 | * @returns {Object} Returns a JSON object with a success status and the result of the function. 507 | * @throws {Error} If there is an issue setting the group picture, an error will be thrown. 508 | */ 509 | const setPicture = async (req, res) => { 510 | /* 511 | #swagger.summary = 'Set the group picture' 512 | #swagger.requestBody = { 513 | required: true, 514 | schema: { 515 | type: 'object', 516 | properties: { 517 | chatId: { 518 | type: 'string', 519 | description: 'Unique WhatsApp id for the given chat group', 520 | example: 'XXXXXXXXXX@g.us' 521 | }, 522 | pictureMimeType: { 523 | type: 'string', 524 | description: 'MIME type of the attachment' 525 | }, 526 | pictureData: { 527 | type: 'string', 528 | description: 'Base64-encoded data of the file' 529 | } 530 | } 531 | } 532 | } 533 | */ 534 | try { 535 | const { pictureMimeType, pictureData, chatId } = req.body 536 | const client = sessions.get(req.params.sessionId) 537 | const chat = await client.getChatById(chatId) 538 | if (!chat.isGroup) { throw new Error('The chat is not a group') } 539 | const media = new MessageMedia(pictureMimeType, pictureData) 540 | const result = await chat.setPicture(media) 541 | res.json({ success: true, result }) 542 | } catch (error) { 543 | sendErrorResponse(res, 500, error.message) 544 | } 545 | } 546 | 547 | /** 548 | * Delete the group Picture 549 | * @param {Object} req - The request object. 550 | * @param {Object} res - The response object. 551 | * @param {Object} req.body.chatId - ID of the group chat. 552 | * @param {string} req.params.sessionId - The ID of the session for the user. 553 | * @returns {Object} Returns a JSON object with a success status and the result of the function. 554 | * @throws {Error} If there is an issue setting the group picture, an error will be thrown. 555 | */ 556 | const deletePicture = async (req, res) => { 557 | /* 558 | #swagger.summary = 'Delete the group picture' 559 | */ 560 | try { 561 | const { chatId } = req.body 562 | const client = sessions.get(req.params.sessionId) 563 | const chat = await client.getChatById(chatId) 564 | if (!chat.isGroup) { throw new Error('The chat is not a group') } 565 | const result = await chat.deletePicture() 566 | res.json({ success: true, result }) 567 | } catch (error) { 568 | sendErrorResponse(res, 500, error.message) 569 | } 570 | } 571 | 572 | /** 573 | * Get an array of membership requests 574 | * @param {Object} req - The request object. 575 | * @param {Object} res - The response object. 576 | * @param {Object} req.body.chatId - ID of the group chat. 577 | * @param {string} req.params.sessionId - The ID of the session for the user. 578 | * @returns {Object} Returns a JSON object with a success status and the result of the function. 579 | * @throws {Error} If there is an issue setting the group picture, an error will be thrown. 580 | */ 581 | const getGroupMembershipRequests = async (req, res) => { 582 | /* 583 | #swagger.summary = 'Get the membership requests' 584 | */ 585 | try { 586 | const { chatId } = req.body 587 | const client = sessions.get(req.params.sessionId) 588 | const chat = await client.getChatById(chatId) 589 | if (!chat.isGroup) { throw new Error('The chat is not a group') } 590 | const result = await chat.getGroupMembershipRequests() 591 | res.json({ success: true, result }) 592 | } catch (error) { 593 | sendErrorResponse(res, 500, error.message) 594 | } 595 | } 596 | 597 | /** 598 | * Approve membership requests if any 599 | * @param {Object} req - The request object. 600 | * @param {Object} res - The response object. 601 | * @param {Object} req.body.chatId - ID of the group chat. 602 | * @param {string} req.params.sessionId - The ID of the session for the user. 603 | * @returns {Object} Returns a JSON object with a success status and the result of the function. 604 | * @throws {Error} If there is an issue setting the group picture, an error will be thrown. 605 | */ 606 | const approveGroupMembershipRequests = async (req, res) => { 607 | /* 608 | #swagger.summary = 'Approve membership request' 609 | #swagger.requestBody = { 610 | required: true, 611 | schema: { 612 | type: 'object', 613 | properties: { 614 | chatId: { 615 | type: 'string', 616 | description: 'Unique WhatsApp id for the given chat group', 617 | example: 'XXXXXXXXXX@g.us' 618 | }, 619 | options: { 620 | type: 'object', 621 | description: 'Options for performing a membership request action', 622 | example: { requesterIds: [], sleep: [250, 500] } 623 | } 624 | } 625 | } 626 | } 627 | */ 628 | try { 629 | const { chatId, options = {} } = req.body 630 | const client = sessions.get(req.params.sessionId) 631 | const chat = await client.getChatById(chatId) 632 | if (!chat.isGroup) { throw new Error('The chat is not a group') } 633 | const result = await chat.approveGroupMembershipRequests(options) 634 | res.json({ success: true, result }) 635 | } catch (error) { 636 | sendErrorResponse(res, 500, error.message) 637 | } 638 | } 639 | 640 | /** 641 | * Reject membership requests if any 642 | * @param {Object} req - The request object. 643 | * @param {Object} res - The response object. 644 | * @param {Object} req.body.chatId - ID of the group chat. 645 | * @param {string} req.params.sessionId - The ID of the session for the user. 646 | * @returns {Object} Returns a JSON object with a success status and the result of the function. 647 | * @throws {Error} If there is an issue setting the group picture, an error will be thrown. 648 | */ 649 | const rejectGroupMembershipRequests = async (req, res) => { 650 | /* 651 | #swagger.summary = 'Reject membership request' 652 | #swagger.requestBody = { 653 | required: true, 654 | schema: { 655 | type: 'object', 656 | properties: { 657 | chatId: { 658 | type: 'string', 659 | description: 'Unique WhatsApp id for the given chat group', 660 | example: 'XXXXXXXXXX@g.us' 661 | }, 662 | options: { 663 | type: 'object', 664 | description: 'Options for performing a membership request action', 665 | example: { requesterIds: [], sleep: [250, 500] } 666 | } 667 | } 668 | } 669 | } 670 | */ 671 | try { 672 | const { chatId, options = {} } = req.body 673 | const client = sessions.get(req.params.sessionId) 674 | const chat = await client.getChatById(chatId) 675 | if (!chat.isGroup) { throw new Error('The chat is not a group') } 676 | const result = await chat.rejectGroupMembershipRequests(options) 677 | res.json({ success: true, result }) 678 | } catch (error) { 679 | sendErrorResponse(res, 500, error.message) 680 | } 681 | } 682 | 683 | /** 684 | * Executes a method on the group associated with the given sessionId. 685 | * 686 | * @async 687 | * @function 688 | * @param {Object} req - The HTTP request object containing the chatId and sessionId. 689 | * @param {string} req.body.chatId - The unique identifier of the chat. 690 | * @param {string} req.params.sessionId - The unique identifier of the session associated with the client to use. 691 | * @param {Object} res - The HTTP response object. 692 | * @returns {Promise} - A Promise that resolves with a JSON object containing a success flag and the result of the operation. 693 | * @throws {Error} - If an error occurs during the operation, it is thrown and handled by the catch block. 694 | */ 695 | const runMethod = async (req, res) => { 696 | /* 697 | #swagger.summary = 'Execute a method on the group' 698 | #swagger.description = 'Execute a method on the group and return the result' 699 | #swagger.requestBody = { 700 | required: true, 701 | schema: { 702 | type: 'object', 703 | properties: { 704 | chatId: { 705 | type: 'string', 706 | description: 'Unique WhatsApp id for the given chat group', 707 | example: 'XXXXXXXXXX@g.us' 708 | }, 709 | method: { 710 | type: 'string', 711 | description: 'The name of the method to execute', 712 | example: 'getLabels' 713 | }, 714 | options: { 715 | anyOf: [ 716 | { type: 'object' }, 717 | { type: 'string' } 718 | ], 719 | description: 'The options to pass to the method', 720 | } 721 | } 722 | }, 723 | } 724 | */ 725 | try { 726 | const { chatId, method, options } = req.body 727 | const client = sessions.get(req.params.sessionId) 728 | const chat = await client.getChatById(chatId) 729 | if (!chat.isGroup) { throw new Error('The chat is not a group') } 730 | if (typeof chat[method] !== 'function') { 731 | throw new Error('Method is not implemented') 732 | } 733 | const result = options ? await chat[method](options) : await chat[method]() 734 | res.json({ success: true, data: result }) 735 | } catch (error) { 736 | sendErrorResponse(res, 500, error.message) 737 | } 738 | } 739 | 740 | module.exports = { 741 | getClassInfo, 742 | addParticipants, 743 | demoteParticipants, 744 | getInviteCode, 745 | leave, 746 | promoteParticipants, 747 | removeParticipants, 748 | revokeInvite, 749 | setDescription, 750 | setInfoAdminsOnly, 751 | setMessagesAdminsOnly, 752 | setSubject, 753 | setPicture, 754 | deletePicture, 755 | getGroupMembershipRequests, 756 | approveGroupMembershipRequests, 757 | rejectGroupMembershipRequests, 758 | runMethod 759 | } 760 | -------------------------------------------------------------------------------- /src/controllers/messageController.js: -------------------------------------------------------------------------------- 1 | const { MessageMedia, Location, Poll } = require('whatsapp-web.js') 2 | const { Readable } = require('stream') 3 | const { sessions } = require('../sessions') 4 | const { sendErrorResponse, decodeBase64 } = require('../utils') 5 | 6 | /** 7 | * Get message by its ID from a given chat using the provided client. 8 | * @async 9 | * @function 10 | * @param {object} client - The chat client. 11 | * @param {string} messageId - The ID of the message to get. 12 | * @param {string} chatId - The ID of the chat to search in. 13 | * @returns {Promise} - A Promise that resolves with the message object that matches the provided ID, or undefined if no such message exists. 14 | * @throws {Error} - Throws an error if the provided client, message ID or chat ID is invalid. 15 | */ 16 | const _getMessageById = async (client, messageId, chatId) => { 17 | const chat = await client.getChatById(chatId) 18 | const messages = await chat.fetchMessages({ limit: 100 }) 19 | return messages.find((message) => { return message.id.id === messageId }) 20 | } 21 | 22 | /** 23 | * Gets information about a message's class. 24 | * @async 25 | * @function 26 | * @param {Object} req - The request object. 27 | * @param {Object} res - The response object. 28 | * @param {string} req.params.sessionId - The session ID. 29 | * @param {string} req.body.messageId - The message ID. 30 | * @param {string} req.body.chatId - The chat ID. 31 | * @returns {Promise} - A Promise that resolves with no value when the function completes. 32 | */ 33 | const getClassInfo = async (req, res) => { 34 | /* 35 | #swagger.summary = 'Get message' 36 | */ 37 | try { 38 | const { messageId, chatId } = req.body 39 | const client = sessions.get(req.params.sessionId) 40 | const message = await _getMessageById(client, messageId, chatId) 41 | if (!message) { throw new Error('Message not found') } 42 | res.json({ success: true, message }) 43 | } catch (error) { 44 | sendErrorResponse(res, 500, error.message) 45 | } 46 | } 47 | 48 | /** 49 | * Deletes a message. 50 | * @async 51 | * @function 52 | * @param {Object} req - The request object. 53 | * @param {Object} res - The response object. 54 | * @param {string} req.params.sessionId - The session ID. 55 | * @param {string} req.body.messageId - The message ID. 56 | * @param {string} req.body.chatId - The chat ID. 57 | * @param {boolean} req.body.everyone - Whether to delete the message for everyone or just the sender. 58 | * @returns {Promise} - A Promise that resolves with no value when the function completes. 59 | */ 60 | const deleteMessage = async (req, res) => { 61 | /* 62 | #swagger.summary = 'Delete a message from the chat' 63 | #swagger.requestBody = { 64 | required: true, 65 | schema: { 66 | type: 'object', 67 | properties: { 68 | chatId: { 69 | type: 'string', 70 | description: 'The chat id which contains the message', 71 | example: '6281288888888@c.us' 72 | }, 73 | messageId: { 74 | type: 'string', 75 | description: 'Unique WhatsApp identifier for the message', 76 | example: 'ABCDEF999999999' 77 | }, 78 | everyone: { 79 | type: 'boolean', 80 | description: 'If true and the message is sent by the current user or the user is an admin, will delete it for everyone in the chat.', 81 | example: true 82 | }, 83 | clearMedia: { 84 | type: 'boolean', 85 | description: 'If true, any associated media will also be deleted from a device', 86 | example: true 87 | } 88 | } 89 | } 90 | } 91 | */ 92 | try { 93 | const { messageId, chatId, everyone, clearMedia = true } = req.body 94 | const client = sessions.get(req.params.sessionId) 95 | const message = await _getMessageById(client, messageId, chatId) 96 | if (!message) { throw new Error('Message not found') } 97 | const result = await message.delete(everyone, clearMedia) 98 | res.json({ success: true, result }) 99 | } catch (error) { 100 | sendErrorResponse(res, 500, error.message) 101 | } 102 | } 103 | 104 | /** 105 | * Downloads media from a message. 106 | * @async 107 | * @function 108 | * @param {Object} req - The request object. 109 | * @param {Object} res - The response object. 110 | * @param {string} req.params.sessionId - The session ID. 111 | * @param {string} req.body.messageId - The message ID. 112 | * @param {string} req.body.chatId - The chat ID. 113 | * @param {boolean} req.body.everyone - Whether to download the media for everyone or just the sender. 114 | * @returns {Promise} - A Promise that resolves with no value when the function completes. 115 | */ 116 | const downloadMedia = async (req, res) => { 117 | /* 118 | #swagger.summary = 'Download attached message media' 119 | */ 120 | try { 121 | const { messageId, chatId } = req.body 122 | const client = sessions.get(req.params.sessionId) 123 | const message = await _getMessageById(client, messageId, chatId) 124 | if (!message) { throw new Error('Message not found') } 125 | if (!message.hasMedia) { throw new Error('Message media not found') } 126 | const messageMedia = await message.downloadMedia() 127 | res.json({ success: true, messageMedia }) 128 | } catch (error) { 129 | sendErrorResponse(res, 500, error.message) 130 | } 131 | } 132 | 133 | /** 134 | * Downloads media from a message and sends it as binary data. 135 | * @async 136 | * @function 137 | * @param {Object} req - The request object. 138 | * @param {Object} res - The response object. 139 | * @param {string} req.params.sessionId - The session ID. 140 | * @param {string} req.body.messageId - The message ID. 141 | * @param {string} req.body.chatId - The chat ID. 142 | * @returns {Promise} - A Promise that resolves with no value when the function completes. 143 | */ 144 | const downloadMediaAsData = async (req, res) => { 145 | /* 146 | #swagger.summary = 'Download attached message media as binary data' 147 | */ 148 | try { 149 | const { messageId, chatId } = req.body 150 | const client = sessions.get(req.params.sessionId) 151 | const message = await _getMessageById(client, messageId, chatId) 152 | if (!message) { throw new Error('Message not found') } 153 | if (!message.hasMedia) { throw new Error('Message media not found') } 154 | const { data, mimetype, filename, filesize } = await message.downloadMedia() 155 | /* #swagger.responses[200] = { 156 | description: 'Binary data.' 157 | } 158 | */ 159 | res.writeHead(200, { 160 | ...(mimetype && { 'Content-Type': mimetype }), 161 | ...(filesize && { 'Content-Length': filesize }), 162 | ...(filename && { 'Content-Disposition': `attachment; filename=${encodeURIComponent(filename)}` }) 163 | }) 164 | const readableStream = new Readable({ 165 | read () { 166 | for (const chunk of decodeBase64(data)) { 167 | this.push(chunk) 168 | } 169 | this.push(null) 170 | } 171 | }) 172 | readableStream.on('end', () => { 173 | res.end() 174 | }) 175 | readableStream.pipe(res) 176 | } catch (error) { 177 | sendErrorResponse(res, 500, error.message) 178 | } 179 | } 180 | 181 | /** 182 | * Forwards a message to a destination chat. 183 | * @async 184 | * @function forward 185 | * @param {Object} req - The request object received by the server. 186 | * @param {Object} req.body - The body of the request object. 187 | * @param {string} req.body.messageId - The ID of the message to forward. 188 | * @param {string} req.body.chatId - The ID of the chat that contains the message to forward. 189 | * @param {string} req.body.destinationChatId - The ID of the chat to forward the message to. 190 | * @param {string} req.params.sessionId - The ID of the session to use the Telegram API with. 191 | * @param {Object} res - The response object to be sent back to the client. 192 | * @returns {Object} - The response object with a JSON body containing the result of the forward operation. 193 | * @throws Will throw an error if the message is not found or if there is an error during the forward operation. 194 | */ 195 | const forward = async (req, res) => { 196 | /* 197 | #swagger.summary = 'Delete a message from the chat' 198 | #swagger.requestBody = { 199 | required: true, 200 | schema: { 201 | type: 'object', 202 | properties: { 203 | chatId: { 204 | type: 'string', 205 | description: 'The chat id which contains the message', 206 | example: '6281288888888@c.us' 207 | }, 208 | messageId: { 209 | type: 'string', 210 | description: 'Unique WhatsApp identifier for the message', 211 | example: 'ABCDEF999999999' 212 | }, 213 | destinationChatId: { 214 | type: 'string', 215 | description: 'The chat id to forward the message to', 216 | example: '6281288888889@c.us' 217 | } 218 | } 219 | } 220 | } 221 | */ 222 | try { 223 | const { messageId, chatId, destinationChatId } = req.body 224 | const client = sessions.get(req.params.sessionId) 225 | const message = await _getMessageById(client, messageId, chatId) 226 | if (!message) { throw new Error('Message not found') } 227 | const result = await message.forward(destinationChatId) 228 | res.json({ success: true, result }) 229 | } catch (error) { 230 | sendErrorResponse(res, 500, error.message) 231 | } 232 | } 233 | 234 | /** 235 | * Gets information about a message. 236 | * @async 237 | * @function getInfo 238 | * @param {Object} req - The request object received by the server. 239 | * @param {Object} req.body - The body of the request object. 240 | * @param {string} req.body.messageId - The ID of the message to get information about. 241 | * @param {string} req.body.chatId - The ID of the chat that contains the message to get information about. 242 | * @param {string} req.params.sessionId - The ID of the session to use the Telegram API with. 243 | * @param {Object} res - The response object to be sent back to the client. 244 | * @returns {Object} - The response object with a JSON body containing the information about the message. 245 | * @throws Will throw an error if the message is not found or if there is an error during the get info operation. 246 | */ 247 | const getInfo = async (req, res) => { 248 | /* 249 | #swagger.summary = 'Get information about message delivery status' 250 | #swagger.description = 'May return null if the message does not exist or is not sent by you.' 251 | */ 252 | try { 253 | const { messageId, chatId } = req.body 254 | const client = sessions.get(req.params.sessionId) 255 | const message = await _getMessageById(client, messageId, chatId) 256 | if (!message) { throw new Error('Message not found') } 257 | const info = await message.getInfo() 258 | res.json({ success: true, info }) 259 | } catch (error) { 260 | sendErrorResponse(res, 500, error.message) 261 | } 262 | } 263 | 264 | /** 265 | * Retrieves a list of contacts mentioned in a specific message 266 | * 267 | * @async 268 | * @function 269 | * @param {Object} req - The HTTP request object 270 | * @param {Object} req.body - The request body 271 | * @param {string} req.body.messageId - The ID of the message to retrieve mentions from 272 | * @param {string} req.body.chatId - The ID of the chat where the message was sent 273 | * @param {string} req.params.sessionId - The ID of the session for the client making the request 274 | * @param {Object} res - The HTTP response object 275 | * @returns {Promise} - The JSON response with the list of contacts 276 | * @throws {Error} - If there's an error retrieving the message or mentions 277 | */ 278 | const getMentions = async (req, res) => { 279 | /* 280 | #swagger.summary = 'Get the contacts mentioned' 281 | */ 282 | try { 283 | const { messageId, chatId } = req.body 284 | const client = sessions.get(req.params.sessionId) 285 | const message = await _getMessageById(client, messageId, chatId) 286 | if (!message) { throw new Error('Message not found') } 287 | const contacts = await message.getMentions() 288 | res.json({ success: true, contacts }) 289 | } catch (error) { 290 | sendErrorResponse(res, 500, error.message) 291 | } 292 | } 293 | 294 | /** 295 | * Retrieves the order information contained in a specific message 296 | * 297 | * @async 298 | * @function 299 | * @param {Object} req - The HTTP request object 300 | * @param {Object} req.body - The request body 301 | * @param {string} req.body.messageId - The ID of the message to retrieve the order from 302 | * @param {string} req.body.chatId - The ID of the chat where the message was sent 303 | * @param {string} req.params.sessionId - The ID of the session for the client making the request 304 | * @param {Object} res - The HTTP response object 305 | * @returns {Promise} - The JSON response with the order information 306 | * @throws {Error} - If there's an error retrieving the message or order information 307 | */ 308 | const getOrder = async (req, res) => { 309 | /* 310 | #swagger.summary = 'Get the order details' 311 | */ 312 | try { 313 | const { messageId, chatId } = req.body 314 | const client = sessions.get(req.params.sessionId) 315 | const message = await _getMessageById(client, messageId, chatId) 316 | if (!message) { throw new Error('Message not found') } 317 | const order = await message.getOrder() 318 | res.json({ success: true, order }) 319 | } catch (error) { 320 | sendErrorResponse(res, 500, error.message) 321 | } 322 | } 323 | 324 | /** 325 | * Retrieves the payment information from a specific message identified by its ID. 326 | * 327 | * @async 328 | * @function getPayment 329 | * @param {Object} req - The HTTP request object. 330 | * @param {Object} res - The HTTP response object. 331 | * @param {string} req.params.sessionId - The session ID associated with the client making the request. 332 | * @param {Object} req.body - The message ID and chat ID associated with the message to retrieve payment information from. 333 | * @param {string} req.body.messageId - The ID of the message to retrieve payment information from. 334 | * @param {string} req.body.chatId - The ID of the chat the message is associated with. 335 | * @returns {Object} An object containing a success status and the payment information for the specified message. 336 | * @throws {Object} If the specified message is not found or if an error occurs during the retrieval process. 337 | */ 338 | const getPayment = async (req, res) => { 339 | /* 340 | #swagger.summary = 'Get the payment details' 341 | */ 342 | try { 343 | const { messageId, chatId } = req.body 344 | const client = sessions.get(req.params.sessionId) 345 | const message = await _getMessageById(client, messageId, chatId) 346 | if (!message) { throw new Error('Message not found') } 347 | const payment = await message.getPayment() 348 | res.json({ success: true, payment }) 349 | } catch (error) { 350 | sendErrorResponse(res, 500, error.message) 351 | } 352 | } 353 | 354 | /** 355 | * Retrieves the quoted message information from a specific message identified by its ID. 356 | * 357 | * @async 358 | * @function getQuotedMessage 359 | * @param {Object} req - The HTTP request object. 360 | * @param {Object} res - The HTTP response object. 361 | * @param {string} req.params.sessionId - The session ID associated with the client making the request. 362 | * @param {Object} req.body - The message ID and chat ID associated with the message to retrieve quoted message information from. 363 | * @param {string} req.body.messageId - The ID of the message to retrieve quoted message information from. 364 | * @param {string} req.body.chatId - The ID of the chat the message is associated with. 365 | * @returns {Object} An object containing a success status and the quoted message information for the specified message. 366 | * @throws {Object} If the specified message is not found or if an error occurs during the retrieval process. 367 | */ 368 | const getQuotedMessage = async (req, res) => { 369 | /* 370 | #swagger.summary = 'Get the quoted message' 371 | */ 372 | try { 373 | const { messageId, chatId } = req.body 374 | const client = sessions.get(req.params.sessionId) 375 | const message = await _getMessageById(client, messageId, chatId) 376 | if (!message) { throw new Error('Message not found') } 377 | const quotedMessage = await message.getQuotedMessage() 378 | res.json({ success: true, quotedMessage }) 379 | } catch (error) { 380 | sendErrorResponse(res, 500, error.message) 381 | } 382 | } 383 | 384 | /** 385 | * React to a specific message in a chat 386 | * 387 | * @async 388 | * @function react 389 | * @param {Object} req - The HTTP request object containing the request parameters and body. 390 | * @param {Object} res - The HTTP response object to send the result. 391 | * @param {string} req.params.sessionId - The ID of the session to use. 392 | * @param {Object} req.body - The body of the request. 393 | * @param {string} req.body.messageId - The ID of the message to react to. 394 | * @param {string} req.body.chatId - The ID of the chat the message is in. 395 | * @param {string} req.body.reaction - The reaction to add to the message. 396 | * @returns {Object} The HTTP response containing the result of the operation. 397 | * @throws {Error} If there was an error during the operation. 398 | */ 399 | const react = async (req, res) => { 400 | /* 401 | #swagger.summary = 'React with an emoji' 402 | #swagger.requestBody = { 403 | required: true, 404 | schema: { 405 | type: 'object', 406 | properties: { 407 | chatId: { 408 | type: 'string', 409 | description: 'The chat id which contains the message', 410 | example: '6281288888888@c.us' 411 | }, 412 | messageId: { 413 | type: 'string', 414 | description: 'Unique WhatsApp identifier for the message', 415 | example: 'ABCDEF999999999' 416 | }, 417 | reaction: { 418 | type: 'string', 419 | description: 'Emoji to react with. Send an empty string to remove the reaction.', 420 | example: '👍' 421 | } 422 | } 423 | } 424 | } 425 | */ 426 | try { 427 | const { messageId, chatId, reaction = '' } = req.body 428 | const client = sessions.get(req.params.sessionId) 429 | const message = await _getMessageById(client, messageId, chatId) 430 | if (!message) { throw new Error('Message not found') } 431 | const result = await message.react(reaction) 432 | res.json({ success: true, result }) 433 | } catch (error) { 434 | sendErrorResponse(res, 500, error.message) 435 | } 436 | } 437 | 438 | /** 439 | * Reply to a specific message in a chat 440 | * 441 | * @async 442 | * @function reply 443 | * @param {Object} req - The HTTP request object containing the request parameters and body. 444 | * @param {Object} res - The HTTP response object to send the result. 445 | * @param {string} req.params.sessionId - The ID of the session to use. 446 | * @param {string} req.body.messageId - The ID of the message to reply to. 447 | * @param {string} req.body.chatId - The ID of the chat the message is in. 448 | * @param {string} req.body.content - The content of the message to send. 449 | * @param {string} req.body.destinationChatId - The ID of the chat to send the reply to. 450 | * @param {Object} req.body.options - Additional options for sending the message. 451 | * @returns {Object} The HTTP response containing the result of the operation. 452 | * @throws {Error} If there was an error during the operation. 453 | */ 454 | const reply = async (req, res) => { 455 | /* 456 | #swagger.summary = 'Send a message as a reply' 457 | #swagger.requestBody = { 458 | required: true, 459 | '@content': { 460 | "application/json": { 461 | schema: { 462 | type: 'object', 463 | properties: { 464 | chatId: { 465 | type: 'string', 466 | description: 'The chat id which contains the message', 467 | example: '6281288888888@c.us' 468 | }, 469 | messageId: { 470 | type: 'string', 471 | description: 'Unique WhatsApp identifier for the message', 472 | example: 'ABCDEF999999999' 473 | }, 474 | contentType: { 475 | type: 'string', 476 | description: 'The type of message content, must be one of the following: string, MessageMedia, MessageMediaFromURL, Location, Contact or Poll', 477 | }, 478 | content: { 479 | type: 'object', 480 | description: 'The content of the message, can be a string or an object', 481 | }, 482 | options: { 483 | type: 'object', 484 | description: 'The message send options', 485 | } 486 | } 487 | }, 488 | examples: { 489 | string: { value: { messageId: '3A80E857F9B44AF60C2C', chatId: '6281288888888@c.us', contentType: 'string', content: 'Reply text!' } } 490 | } 491 | } 492 | } 493 | } 494 | */ 495 | try { 496 | const { messageId, chatId, content, contentType, options } = req.body 497 | const client = sessions.get(req.params.sessionId) 498 | const message = await _getMessageById(client, messageId, chatId) 499 | if (!message) { throw new Error('Message not found') } 500 | let contentMessage 501 | switch (contentType) { 502 | case 'string': 503 | if (options?.media) { 504 | const media = options.media 505 | media.filename = null 506 | media.filesize = null 507 | options.media = new MessageMedia(media.mimetype, media.data, media.filename, media.filesize) 508 | } 509 | contentMessage = content 510 | break 511 | case 'MessageMediaFromURL': { 512 | contentMessage = await MessageMedia.fromUrl(content, { unsafeMime: true }) 513 | break 514 | } 515 | case 'MessageMedia': { 516 | contentMessage = new MessageMedia(content.mimetype, content.data, content.filename, content.filesize) 517 | break 518 | } 519 | case 'Location': { 520 | contentMessage = new Location(content.latitude, content.longitude, content.description) 521 | break 522 | } 523 | case 'Contact': { 524 | const contactId = content.contactId.endsWith('@c.us') ? content.contactId : `${content.contactId}@c.us` 525 | contentMessage = await client.getContactById(contactId) 526 | break 527 | } 528 | case 'Poll': { 529 | contentMessage = new Poll(content.pollName, content.pollOptions, content.options) 530 | // fix for poll events not being triggered (open the chat that you sent the poll) 531 | await client.interface.openChatWindow(chatId) 532 | break 533 | } 534 | default: 535 | return sendErrorResponse(res, 400, 'contentType invalid, must be string, MessageMedia, MessageMediaFromURL, Location, Contact or Poll') 536 | } 537 | const repliedMessage = await message.reply(contentMessage, chatId, options) 538 | res.json({ success: true, repliedMessage }) 539 | } catch (error) { 540 | sendErrorResponse(res, 500, error.message) 541 | } 542 | } 543 | 544 | /** 545 | * @function star 546 | * @async 547 | * @description Stars a message by message ID and chat ID. 548 | * @param {Object} req - The request object. 549 | * @param {Object} res - The response object. 550 | * @param {string} req.params.sessionId - The session ID. 551 | * @param {string} req.body.messageId - The message ID. 552 | * @param {string} req.body.chatId - The chat ID. 553 | * @returns {Promise} A Promise that resolves with the result of the message.star() call. 554 | * @throws {Error} If message is not found, it throws an error with the message "Message not found". 555 | */ 556 | const star = async (req, res) => { 557 | /* 558 | #swagger.summary = 'Star the message' 559 | */ 560 | try { 561 | const { messageId, chatId } = req.body 562 | const client = sessions.get(req.params.sessionId) 563 | const message = await _getMessageById(client, messageId, chatId) 564 | if (!message) { throw new Error('Message not found') } 565 | const result = await message.star() 566 | res.json({ success: true, result }) 567 | } catch (error) { 568 | sendErrorResponse(res, 500, error.message) 569 | } 570 | } 571 | 572 | /** 573 | * @function unstar 574 | * @async 575 | * @description Unstars a message by message ID and chat ID. 576 | * @param {Object} req - The request object. 577 | * @param {Object} res - The response object. 578 | * @param {string} req.params.sessionId - The session ID. 579 | * @param {string} req.body.messageId - The message ID. 580 | * @param {string} req.body.chatId - The chat ID. 581 | * @returns {Promise} A Promise that resolves with the result of the message.unstar() call. 582 | * @throws {Error} If message is not found, it throws an error with the message "Message not found". 583 | */ 584 | const unstar = async (req, res) => { 585 | /* 586 | #swagger.summary = 'Unstar the message' 587 | */ 588 | try { 589 | const { messageId, chatId } = req.body 590 | const client = sessions.get(req.params.sessionId) 591 | const message = await _getMessageById(client, messageId, chatId) 592 | if (!message) { throw new Error('Message not found') } 593 | const result = await message.unstar() 594 | res.json({ success: true, result }) 595 | } catch (error) { 596 | sendErrorResponse(res, 500, error.message) 597 | } 598 | } 599 | 600 | /** 601 | * @function getReactions 602 | * @async 603 | * @description Gets the reactions associated with the given message. 604 | * @param {Object} req - The request object. 605 | * @param {Object} res - The response object. 606 | * @param {string} req.params.sessionId - The session ID. 607 | * @param {string} req.body.messageId - The message ID. 608 | * @param {string} req.body.chatId - The chat ID. 609 | * @returns {Promise} A Promise that resolves with the result of the message.getReactions() call. 610 | * @throws {Error} If message is not found, it throws an error with the message "Message not found". 611 | */ 612 | const getReactions = async (req, res) => { 613 | /* 614 | #swagger.summary = 'Get the reactions associated' 615 | */ 616 | try { 617 | const { messageId, chatId } = req.body 618 | const client = sessions.get(req.params.sessionId) 619 | const message = await _getMessageById(client, messageId, chatId) 620 | if (!message) { throw new Error('Message not found') } 621 | const result = await message.getReactions() 622 | res.json({ success: true, result }) 623 | } catch (error) { 624 | sendErrorResponse(res, 500, error.message) 625 | } 626 | } 627 | 628 | /** 629 | * @function getReactions 630 | * @async 631 | * @description Gets groups mentioned in this message 632 | * @param {Object} req - The request object. 633 | * @param {Object} res - The response object. 634 | * @param {string} req.params.sessionId - The session ID. 635 | * @param {string} req.body.messageId - The message ID. 636 | * @param {string} req.body.chatId - The chat ID. 637 | * @returns {Promise} A Promise that resolves with the result of the message.getReactions() call. 638 | * @throws {Error} If message is not found, it throws an error with the message "Message not found". 639 | */ 640 | const getGroupMentions = async (req, res) => { 641 | /* 642 | #swagger.summary = 'Get groups mentioned in this message' 643 | */ 644 | try { 645 | const { messageId, chatId } = req.body 646 | const client = sessions.get(req.params.sessionId) 647 | const message = await _getMessageById(client, messageId, chatId) 648 | if (!message) { throw new Error('Message not found') } 649 | const result = await message.getGroupMentions() 650 | res.json({ success: true, result }) 651 | } catch (error) { 652 | sendErrorResponse(res, 500, error.message) 653 | } 654 | } 655 | 656 | /** 657 | * @function edit 658 | * @async 659 | * @description Edits the current message 660 | * @param {Object} req - The request object. 661 | * @param {Object} res - The response object. 662 | * @param {string} req.params.sessionId - The session ID. 663 | * @param {string} req.body.messageId - The message ID. 664 | * @param {string} req.body.chatId - The chat ID. 665 | * @returns {Promise} A Promise that resolves with the result of the message.edit() call. 666 | * @throws {Error} If message is not found, it throws an error with the message "Message not found". 667 | */ 668 | const edit = async (req, res) => { 669 | /* 670 | #swagger.summary = 'Edit the message' 671 | #swagger.requestBody = { 672 | required: true, 673 | schema: { 674 | type: 'object', 675 | properties: { 676 | chatId: { 677 | type: 'string', 678 | description: 'The chat id which contains the message', 679 | example: '6281288888888@c.us' 680 | }, 681 | messageId: { 682 | type: 'string', 683 | description: 'Unique WhatsApp identifier for the message', 684 | example: 'ABCDEF999999999' 685 | }, 686 | content: { 687 | type: 'string', 688 | description: 'The content of the message', 689 | }, 690 | options: { 691 | type: 'object', 692 | description: 'Options used when editing the message', 693 | } 694 | } 695 | } 696 | } 697 | */ 698 | try { 699 | const { messageId, chatId, content, options } = req.body 700 | const client = sessions.get(req.params.sessionId) 701 | const message = await _getMessageById(client, messageId, chatId) 702 | if (!message) { throw new Error('Message not found') } 703 | const editedMessage = await message.edit(content, options) 704 | res.json({ success: true, message: editedMessage }) 705 | } catch (error) { 706 | sendErrorResponse(res, 500, error.message) 707 | } 708 | } 709 | 710 | /** 711 | * @function getContact 712 | * @async 713 | * @description Gets groups mentioned in this message 714 | * @param {Object} req - The request object. 715 | * @param {Object} res - The response object. 716 | * @param {string} req.params.sessionId - The session ID. 717 | * @param {string} req.body.messageId - The message ID. 718 | * @param {string} req.body.chatId - The chat ID. 719 | * @returns {Promise} A Promise that resolves with the result of the message.getReactions() call. 720 | * @throws {Error} If message is not found, it throws an error with the message "Message not found". 721 | */ 722 | const getContact = async (req, res) => { 723 | /* 724 | #swagger.summary = 'Get the contact' 725 | */ 726 | try { 727 | const { messageId, chatId } = req.body 728 | const client = sessions.get(req.params.sessionId) 729 | const message = await _getMessageById(client, messageId, chatId) 730 | if (!message) { throw new Error('Message not found') } 731 | const contact = await message.getContact() 732 | res.json({ success: true, contact }) 733 | } catch (error) { 734 | sendErrorResponse(res, 500, error.message) 735 | } 736 | } 737 | 738 | /** 739 | * Executes a method on the message associated with the given sessionId. 740 | * 741 | * @async 742 | * @function 743 | * @param {Object} req - The HTTP request object containing the chatId and sessionId. 744 | * @param {string} req.body.chatId - The unique identifier of the chat. 745 | * @param {string} req.params.sessionId - The unique identifier of the session associated with the client to use. 746 | * @param {Object} res - The HTTP response object. 747 | * @returns {Promise} - A Promise that resolves with a JSON object containing a success flag and the result of the operation. 748 | * @throws {Error} - If an error occurs during the operation, it is thrown and handled by the catch block. 749 | */ 750 | const runMethod = async (req, res) => { 751 | /* 752 | #swagger.summary = 'Execute a method on the message' 753 | #swagger.description = 'Execute a method on the message and return the result' 754 | #swagger.requestBody = { 755 | required: true, 756 | schema: { 757 | type: 'object', 758 | properties: { 759 | chatId: { 760 | type: 'string', 761 | description: 'The chat id which contains the message', 762 | example: '6281288888888@c.us' 763 | }, 764 | messageId: { 765 | type: 'string', 766 | description: 'Unique WhatsApp identifier for the message', 767 | example: 'ABCDEF999999999' 768 | }, 769 | method: { 770 | type: 'string', 771 | description: 'The name of the method to execute', 772 | example: 'getInfo' 773 | }, 774 | options: { 775 | anyOf: [ 776 | { type: 'object' }, 777 | { type: 'string' } 778 | ], 779 | description: 'The options to pass to the method', 780 | } 781 | } 782 | }, 783 | } 784 | */ 785 | try { 786 | const { messageId, chatId, method, options } = req.body 787 | const client = sessions.get(req.params.sessionId) 788 | const message = await _getMessageById(client, messageId, chatId) 789 | if (!message) { throw new Error('Message not found') } 790 | if (typeof message[method] !== 'function') { 791 | throw new Error('Method is not implemented') 792 | } 793 | const result = options ? await message[method](options) : await message[method]() 794 | res.json({ success: true, data: result }) 795 | } catch (error) { 796 | sendErrorResponse(res, 500, error.message) 797 | } 798 | } 799 | 800 | module.exports = { 801 | getClassInfo, 802 | deleteMessage, 803 | downloadMedia, 804 | downloadMediaAsData, 805 | forward, 806 | getInfo, 807 | getMentions, 808 | getOrder, 809 | getPayment, 810 | getQuotedMessage, 811 | react, 812 | reply, 813 | star, 814 | unstar, 815 | getReactions, 816 | getGroupMentions, 817 | edit, 818 | getContact, 819 | runMethod 820 | } 821 | --------------------------------------------------------------------------------