├── prisma ├── migrations │ ├── migration_lock.toml │ └── 20251118163707_init │ │ └── migration.sql └── schema.prisma ├── .dockerignore ├── prisma.config.ts ├── .gitignore ├── src ├── infra │ ├── mappers │ │ ├── contactMapper.ts │ │ └── messageMapper.ts │ ├── webhook │ │ └── queue.ts │ ├── state │ │ ├── sessions.ts │ │ └── auth.ts │ ├── config │ │ └── env.ts │ ├── http │ │ ├── routes │ │ │ ├── media.ts │ │ │ ├── instances.ts │ │ │ ├── chat.ts │ │ │ ├── profile.ts │ │ │ ├── privacy.ts │ │ │ ├── messages.ts │ │ │ └── group.ts │ │ └── controllers │ │ │ ├── media.ts │ │ │ ├── instances.ts │ │ │ ├── privacy.ts │ │ │ ├── chat.ts │ │ │ ├── profile.ts │ │ │ ├── messages.ts │ │ │ └── group.ts │ └── baileys │ │ └── services.ts ├── shared │ ├── webhookWorker.ts │ ├── constants.ts │ ├── types.ts │ ├── guards.ts │ └── utils.ts ├── main.ts └── core │ ├── repositories │ └── instances.ts │ └── connection │ └── prisma.ts ├── .github └── workflows │ └── docker-image.yml ├── tsconfig.json ├── .env.example ├── LICENSE ├── docker-compose.yml ├── Dockerfile ├── package.json └── README.md /prisma/migrations/migration_lock.toml: -------------------------------------------------------------------------------- 1 | # Please do not edit this file manually 2 | # It should be added in your version-control system (e.g., Git) 3 | provider = "postgresql" 4 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | #output 2 | /dist 3 | /node_modules 4 | 5 | #env 6 | .env 7 | 8 | #packages 9 | /yarn.lock 10 | /package-lock.json 11 | 12 | #VsCode 13 | .vscode/* 14 | !.vscode/settings.json 15 | !.vscode/tasks.json 16 | !.vscode/launch.json 17 | !.vscode/extensions.json 18 | 19 | #Project 20 | /sessions/* 21 | /instances/* 22 | /src/generated/prisma 23 | -------------------------------------------------------------------------------- /prisma.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig, env } from "prisma/config"; 2 | import dotenv from "dotenv"; 3 | 4 | dotenv.config(); 5 | 6 | export default defineConfig({ 7 | schema: "prisma/schema.prisma", 8 | migrations: { 9 | path: "prisma/migrations", 10 | }, 11 | engine: "classic", 12 | datasource: { 13 | url: env("DATABASE_URL"), 14 | }, 15 | }); 16 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | #output 2 | /dist 3 | /node_modules 4 | 5 | #env 6 | .env 7 | 8 | #packages 9 | /yarn.lock 10 | /package-lock.json 11 | 12 | #VsCode 13 | .vscode/* 14 | !.vscode/settings.json 15 | !.vscode/tasks.json 16 | !.vscode/launch.json 17 | !.vscode/extensions.json 18 | 19 | #Project 20 | /sessions/* 21 | /instances/* 22 | /src/generated/prisma 23 | 24 | /src/generated/prisma 25 | 26 | /src/generated/prisma 27 | -------------------------------------------------------------------------------- /src/infra/mappers/contactMapper.ts: -------------------------------------------------------------------------------- 1 | import { Contact } from "@whiskeysockets/baileys"; 2 | 3 | export class ContactMapper { 4 | 5 | static toContact(numId: any): Contact { 6 | 7 | return { 8 | id: numId.jid, 9 | lid: numId.lid, 10 | phoneNumber: numId.jid.replace("@s.whatsapp.net", ""), 11 | name: numId.name 12 | }; 13 | } 14 | 15 | } 16 | -------------------------------------------------------------------------------- /.github/workflows/docker-image.yml: -------------------------------------------------------------------------------- 1 | name: Docker ZapToBox whatsapp API Image 2 | 3 | on: 4 | push: 5 | branches: [ "main" ] 6 | pull_request: 7 | branches: [ "main" ] 8 | 9 | jobs: 10 | 11 | build: 12 | 13 | runs-on: ubuntu-latest 14 | 15 | steps: 16 | - uses: actions/checkout@v4 17 | - name: Build the Docker image 18 | run: docker build . --file Dockerfile --tag zaptobox:$(date +%s) 19 | -------------------------------------------------------------------------------- /src/infra/webhook/queue.ts: -------------------------------------------------------------------------------- 1 | import { instanceStatus } from "../../shared/constants"; 2 | import { ConnectionStatus } from "../../shared/types"; 3 | import { startWebhookRetryLoop } from "../../shared/utils"; 4 | 5 | export default class Queue{ 6 | 7 | async start(){ 8 | startWebhookRetryLoop(this.getInstanceStatus); 9 | } 10 | 11 | getInstanceStatus(name: string): ConnectionStatus{ 12 | return instanceStatus.get(name) || "OFFLINE"; 13 | } 14 | 15 | } -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "rootDir": "./src", 4 | "outDir": "./dist", 5 | 6 | "module": "CommonJS", 7 | "target": "ES2020", 8 | "types": ["node"], 9 | 10 | "sourceMap": true, 11 | "declaration": true, 12 | "declarationMap": true, 13 | 14 | "noUncheckedIndexedAccess": true, 15 | "exactOptionalPropertyTypes": true, 16 | 17 | "strict": true, 18 | "verbatimModuleSyntax": false, 19 | "moduleDetection": "auto", 20 | "skipLibCheck": true, 21 | "esModuleInterop": true 22 | }, 23 | "exclude": ["node_modules", "dist", "./prisma.config.ts"] 24 | } 25 | -------------------------------------------------------------------------------- /src/shared/webhookWorker.ts: -------------------------------------------------------------------------------- 1 | import { parentPort } from 'worker_threads'; 2 | 3 | parentPort?.on('message', async (payload) => { 4 | try { 5 | const res = await fetch(payload.targetUrl, { 6 | method: "POST", 7 | headers: { "Content-Type": "application/json" }, 8 | body: JSON.stringify(payload), 9 | }); 10 | 11 | if (!res.ok) { 12 | parentPort?.postMessage({ success: false, error: `HTTP ${res.status}` }); 13 | } else { 14 | parentPort?.postMessage({ success: true }); 15 | } 16 | } catch (err) { 17 | parentPort?.postMessage({ success: false, error: (err as Error).message }); 18 | } 19 | }); 20 | -------------------------------------------------------------------------------- /src/infra/state/sessions.ts: -------------------------------------------------------------------------------- 1 | import * as fs from "fs"; 2 | import { instances, sessionsPath } from "../../shared/constants"; 3 | import path from "path"; 4 | import Instance from "../baileys/services"; 5 | 6 | export default class Sessions{ 7 | 8 | async start(){ 9 | 10 | if (!fs.existsSync(sessionsPath)) fs.mkdirSync(sessionsPath); 11 | 12 | const owners = fs.readdirSync(sessionsPath); 13 | for (const owner of owners) { 14 | const ownerPath = path.join(sessionsPath, owner); 15 | const instancesDirs = fs.readdirSync(ownerPath); 16 | for (const instanceName of instancesDirs) { 17 | const key = `${owner}_${instanceName}`; 18 | instances[key] = new Instance; 19 | await instances[key].create({ owner, instanceName, phoneNumber: undefined }); 20 | } 21 | } 22 | 23 | } 24 | 25 | } -------------------------------------------------------------------------------- /src/infra/mappers/messageMapper.ts: -------------------------------------------------------------------------------- 1 | import { MinimalMessage, WAMessage } from "@whiskeysockets/baileys"; 2 | import { Message as PrismaMessage } from "@prisma/client"; 3 | 4 | export class MessageMapper { 5 | 6 | static toMinimalMessage(row: any): MinimalMessage { 7 | const content = row.content as any; 8 | 9 | return { 10 | key: { 11 | remoteJid: row?.remoteJid, 12 | fromMe: row?.fromMe, 13 | id: row?.messageId 14 | }, 15 | messageTimestamp: Number(row?.messageTimestamp) 16 | }; 17 | } 18 | 19 | static toWAMessage(row: PrismaMessage): WAMessage { 20 | const content = row.content as any; 21 | 22 | return { 23 | key: { 24 | remoteJid: row?.remoteJid, 25 | fromMe: row?.fromMe, 26 | id: row?.messageId 27 | }, 28 | messageTimestamp: Number(row.messageTimestamp), 29 | message: content?.message 30 | }; 31 | } 32 | 33 | } 34 | -------------------------------------------------------------------------------- /src/infra/config/env.ts: -------------------------------------------------------------------------------- 1 | import dotenv from "dotenv"; 2 | dotenv.config({quiet: true}); 3 | 4 | export default class UserConfig{ 5 | 6 | static sessionFolderName: string = process.env.SESSION_FOLDER_NAME || "sessions"; 7 | static portConfig: string = process.env.PORT || "3000"; 8 | static jwtToken: string = process.env.JWT_TOKEN || ""; 9 | static webhookUrl: string = process.env.WEBHOOK_URL || "https://localhost"; 10 | static sessionClient: string = process.env.SESSION || "Linux"; 11 | static sessionName: string = process.env.PHONE_NAME || "Edge"; 12 | static proxyUrl: (string | undefined) = process.env.PROXY_URL; 13 | static useWebhookQueue: boolean = (process.env.WEBHOOK_QUEUE === 'true'); 14 | static webhook_queue_dir: string = process.env.WEBHOOK_QUEUE_DIR || "./webhook"; 15 | static webhook_interval: number = (Number(process.env.QUEUE_INTERVAL) * 60 * 1000) || 5 * 60 * 1000; 16 | static qrCodeLimit: number = Number(process.env.QRCODE_LIMIT || 5); 17 | static qrCodeTimeout: number = Number(process.env.QRCODE_TIMEOUT || 20); 18 | 19 | } -------------------------------------------------------------------------------- /src/infra/state/auth.ts: -------------------------------------------------------------------------------- 1 | import {Request, Response, NextFunction} from "express"; 2 | import jwt from "jsonwebtoken"; 3 | import UserConfig from "../config/env"; 4 | 5 | export default class Token{ 6 | 7 | async verify(req: Request, res: Response, next: NextFunction){ 8 | const token = req.headers?.authorization?.replace(/^Bearer\s+/i, "");; 9 | const secret = UserConfig.jwtToken; 10 | 11 | if(!token || !secret){ 12 | console.log("Token or secret is missing"); 13 | return res.status(401).json({ 14 | error: "Invalid Token" 15 | }); 16 | } 17 | 18 | try { 19 | 20 | if(token === secret){ 21 | next(); 22 | }else{ 23 | jwt.verify(token, secret); 24 | next(); 25 | } 26 | } catch (error) { 27 | console.error("Token verification error:", error); 28 | return res.status(401).json({ 29 | error: "Invalid Token" 30 | }); 31 | } 32 | } 33 | 34 | } -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | /** 2 | * Environment configuration file 3 | * Rename this file to .env and fill in the appropriate values 4 | **/ 5 | 6 | /* 7 | * Database Connection URL 8 | */ 9 | DATABASE_URL="postgresql://username:password@localhost:5432/dbname?schema=public" 10 | 11 | /* 12 | * Session Folder Name 13 | */ 14 | SESSION_FOLDER_NAME=sessions 15 | 16 | /* 17 | * Application Port 18 | */ 19 | PORT=3000 20 | 21 | /* 22 | * QrCode Limit and Timeout 23 | * Timeout in seconds 24 | */ 25 | QRCODE_LIMIT=5 26 | QRCODE_TIMEOUT=20 27 | 28 | /* 29 | * API Token for authentication 30 | */ 31 | JWT_TOKEN=yourtokensecurehere123456789 32 | 33 | /* 34 | * Webhook URL 35 | */ 36 | WEBHOOK_URL=http://localhost:8443/webhook 37 | 38 | /* 39 | * Client Session Name and Browser 40 | * Change PHONE_NAME to "Desktop" if you would like to receive all history messages 41 | */ 42 | SESSION=Linux 43 | PHONE_NAME=Edge 44 | 45 | /* 46 | * Proxy URL (if needed) 47 | */ 48 | PROXY_URL= 49 | 50 | /* 51 | * Webhook Queue Settings 52 | * Queue interval in minutes 53 | */ 54 | WEBHOOK_QUEUE=true 55 | WEBHOOK_QUEUE_DIR=./webhook-queue 56 | QUEUE_INTERVAL=5 57 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 Jean Kássio 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 | -------------------------------------------------------------------------------- /prisma/schema.prisma: -------------------------------------------------------------------------------- 1 | // schema.prisma 2 | generator client { 3 | provider = "prisma-client-js" 4 | } 5 | 6 | datasource db { 7 | provider = "postgresql" 8 | url = env("DATABASE_URL") 9 | } 10 | 11 | model Message { 12 | id Int @id @default(autoincrement()) 13 | instance String 14 | messageId String 15 | remoteJid String 16 | senderLid String? 17 | fromMe Boolean 18 | pushName String? 19 | content Json 20 | status String? 21 | messageTimestamp BigInt 22 | 23 | @@unique([instance, messageId], name: "instance_messageId") 24 | @@index([instance]) 25 | @@index([remoteJid]) 26 | } 27 | 28 | model Contact { 29 | id Int @id @default(autoincrement()) 30 | instance String 31 | name String? 32 | jid String? 33 | lid String? 34 | 35 | @@unique([instance, jid], name: "instance_jid") 36 | @@unique([instance, lid], name: "instance_lid") 37 | @@index([instance]) 38 | } 39 | 40 | model Chat { 41 | id Int @id @default(autoincrement()) 42 | instance String 43 | jid String 44 | data Json 45 | 46 | @@unique([instance, jid], name: "instance_chat_jid") 47 | @@index([instance]) 48 | } 49 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import express from "express"; 2 | import Token from "./infra/state/auth"; 3 | import InstanceRoutes from "./infra/http/routes/instances"; 4 | import MessageRoutes from "./infra/http/routes/messages"; 5 | import UserConfig from "./infra/config/env"; 6 | import Sessions from "./infra/state/sessions"; 7 | import Queue from "./infra/webhook/queue"; 8 | import MediaRoutes from "./infra/http/routes/media"; 9 | import ChatRoutes from "./infra/http/routes/chat"; 10 | import GroupRoutes from "./infra/http/routes/group"; 11 | import ProfileRoutes from "./infra/http/routes/profile"; 12 | import PrivacyRoutes from "./infra/http/routes/privacy"; 13 | 14 | async function bootstrap(){ 15 | 16 | const app = express(); 17 | app.use(express.json()); 18 | 19 | app.use((req, res, next) => { 20 | (new Token).verify(req, res, next); 21 | }); 22 | 23 | app.use("/instances/", (new InstanceRoutes).get()); 24 | app.use("/messages/", (new MessageRoutes).get()); 25 | app.use("/media/", (new MediaRoutes).get()); 26 | app.use("/chat/", (new ChatRoutes).get()); 27 | app.use("/group/", (new GroupRoutes).get()); 28 | app.use("/profile/", (new ProfileRoutes).get()); 29 | app.use("/privacy/", (new PrivacyRoutes).get()); 30 | 31 | app.listen(UserConfig.portConfig, async () => { 32 | await (new Sessions).start(); 33 | (new Queue).start(); 34 | console.log(`Server running in port ${UserConfig.portConfig}`); 35 | }); 36 | 37 | } 38 | 39 | bootstrap(); -------------------------------------------------------------------------------- /src/shared/constants.ts: -------------------------------------------------------------------------------- 1 | import path from "path"; 2 | import { ConnectionStatus, InstanceData } from "./types"; 3 | import Instance from "../infra/baileys/services"; 4 | import UserConfig from "../infra/config/env"; 5 | import { BaileysEventMap } from "@whiskeysockets/baileys"; 6 | 7 | export const SessionFolderName = UserConfig.sessionFolderName; 8 | export const sessionsPath = path.join(__dirname, "../..", SessionFolderName); 9 | 10 | export const instanceConnection: Record = {}; 11 | export const instanceStatus = new Map(); 12 | export const instances: Record = {}; 13 | 14 | export const baileysEvents = [ 15 | "creds.update", 16 | "connection.update", 17 | "messaging-history.set", 18 | "chats.upsert", 19 | "chats.update", 20 | "chats.delete", 21 | "lid-mapping.update", 22 | "presence.update", 23 | "contacts.upsert", 24 | "contacts.update", 25 | "messages.upsert", 26 | "messages.update", 27 | "messages.delete", 28 | "messages.media-update", 29 | "messages.reaction", 30 | "message-receipt.update", 31 | "groups.upsert", 32 | "groups.update", 33 | "group-participants.update", 34 | "group.join-request", 35 | "blocklist.set", 36 | "blocklist.update", 37 | "call", 38 | "labels.edit", 39 | "labels.association", 40 | "newsletter.reaction", 41 | "newsletter.view", 42 | "newsletter-participants.update", 43 | "newsletter-settings.update", 44 | ] as const satisfies (keyof BaileysEventMap)[] -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3.9" 2 | 3 | services: 4 | app: 5 | build: . 6 | container_name: zaptobox_api 7 | restart: always 8 | ports: 9 | - "3000:3000" 10 | environment: 11 | - NODE_ENV=production 12 | 13 | - DATABASE_URL="postgresql://username:password@localhost:5432/dbname?schema=public" 14 | 15 | - SESSION_FOLDER_NAME=sessions 16 | 17 | - PORT=3000 18 | 19 | - QRCODE_LIMIT=5 20 | - QRCODE_TIMEOUT=20 21 | 22 | - JWT_TOKEN=yourtokensecurehere123456789 23 | 24 | - WEBHOOK_URL=http://localhost:8443/webhook 25 | 26 | - SESSION=Linux 27 | - PHONE_NAME=Edge 28 | 29 | - PROXY_URL= 30 | 31 | - WEBHOOK_QUEUE_DIR=./webhook-queue 32 | - QUEUE_INTERVAL=5 33 | 34 | volumes: 35 | - ./sessions:/zaptobox/sessions 36 | depends_on: 37 | db: 38 | condition: service_healthy 39 | networks: 40 | - zaptobox_network 41 | 42 | db: 43 | image: postgres:16 44 | container_name: zaptobox_db 45 | restart: always 46 | environment: 47 | POSTGRES_USER: admin 48 | POSTGRES_PASSWORD: password 49 | POSTGRES_DB: zaptobox 50 | volumes: 51 | - pgdata:/var/lib/postgresql/data 52 | ports: 53 | - "5432:5432" 54 | healthcheck: 55 | test: ["CMD-SHELL", "pg_isready -U admin -d zaptobox"] 56 | interval: 10s 57 | timeout: 5s 58 | retries: 5 59 | networks: 60 | - zaptobox_network 61 | 62 | volumes: 63 | pgdata: 64 | 65 | networks: 66 | zaptobox_network: 67 | driver: bridge 68 | -------------------------------------------------------------------------------- /src/infra/http/routes/media.ts: -------------------------------------------------------------------------------- 1 | import express, { Request, Response } from "express"; 2 | import MediaController from "../controllers/media"; 3 | 4 | export default class MediaRoutes{ 5 | 6 | private router = express.Router(); 7 | 8 | get(){ 9 | 10 | this.router 11 | .post("/download/:owner/:instanceName", async (req: Request, res: Response) => { 12 | 13 | const owner = req.params.owner; 14 | const instanceName = req.params.instanceName; 15 | 16 | if(!owner || !instanceName){ 17 | return res.status(400).json({ error: "Owner and instanceName are required." }); 18 | } 19 | 20 | const { messageId, isBase64} = req.body; 21 | 22 | if(!owner || !instanceName || !messageId){ 23 | return res.status(400).json({ error: "Field 'messageId' is required." }); 24 | } 25 | 26 | const mediaController = new MediaController(owner, instanceName); 27 | const result = await mediaController.getMedia(messageId, isBase64); 28 | 29 | if(result.error){ 30 | return res.status(400).json({ error: result.error }); 31 | }else if(result?.base64){ 32 | return res.json(result); 33 | }else{ 34 | res.setHeader("Content-Type", result.mimeType); 35 | res.send(result.buffer); 36 | } 37 | 38 | }) 39 | return this.router; 40 | 41 | } 42 | 43 | } -------------------------------------------------------------------------------- /prisma/migrations/20251118163707_init/migration.sql: -------------------------------------------------------------------------------- 1 | -- CreateTable 2 | CREATE TABLE "Message" ( 3 | "id" SERIAL NOT NULL, 4 | "instance" TEXT NOT NULL, 5 | "messageId" TEXT NOT NULL, 6 | "remoteJid" TEXT NOT NULL, 7 | "senderLid" TEXT, 8 | "fromMe" BOOLEAN NOT NULL, 9 | "pushName" TEXT, 10 | "content" JSONB NOT NULL, 11 | "status" TEXT, 12 | "messageTimestamp" BIGINT NOT NULL, 13 | 14 | CONSTRAINT "Message_pkey" PRIMARY KEY ("id") 15 | ); 16 | 17 | -- CreateTable 18 | CREATE TABLE "Contact" ( 19 | "id" SERIAL NOT NULL, 20 | "instance" TEXT NOT NULL, 21 | "name" TEXT, 22 | "jid" TEXT, 23 | "lid" TEXT, 24 | 25 | CONSTRAINT "Contact_pkey" PRIMARY KEY ("id") 26 | ); 27 | 28 | -- CreateTable 29 | CREATE TABLE "Chat" ( 30 | "id" SERIAL NOT NULL, 31 | "instance" TEXT NOT NULL, 32 | "jid" TEXT NOT NULL, 33 | "data" JSONB NOT NULL, 34 | 35 | CONSTRAINT "Chat_pkey" PRIMARY KEY ("id") 36 | ); 37 | 38 | -- CreateIndex 39 | CREATE INDEX "Message_instance_idx" ON "Message"("instance"); 40 | 41 | -- CreateIndex 42 | CREATE INDEX "Message_remoteJid_idx" ON "Message"("remoteJid"); 43 | 44 | -- CreateIndex 45 | CREATE UNIQUE INDEX "Message_instance_messageId_key" ON "Message"("instance", "messageId"); 46 | 47 | -- CreateIndex 48 | CREATE INDEX "Contact_instance_idx" ON "Contact"("instance"); 49 | 50 | -- CreateIndex 51 | CREATE UNIQUE INDEX "Contact_instance_jid_key" ON "Contact"("instance", "jid"); 52 | 53 | -- CreateIndex 54 | CREATE UNIQUE INDEX "Contact_instance_lid_key" ON "Contact"("instance", "lid"); 55 | 56 | -- CreateIndex 57 | CREATE INDEX "Chat_instance_idx" ON "Chat"("instance"); 58 | 59 | -- CreateIndex 60 | CREATE UNIQUE INDEX "Chat_instance_jid_key" ON "Chat"("instance", "jid"); 61 | -------------------------------------------------------------------------------- /src/core/repositories/instances.ts: -------------------------------------------------------------------------------- 1 | import * as fs from "fs"; 2 | import path from "path"; 3 | import { InstanceInfo } from "../../shared/types"; 4 | import { instanceConnection, sessionsPath } from "../../shared/constants"; 5 | 6 | export default class InstancesRepository { 7 | 8 | async list(ownerFilter?: string): Promise { 9 | 10 | const results: InstanceInfo[] = []; 11 | 12 | if(!fs.existsSync(sessionsPath)){ 13 | return results; 14 | } 15 | 16 | const owners = (ownerFilter ? [ownerFilter] : await this.getOwnersPath(sessionsPath)); 17 | 18 | for(const owner of owners){ 19 | 20 | const ownerPath = path.join(sessionsPath, owner); 21 | 22 | if(!fs.existsSync(ownerPath)){ 23 | continue; 24 | } 25 | 26 | const instancesDirs = await this.getOwnersPath(ownerPath); 27 | 28 | for(const instanceName of instancesDirs){ 29 | 30 | const key = `${owner}_${instanceName}`; 31 | const loaded = instanceConnection[key]; 32 | 33 | results.push({ 34 | instanceName, 35 | owner, 36 | connectionStatus: loaded ? loaded.connectionStatus : "OFFLINE", 37 | profilePictureUrl: loaded ? loaded.profilePictureUrl : undefined 38 | }); 39 | 40 | } 41 | 42 | } 43 | 44 | return results; 45 | 46 | } 47 | 48 | async getOwnersPath(opath: string): Promise { 49 | return fs.readdirSync(opath).filter((f) => { 50 | try{ 51 | return fs.statSync(path.join(opath, f)).isDirectory(); 52 | }catch{ 53 | return false; 54 | } 55 | }); 56 | } 57 | 58 | } 59 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | ############################################ 2 | # BASE IMAGE 3 | ############################################ 4 | FROM node:22-slim AS base 5 | 6 | RUN apt-get update && apt-get install -y \ 7 | git \ 8 | ffmpeg \ 9 | openssl \ 10 | openssh-client \ 11 | ca-certificates \ 12 | --no-install-recommends && \ 13 | apt-get clean && rm -rf /var/lib/apt/lists/* 14 | 15 | WORKDIR /zaptobox 16 | 17 | 18 | ############################################ 19 | # BUILDER 20 | ############################################ 21 | FROM base AS builder 22 | 23 | WORKDIR /zaptobox 24 | 25 | RUN git config --global url."https://github.com/".insteadOf ssh://git@github.com/ && \ 26 | git config --global url."https://".insteadOf git:// && \ 27 | git config --global url."https://".insteadOf ssh:// && \ 28 | git config --global http.sslVerify false 29 | 30 | COPY package*.json ./ 31 | COPY tsconfig.json ./ 32 | COPY prisma ./prisma 33 | 34 | RUN npm install 35 | 36 | COPY src ./src 37 | 38 | RUN npx prisma generate 39 | 40 | RUN npm run build 41 | 42 | 43 | ############################################ 44 | # PRODUCTION IMAGE 45 | ############################################ 46 | FROM base AS production 47 | 48 | WORKDIR /zaptobox 49 | 50 | LABEL com.api.version="1.2.6" 51 | LABEL com.api.maintainer="https://github.com/jeankassio" 52 | LABEL com.api.repository="https://github.com/jeankassio/ZapToBox-Whatsapp-Api" 53 | LABEL com.api.issues="https://github.com/jeankassio/ZapToBox-Whatsapp-Api/issues" 54 | 55 | RUN git config --global url."https://github.com/".insteadOf ssh://git@github.com/ && \ 56 | git config --global url."https://".insteadOf git:// && \ 57 | git config --global url."https://".insteadOf ssh:// && \ 58 | git config --global http.sslVerify false 59 | 60 | COPY package*.json ./ 61 | COPY prisma ./prisma 62 | 63 | RUN npm install --omit=dev && \ 64 | npx prisma generate 65 | 66 | COPY --from=builder /zaptobox/dist ./dist 67 | 68 | RUN mkdir -p /zaptobox/sessions 69 | 70 | ENV DOCKER_ENV=true 71 | 72 | EXPOSE 3000 73 | 74 | CMD ["sh", "-c", "npx prisma migrate deploy && node dist/main.js"] 75 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "zaptobox-whatsapp-api", 3 | "version": "1.2.6", 4 | "description": "REST API platform for integrating systems with WhatsApp with stability, multiple instances, message sending, and event webhooking. Ideal for automation, bots, and enterprise systems.", 5 | "main": "./dist/main.js", 6 | "scripts": { 7 | "dev": "ts-node-dev --respawn --transpile-only src/main.ts", 8 | "build": "tsc", 9 | "build:start": "tsc && node dist/main.js", 10 | "build:pm2": "tsc && pm2 start dist/main.js --name 'ZapToBox Whatsapp Api'", 11 | "pm2": "pm2 start dist/main.js --name 'ZapToBox Whatsapp Api'" 12 | }, 13 | "repository": { 14 | "type": "git", 15 | "url": "git@github.com:jeankassio/ZapToBox-Whatsapp-Api.git" 16 | }, 17 | "keywords": [ 18 | "whatsapp", 19 | "api", 20 | "whatsapp-api", 21 | "whatsapp-business", 22 | "automation", 23 | "bots", 24 | "prisma", 25 | "baileys", 26 | "express", 27 | "nodejs", 28 | "typescript", 29 | "zaptobox", 30 | "jeankassio", 31 | "webhook", 32 | "multi-instance", 33 | "message-sending", 34 | "enterprise", 35 | "stability", 36 | "integration", 37 | "systems", 38 | "rest", 39 | "rest-api", 40 | "whatsapp-automation", 41 | "whatsapp-bots", 42 | "whatsapp-integration", 43 | "whatsapp-multi-instance", 44 | "whatsapp-message-sending", 45 | "whatsapp-enterprise", 46 | "whatsapp-stability", 47 | "whatsapp-zaptobox", 48 | "zaptoapi" 49 | ], 50 | "author": "Jean Kassio ", 51 | "license": "MIT", 52 | "bugs": { 53 | "url": "https://github.com/jeankassio/ZapToBox-Whatsapp-Api/issues" 54 | }, 55 | "homepage": "https://github.com/jeankassio/ZapToBox-Whatsapp-Api#readme", 56 | "type": "commonjs", 57 | "dependencies": { 58 | "@prisma/client": "^6.19.0", 59 | "@whiskeysockets/baileys": "^7.0.0-rc.9", 60 | "dotenv": "^17.2.3", 61 | "express": "^5.1.0", 62 | "https-proxy-agent": "^7.0.6", 63 | "jimp": "^1.6.0", 64 | "jsonwebtoken": "^9.0.2", 65 | "link-preview-js": "^3.1.0", 66 | "node-cache": "^5.1.2", 67 | "pino": "^10.1.0", 68 | "prisma": "^6.19.0", 69 | "qrcode": "^1.5.4", 70 | "socks-proxy-agent": "^8.0.5", 71 | "undici": "^7.16.0" 72 | }, 73 | "devDependencies": { 74 | "@types/express": "^5.0.5", 75 | "@types/jsonwebtoken": "^9.0.10", 76 | "@types/node": "^24.10.1", 77 | "@types/qrcode": "^1.5.6", 78 | "ts-node-dev": "^2.0.0", 79 | "typescript": "^5.9.3" 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/infra/http/controllers/media.ts: -------------------------------------------------------------------------------- 1 | import PrismaConnection from "../../../core/connection/prisma"; 2 | import { instances } from "../../../shared/constants"; 3 | import { downloadMediaMessage, WASocket, WAMessage } from "@whiskeysockets/baileys"; 4 | 5 | export default class MediaController { 6 | 7 | private sock: WASocket | undefined; 8 | 9 | constructor(owner: string, instanceName: string){ 10 | const key = `${owner}_${instanceName}`; 11 | this.sock = instances[key]?.getSock(); 12 | } 13 | 14 | async getMedia(messageId: string, isBase64: boolean = false){ 15 | 16 | if(!this.sock){ 17 | return { 18 | success: false, 19 | error: "Instance not connected.", 20 | }; 21 | } 22 | 23 | const msg = await PrismaConnection.getMessageById(messageId) as WAMessage | undefined; 24 | 25 | if(!msg || !msg.message){ 26 | return { 27 | success: false, 28 | error: "Message not found.", 29 | }; 30 | } 31 | 32 | try{ 33 | 34 | const content = msg.message; 35 | const isMediaMessage = Object.keys(content).find(k => k.startsWith("image") || k.startsWith("video") || k.startsWith("audio") || k.startsWith("document") || k.startsWith("sticker")); 36 | 37 | if(!isMediaMessage){ 38 | console.error("Message is not a media message."); 39 | return { 40 | success: false, 41 | error: "Message is not a media message.", 42 | }; 43 | } 44 | 45 | try{ 46 | 47 | const buffer = await downloadMediaMessage(msg, "buffer", {}, {logger: this.sock?.logger, reuploadRequest: this.sock?.updateMediaMessage!}); 48 | 49 | const mimeType = (content as any)[isMediaMessage!].mimetype || "application/octet-stream"; 50 | 51 | if(isBase64){ 52 | 53 | const base64Data = buffer.toString('base64'); 54 | 55 | return { 56 | success: true, 57 | base64: `data:${mimeType};base64,${base64Data}`, 58 | }; 59 | 60 | }else{ 61 | return { 62 | success: true, 63 | buffer: buffer, 64 | mimeType: mimeType, 65 | }; 66 | } 67 | 68 | }catch(err){ 69 | return { 70 | success: false, 71 | error: "Error downloading media message.", 72 | }; 73 | } 74 | 75 | 76 | 77 | }catch(err){ 78 | console.error("Error fetching media message:", err); 79 | return { 80 | success: false, 81 | error: "Error fetching media message.", 82 | }; 83 | } 84 | 85 | } 86 | 87 | } -------------------------------------------------------------------------------- /src/infra/http/routes/instances.ts: -------------------------------------------------------------------------------- 1 | import express, { Request, Response } from "express"; 2 | import InstancesController from "../controllers/instances"; 3 | 4 | export default class InstanceRoutes{ 5 | 6 | private router = express.Router(); 7 | 8 | get(){ 9 | 10 | const instancesController = new InstancesController(); 11 | 12 | this.router 13 | .post("/create", async (req: Request, res: Response) => { 14 | 15 | const { owner, instanceName, phoneNumber } = req.body; 16 | 17 | if (!owner || !instanceName) { 18 | return res.status(400).json({ error: "Fields 'owner' and 'instanceName' is required" }); 19 | } 20 | 21 | const result = await instancesController.create(owner, instanceName, phoneNumber); 22 | 23 | if(result?.error){ 24 | return res.status(500).json(result); 25 | }else{ 26 | return res.status(200).json(result); 27 | } 28 | 29 | }) 30 | .get("/get", async (req: Request, res: Response) => { 31 | 32 | const owner = req.query?.owner?.toString().trim(); 33 | 34 | const result = await instancesController.get(owner); 35 | 36 | if(result?.error){ 37 | return res.status(500).json(result); 38 | }else{ 39 | return res.status(200).json(result); 40 | } 41 | 42 | }) 43 | .get("/connect/:owner/:instanceName", async (req: Request, res: Response) => { 44 | 45 | const owner = req.params.owner; 46 | const instanceName = req.params.instanceName; 47 | 48 | if (!owner || !instanceName) { 49 | return res.status(400).json({ error: "Fields 'owner' and 'instanceName' is required" }); 50 | } 51 | 52 | const result = await instancesController.connect(owner, instanceName); 53 | 54 | if(result?.error){ 55 | return res.status(500).json(result); 56 | }else{ 57 | return res.status(200).json(result); 58 | } 59 | 60 | }) 61 | .delete("/delete/:owner/:instanceName", async (req: Request, res: Response) => { 62 | 63 | const owner = req.params.owner; 64 | const instanceName = req.params.instanceName; 65 | 66 | if (!owner || !instanceName) { 67 | return res.status(400).json({ error: "Fields 'owner' and 'instanceName' is required" }); 68 | } 69 | 70 | const result = await instancesController.delete(owner, instanceName); 71 | 72 | if(result?.error){ 73 | return res.status(500).json(result); 74 | }else{ 75 | return res.status(200).json(result); 76 | } 77 | 78 | }) 79 | 80 | return this.router; 81 | 82 | } 83 | 84 | } -------------------------------------------------------------------------------- /src/shared/types.ts: -------------------------------------------------------------------------------- 1 | import {WAMessage, WAMessageKey, WASocket} from "@whiskeysockets/baileys"; 2 | import { BlobOptions } from "buffer"; 3 | 4 | export type ConnectionStatus = "ONLINE" | "OFFLINE" | "REMOVED"; 5 | export type StatusPresence = "available" | "unavailable" | "composing" | "recording" | "paused"; 6 | 7 | export interface InstanceInfo { 8 | instanceName: string; 9 | owner: string; 10 | connectionStatus: ConnectionStatus; 11 | profilePictureUrl?: string | undefined; 12 | instanceJid?: string | null; 13 | } 14 | 15 | export interface InstanceData extends InstanceInfo{ 16 | socket?: WASocket; 17 | } 18 | 19 | export type InstanceCreated = { 20 | success: boolean; 21 | message?: string; 22 | error?: string; 23 | instance?: InstanceInfo; 24 | qrCode?: string; 25 | pairingCode?: string; 26 | } 27 | 28 | export interface WebhookPayload { 29 | event: string; 30 | instance: InstanceInfo; 31 | data: WAMessage[]; 32 | targetUrl: string; 33 | } 34 | 35 | export interface MessageWebhook extends WAMessage{ 36 | messageType?: string; 37 | } 38 | 39 | export interface ProxyAgent{ 40 | wsAgent?: any, 41 | fetchAgent?: any 42 | } 43 | 44 | export interface Contact{ 45 | id?: string; 46 | name?: string; 47 | lid?: string; 48 | } 49 | 50 | export interface ForwardMessage{ 51 | forward: string 52 | } 53 | 54 | interface MentionUser{ 55 | mentions?: string[] 56 | } 57 | 58 | interface ViewOnceMessage{ 59 | viewOnce?: boolean 60 | } 61 | 62 | export interface TextMessage extends MentionUser{ 63 | text: string 64 | } 65 | 66 | export interface LocationMessage{ 67 | location: { 68 | degreesLatitude: number, 69 | degreesLongitude: number 70 | } 71 | } 72 | 73 | export interface ContactMessage{ 74 | displayName: string, 75 | waid: number, 76 | phoneNumber: string 77 | } 78 | 79 | export interface ReactionMessage{ 80 | emoji: string, 81 | messageId: string 82 | } 83 | 84 | export interface PinMessage{ 85 | pin:{ 86 | type: number, 87 | time: number, 88 | key: WAMessageKey 89 | } 90 | } 91 | 92 | export interface PollMessage{ 93 | poll:{ 94 | name: string, 95 | values: string[], 96 | selectableCount: number, 97 | toAnnouncementGroup: boolean 98 | } 99 | } 100 | 101 | export interface ImageMessage extends ViewOnceMessage{ 102 | image:{ 103 | url: string 104 | }, 105 | caption?: string 106 | } 107 | 108 | export interface VideoMessage extends ViewOnceMessage{ 109 | video:{ 110 | url: string 111 | }, 112 | caption?: string, 113 | ptv?: boolean 114 | } 115 | 116 | export interface GifMessage extends VideoMessage{ 117 | gifPlayback: boolean 118 | } 119 | 120 | export interface AudioMessage extends ViewOnceMessage{ 121 | audio:{ 122 | url: string 123 | }, 124 | mimetype: string, 125 | ptv?: boolean 126 | } 127 | 128 | export interface DocumentMessage{ 129 | document:{ 130 | url: string 131 | }, 132 | mimetype: string, 133 | fileName: string 134 | } 135 | 136 | export interface StickerMessage{ 137 | sticker:{ 138 | url: string 139 | } 140 | } 141 | -------------------------------------------------------------------------------- /src/shared/guards.ts: -------------------------------------------------------------------------------- 1 | import { 2 | AudioMessage, 3 | ContactMessage, 4 | DocumentMessage, 5 | ForwardMessage, 6 | GifMessage, 7 | ImageMessage, 8 | LocationMessage, 9 | PinMessage, 10 | PollMessage, 11 | ReactionMessage, 12 | StickerMessage, 13 | TextMessage, 14 | VideoMessage } from "./types"; 15 | 16 | function isObject(v: any) { 17 | return typeof v === "object" && v !== null; 18 | } 19 | 20 | function isString(v: any) { 21 | return typeof v === "string"; 22 | } 23 | 24 | function isNumber(v: any) { 25 | return typeof v === "number" && !isNaN(v); 26 | } 27 | 28 | function isBoolean(v: any) { 29 | return typeof v === "boolean"; 30 | } 31 | 32 | export function isForwardMessage(obj: any): obj is ForwardMessage { 33 | return isObject(obj) && isString(obj.forward); 34 | } 35 | 36 | export function isTextMessage(obj: any): obj is TextMessage { 37 | return ( 38 | isObject(obj) && 39 | isString(obj.text) && 40 | ( 41 | obj.mentions === undefined || 42 | (Array.isArray(obj.mentions) && obj.mentions.every(isString)) 43 | ) 44 | ); 45 | } 46 | 47 | export function isLocationMessage(obj: any): obj is LocationMessage { 48 | return ( 49 | isObject(obj) && 50 | isObject(obj.location) && 51 | isNumber(obj.location.degreesLatitude) && 52 | isNumber(obj.location.degreesLongitude) 53 | ); 54 | } 55 | 56 | export function isContactMessage(obj: any): obj is ContactMessage { 57 | return ( 58 | isObject(obj) && 59 | isString(obj.displayName) && 60 | isNumber(obj.waid) && 61 | isString(obj.phoneNumber) 62 | ); 63 | } 64 | 65 | export function isReactionMessage(obj: any): obj is ReactionMessage { 66 | return ( 67 | isObject(obj) && 68 | isString(obj.emoji) && 69 | isString(obj.messageId) 70 | ); 71 | } 72 | 73 | export function isPinMessage(obj: any): obj is PinMessage { 74 | return ( 75 | isObject(obj) && 76 | isObject(obj.pin) && 77 | isNumber(obj.pin.type) && 78 | isNumber(obj.pin.time) && 79 | isObject(obj.pin.key) 80 | ); 81 | } 82 | 83 | export function isPollMessage(obj: any): obj is PollMessage { 84 | return ( 85 | isObject(obj) && 86 | isObject(obj.poll) && 87 | isString(obj.poll.name) && 88 | Array.isArray(obj.poll.values) && 89 | obj.poll.values.every(isString) && 90 | isNumber(obj.poll.selectableCount) && 91 | isBoolean(obj.poll.toAnnouncementGroup) 92 | ); 93 | } 94 | 95 | export function isImageMessage(obj: any): obj is ImageMessage { 96 | return ( 97 | isObject(obj) && 98 | isObject(obj.image) && 99 | isString(obj.image.url) && 100 | (obj.caption === undefined || isString(obj.caption)) && 101 | (obj.viewOnce === undefined || isBoolean(obj.viewOnce)) 102 | ); 103 | } 104 | 105 | export function isVideoMessage(obj: any): obj is VideoMessage { 106 | return ( 107 | isObject(obj) && 108 | isObject(obj.video) && 109 | isString(obj.video.url) && 110 | (obj.caption === undefined || isString(obj.caption)) && 111 | (obj.ptv === undefined || isBoolean(obj.ptv)) && 112 | (obj.viewOnce === undefined || isBoolean(obj.viewOnce)) 113 | ); 114 | } 115 | 116 | export function isGifMessage(obj: any): obj is GifMessage { 117 | return ( 118 | isObject(obj) && 119 | isObject(obj.video) && 120 | isString(obj.video.url) && 121 | (obj.caption === undefined || isString(obj.caption)) && 122 | (obj.ptv === undefined || isBoolean(obj.ptv)) && 123 | (obj.viewOnce === undefined || isBoolean(obj.viewOnce)) && 124 | isBoolean(obj.gifPlayback) 125 | ); 126 | } 127 | 128 | export function isAudioMessage(obj: any): obj is AudioMessage { 129 | return ( 130 | isObject(obj) && 131 | isObject(obj.audio) && 132 | isString(obj.audio.url) && 133 | isString(obj.mimetype) && 134 | (obj.viewOnce === undefined || isBoolean(obj.viewOnce)) 135 | ); 136 | } 137 | 138 | export function isDocumentMessage(obj: any): obj is DocumentMessage{ 139 | return ( 140 | isObject(obj) && 141 | isObject(obj.document) && 142 | isString(obj.document.url) && 143 | isString(obj.mimetype) && 144 | isString(obj.fileName) 145 | ); 146 | } 147 | 148 | export function isStickerMessage(obj: any): obj is StickerMessage{ 149 | return ( 150 | isObject(obj) && 151 | isObject(obj.sticker) && 152 | isString(obj.sticker.url) 153 | ); 154 | } 155 | -------------------------------------------------------------------------------- /src/infra/http/controllers/instances.ts: -------------------------------------------------------------------------------- 1 | import Instance from "../../baileys/services"; 2 | import InstancesRepository from "../../../core/repositories/instances"; 3 | import { instances, instanceStatus, sessionsPath } from "../../../shared/constants"; 4 | import { clearInstanceWebhooks, removeInstancePath } from "../../../shared/utils"; 5 | import path from "path"; 6 | import { InstanceCreated } from "../../../shared/types"; 7 | 8 | export default class InstancesController { 9 | 10 | async create(owner: string, instanceName: string, phoneNumber: string | undefined) { 11 | try { 12 | 13 | const key = `${owner}_${instanceName}`; 14 | 15 | if(typeof instances[key] === 'undefined'){ 16 | 17 | instances[key] = new Instance; 18 | 19 | const {instance, qrCode, pairingCode} = await instances[key].create({ owner, instanceName, phoneNumber }); 20 | 21 | const response: InstanceCreated = { 22 | success: true, 23 | message: "Instance Created Successfully!", 24 | instance: { 25 | owner: instance.owner, 26 | instanceName: instance.instanceName, 27 | connectionStatus: instance.connectionStatus, 28 | profilePictureUrl: instance.profilePictureUrl || undefined 29 | }, 30 | }; 31 | 32 | if(pairingCode){ 33 | response.pairingCode = pairingCode; 34 | }else if(qrCode){ 35 | response.qrCode = qrCode; 36 | } 37 | 38 | return response; 39 | 40 | }else{ 41 | 42 | return { 43 | success: false, 44 | error: "Instance with this owner exists.", 45 | }; 46 | 47 | } 48 | 49 | 50 | } catch (err: any) { 51 | console.error("Error in Instance Creator", err); 52 | return { 53 | success: false, 54 | error: "Internal Error in Instance Creator.", 55 | details: err.message, 56 | }; 57 | } 58 | } 59 | 60 | async connect(owner: string, instanceName: string){ 61 | 62 | try{ 63 | 64 | const key = `${owner}_${instanceName}`; 65 | 66 | const status = instanceStatus.get(key); 67 | 68 | if(status && status === "ONLINE"){ 69 | return { 70 | success: false, 71 | error: "Instance is already connected", 72 | }; 73 | } 74 | 75 | await clearInstanceWebhooks(key); 76 | const instancePath = path.join(sessionsPath, owner, instanceName); 77 | await removeInstancePath(instancePath); 78 | 79 | return this.create(owner, instanceName, undefined); 80 | 81 | }catch(err: any){ 82 | console.error("Error on connect instance:", err); 83 | return { 84 | success: false, 85 | error: err.message, 86 | }; 87 | } 88 | 89 | } 90 | 91 | async delete(owner: string, instanceName: string){ 92 | 93 | try{ 94 | 95 | const key = `${owner}_${instanceName}`; 96 | const instanceRemove = instances[key]; 97 | 98 | instanceRemove?.clearInstance(); 99 | 100 | console.log(`[${owner}/${instanceName}] REMOVED`); 101 | 102 | return { 103 | success: true, 104 | message: "Instance removed successfully" 105 | } 106 | 107 | }catch(err: any){ 108 | console.error("Error on connect instance:", err); 109 | return { 110 | success: false, 111 | error: err.message, 112 | }; 113 | } 114 | 115 | } 116 | 117 | async get(owner: string | undefined) { 118 | 119 | try{ 120 | 121 | const repo = await (new InstancesRepository).list(owner); 122 | 123 | return { 124 | success: true, 125 | data: repo 126 | }; 127 | 128 | }catch(err: any){ 129 | console.error("Error in List Instances:", err); 130 | return { 131 | success: false, 132 | error: err.message, 133 | }; 134 | } 135 | 136 | } 137 | 138 | } 139 | 140 | -------------------------------------------------------------------------------- /src/infra/http/controllers/privacy.ts: -------------------------------------------------------------------------------- 1 | import { instances } from "../../../shared/constants"; 2 | import { WAMessage, WAPrivacyGroupAddValue, WAPrivacyOnlineValue, WAPrivacyValue, WAReadReceiptsValue, WASocket } from "@whiskeysockets/baileys"; 3 | import { StatusPresence } from "../../../shared/types"; 4 | import PrismaConnection from "../../../core/connection/prisma"; 5 | 6 | export default class ProfileController { 7 | 8 | private sock: WASocket | undefined; 9 | private instance: string | undefined; 10 | 11 | constructor(owner: string, instanceName: string){ 12 | const key = `${owner}_${instanceName}`; 13 | this.instance = key; 14 | this.sock = instances[key]?.getSock(); 15 | } 16 | 17 | async unBlockUser(remoteJid: string, block: boolean){ 18 | 19 | try{ 20 | 21 | const action: "block" | "unblock" = (block ? 'block' : 'unblock'); 22 | 23 | await this.sock?.updateBlockStatus(remoteJid, action); 24 | 25 | return { 26 | success: true, 27 | message: "Block status of user changed with success" 28 | }; 29 | 30 | }catch(err){ 31 | 32 | return { 33 | success: false, 34 | error: "Error change block status of user" 35 | }; 36 | 37 | } 38 | 39 | } 40 | 41 | async getPrivacySettings(){ 42 | 43 | try{ 44 | 45 | const privacy = await this.sock?.fetchPrivacySettings(); 46 | 47 | return { 48 | success: true, 49 | message: "Get privacy settings with success", 50 | data: { 51 | privacy 52 | } 53 | }; 54 | 55 | }catch(err){ 56 | 57 | return { 58 | success: false, 59 | error: "Error getting privacy settings" 60 | }; 61 | 62 | } 63 | 64 | } 65 | 66 | async getBlockList(){ 67 | 68 | try{ 69 | 70 | const privacy = await this.sock?.fetchBlocklist(); 71 | 72 | return { 73 | success: true, 74 | message: "Get block list with success", 75 | data: { 76 | privacy 77 | } 78 | }; 79 | 80 | }catch(err){ 81 | 82 | return { 83 | success: false, 84 | error: "Error getting block list" 85 | }; 86 | 87 | } 88 | 89 | } 90 | 91 | async updateLastSeen(privacy: WAPrivacyValue){ 92 | 93 | try{ 94 | 95 | await this.sock?.updateLastSeenPrivacy(privacy); 96 | 97 | return { 98 | success: true, 99 | message: "Set privacy last seen with success" 100 | }; 101 | 102 | }catch(err){ 103 | 104 | return { 105 | success: false, 106 | error: "Error setting privacy last seen" 107 | }; 108 | 109 | } 110 | 111 | } 112 | 113 | async updateOnline(privacy: WAPrivacyOnlineValue){ 114 | 115 | try{ 116 | 117 | await this.sock?.updateOnlinePrivacy(privacy); 118 | 119 | return { 120 | success: true, 121 | message: "Set privacy last seen with success" 122 | }; 123 | 124 | }catch(err){ 125 | 126 | return { 127 | success: false, 128 | error: "Error setting privacy last seen" 129 | }; 130 | 131 | } 132 | 133 | } 134 | 135 | async profilePicture(privacy: WAPrivacyValue){ 136 | 137 | try{ 138 | 139 | await this.sock?.updateProfilePicturePrivacy(privacy); 140 | 141 | return { 142 | success: true, 143 | message: "Set privacy last seen with success" 144 | }; 145 | 146 | }catch(err){ 147 | 148 | return { 149 | success: false, 150 | error: "Error setting privacy last seen" 151 | }; 152 | 153 | } 154 | 155 | } 156 | 157 | async status(privacy: WAPrivacyValue){ 158 | 159 | try{ 160 | 161 | await this.sock?.updateStatusPrivacy(privacy); 162 | 163 | return { 164 | success: true, 165 | message: "Set privacy last seen with success" 166 | }; 167 | 168 | }catch(err){ 169 | 170 | return { 171 | success: false, 172 | error: "Error setting privacy last seen" 173 | }; 174 | 175 | } 176 | 177 | } 178 | 179 | async markRead(privacy: WAReadReceiptsValue){ 180 | 181 | try{ 182 | 183 | await this.sock?.updateReadReceiptsPrivacy(privacy); 184 | 185 | return { 186 | success: true, 187 | message: "Set privacy last seen with success" 188 | }; 189 | 190 | }catch(err){ 191 | 192 | return { 193 | success: false, 194 | error: "Error setting privacy last seen" 195 | }; 196 | 197 | } 198 | 199 | } 200 | 201 | async addGroups(privacy: WAPrivacyGroupAddValue){ 202 | 203 | try{ 204 | 205 | await this.sock?.updateGroupsAddPrivacy(privacy); 206 | 207 | return { 208 | success: true, 209 | message: "Set privacy last seen with success" 210 | }; 211 | 212 | }catch(err){ 213 | 214 | return { 215 | success: false, 216 | error: "Error setting privacy last seen" 217 | }; 218 | 219 | } 220 | 221 | } 222 | 223 | async ephemeral(time: number){ 224 | 225 | try{ 226 | 227 | await this.sock?.updateDefaultDisappearingMode(time); 228 | 229 | return { 230 | success: true, 231 | message: "Set privacy last seen with success" 232 | }; 233 | 234 | }catch(err){ 235 | 236 | return { 237 | success: false, 238 | error: "Error setting privacy last seen" 239 | }; 240 | 241 | } 242 | 243 | } 244 | 245 | 246 | } -------------------------------------------------------------------------------- /src/core/connection/prisma.ts: -------------------------------------------------------------------------------- 1 | import { Prisma, PrismaClient } from "@prisma/client"; 2 | import { JsonValue } from "@prisma/client/runtime/library"; 3 | import { Contact } from "../../shared/types"; 4 | import { WAMessage } from "@whiskeysockets/baileys"; 5 | import { MessageMapper } from "../../infra/mappers/messageMapper"; 6 | import { ContactMapper } from "../../infra/mappers/contactMapper"; 7 | 8 | export default class PrismaConnection { 9 | 10 | private static conn: PrismaClient = new PrismaClient();; 11 | 12 | static async saveMessages(instance: string, msg: any): Promise { 13 | 14 | const key = msg.key; 15 | 16 | if(!key || !key.id){ 17 | return; 18 | } 19 | 20 | const updateData: any = { 21 | content: msg, 22 | }; 23 | 24 | if (msg.pushName !== undefined && msg.pushName !== null) updateData.pushName = msg.pushName; 25 | if (msg?.status !== undefined && msg?.status !== null) updateData.status = msg.status.toString(); 26 | if (msg.messageTimestamp !== undefined && msg.messageTimestamp !== null) updateData.messageTimestamp = BigInt(msg.messageTimestamp); 27 | 28 | return PrismaConnection.conn.message.upsert({ 29 | where: { 30 | instance_messageId: { 31 | instance, 32 | messageId: key.id, 33 | }, 34 | }, 35 | update: updateData, 36 | create: { 37 | instance, 38 | messageId: key.id, 39 | remoteJid: key.remoteJid!, 40 | senderLid: key?.senderLid || null, 41 | fromMe: !!key.fromMe, 42 | pushName: msg.pushName || null, 43 | content: msg, 44 | status: msg?.status?.toString() || null, 45 | messageTimestamp: BigInt(msg.messageTimestamp || 0), 46 | }, 47 | }); 48 | 49 | } 50 | 51 | static async saveManyMessages(instance: string, msgs: any[]): Promise{ 52 | 53 | for(const msg of msgs){ 54 | await this.saveMessages(instance, msg); 55 | } 56 | 57 | } 58 | 59 | static async saveContact(instance: string, contact: Contact): Promise { 60 | const { id, lid, name } = contact; 61 | 62 | try{ 63 | 64 | const createData = { 65 | instance, 66 | name: name ?? null, 67 | jid: id ?? null, 68 | lid: lid ?? null, 69 | }; 70 | 71 | const updateData: any = { instance }; 72 | 73 | if (name !== undefined && name !== null) updateData.name = name; 74 | if (id !== undefined && id !== null) updateData.jid = id; 75 | if (lid !== undefined && lid !== null) updateData.lid = lid; 76 | 77 | if(id){ 78 | return await PrismaConnection.conn.contact.upsert({ 79 | where: { 80 | instance_jid: { instance, jid: id } 81 | }, 82 | update: updateData, 83 | create: createData 84 | }); 85 | }else if(lid){ 86 | return await PrismaConnection.conn.contact.upsert({ 87 | where: { 88 | instance_lid: { instance, lid } 89 | }, 90 | update: updateData, 91 | create: createData 92 | }); 93 | } 94 | 95 | }catch(err){ 96 | console.log(err); 97 | return false; 98 | } 99 | 100 | } 101 | 102 | static async saveManyContacts(instance: string, contacts: Contact[]): Promise{ 103 | for(const contact of contacts){ 104 | await this.saveContact(instance, contact); 105 | } 106 | } 107 | 108 | static async deleteByInstance(instance: string): Promise{ 109 | await PrismaConnection.conn.message.deleteMany({ 110 | where: { instance }, 111 | }); 112 | await PrismaConnection.conn.chat.deleteMany({ 113 | where: { instance }, 114 | }); 115 | return await PrismaConnection.conn.contact.deleteMany({ 116 | where: { instance }, 117 | }); 118 | } 119 | 120 | static async getMessageByInstance(instance: string): Promise { 121 | const allData = await PrismaConnection.conn.message.findMany({ 122 | where: { instance }, 123 | orderBy: { messageTimestamp: "desc" }, 124 | }); 125 | return await Promise.all(allData?.map(async (data) => data.content)); 126 | } 127 | 128 | static async getMessageById(messageId: string): Promise { 129 | const allData = await PrismaConnection.conn.message.findFirst({ 130 | where: { messageId } 131 | }); 132 | 133 | if(!allData){ 134 | return undefined; 135 | } 136 | 137 | return MessageMapper.toWAMessage(allData); 138 | 139 | } 140 | 141 | static async getLastMessageByInstance(instance: string, remoteJid: string): Promise { 142 | const allData = await PrismaConnection.conn.message.findFirst({ 143 | where: { 144 | AND: [ 145 | { instance }, 146 | { remoteJid } 147 | ] 148 | }, 149 | orderBy: { messageTimestamp: "desc" }, 150 | }); 151 | 152 | if(!allData){ 153 | return undefined; 154 | } 155 | 156 | return MessageMapper.toWAMessage(allData); 157 | 158 | } 159 | 160 | static async getContactById(instance: string, id: string): Promise { 161 | const allData = await PrismaConnection.conn.contact.findFirst({ 162 | where: { 163 | instance, 164 | OR: [ 165 | { jid: id }, 166 | { lid: id } 167 | ] 168 | } 169 | }); 170 | 171 | if(!allData){ 172 | return undefined; 173 | } 174 | 175 | return ContactMapper.toContact(allData); 176 | 177 | } 178 | 179 | 180 | } -------------------------------------------------------------------------------- /src/infra/http/controllers/chat.ts: -------------------------------------------------------------------------------- 1 | import { instances } from "../../../shared/constants"; 2 | import { MinimalMessage, WAMessage, WASocket } from "@whiskeysockets/baileys"; 3 | import { StatusPresence } from "../../../shared/types"; 4 | import PrismaConnection from "../../../core/connection/prisma"; 5 | 6 | export default class ChatController { 7 | 8 | private sock: WASocket | undefined; 9 | private instance: string | undefined; 10 | 11 | constructor(owner: string, instanceName: string){ 12 | const key = `${owner}_${instanceName}`; 13 | this.instance = key; 14 | this.sock = instances[key]?.getSock(); 15 | } 16 | 17 | async rejectCall(callId: string, callFrom: string){ 18 | 19 | if(!this.sock){ 20 | return { 21 | success: false, 22 | error: "Instance not connected.", 23 | }; 24 | } 25 | 26 | try{ 27 | 28 | await this.sock.rejectCall(callId, callFrom); 29 | 30 | return { 31 | success: true, 32 | message: "Call rejected successfully.", 33 | }; 34 | 35 | }catch(err){ 36 | return { 37 | success: false, 38 | error: "Failed to reject call.", 39 | }; 40 | } 41 | 42 | 43 | } 44 | 45 | async sendPresence(presence: StatusPresence, jid: string | undefined){ 46 | 47 | try{ 48 | 49 | this.sock?.sendPresenceUpdate(presence, jid); 50 | 51 | return { 52 | success: true, 53 | message: "Presence sent successfully.", 54 | }; 55 | 56 | }catch(err){ 57 | return { 58 | success: false, 59 | error: "Failed to send presence.", 60 | }; 61 | } 62 | 63 | } 64 | 65 | async arquiveChat(remoteJid: string, archive: boolean){ 66 | 67 | if(!this.sock){ 68 | return { 69 | success: false, 70 | error: "Instance not connected.", 71 | }; 72 | } 73 | 74 | try{ 75 | 76 | const lastMessage: WAMessage | undefined = await PrismaConnection.getLastMessageByInstance(this.instance!, remoteJid); 77 | 78 | if(!lastMessage){ 79 | return { 80 | success: false, 81 | error: "Failed to change chat archive status, message not found", 82 | }; 83 | } 84 | 85 | await this.sock.chatModify({ archive, lastMessages: [lastMessage] }, remoteJid); 86 | 87 | return { 88 | success: true, 89 | message: "Chat archive status changed successfully.", 90 | }; 91 | 92 | }catch(err){ 93 | return { 94 | success: false, 95 | error: "Failed to change chat archive status.", 96 | }; 97 | } 98 | } 99 | 100 | async muteChat(remoteJid: string, mute: number){ 101 | 102 | if(!this.sock){ 103 | return { 104 | success: false, 105 | error: "Instance not connected.", 106 | }; 107 | } 108 | try{ 109 | 110 | const muteDuration = (mute > 0 ? (mute == 1 ? 86400000 : 604800000) : null); 111 | await this.sock.chatModify({ mute: muteDuration}, remoteJid); 112 | 113 | return { 114 | success: true, 115 | message: "Chat mute status changed successfully.", 116 | }; 117 | 118 | }catch(err){ 119 | 120 | return { 121 | success: false, 122 | error: "Failed to change chat mute status.", 123 | }; 124 | 125 | } 126 | } 127 | 128 | async markChatAsRead(remoteJid: string, markRead: boolean){ 129 | 130 | if(!this.sock){ 131 | return { 132 | success: false, 133 | error: "Instance not connected.", 134 | }; 135 | } 136 | 137 | try{ 138 | 139 | const lastMessage: WAMessage | undefined = await PrismaConnection.getLastMessageByInstance(this.instance!, remoteJid); 140 | 141 | if(!lastMessage){ 142 | return { 143 | success: false, 144 | error: "Failed to mark chat as read, message not found", 145 | }; 146 | } 147 | 148 | await this.sock.chatModify({ markRead, lastMessages: [lastMessage] }, remoteJid); 149 | 150 | return { 151 | success: true, 152 | message: "Chat marked as read successfully.", 153 | }; 154 | 155 | }catch(err){ 156 | 157 | return { 158 | success: false, 159 | error: "Failed to mark chat as read.", 160 | }; 161 | 162 | } 163 | 164 | } 165 | 166 | async deleteChat(remoteJid: string){ 167 | 168 | try{ 169 | 170 | const lastMessage: WAMessage | undefined = await PrismaConnection.getLastMessageByInstance(this.instance!, remoteJid); 171 | 172 | if(!lastMessage){ 173 | return { 174 | success: false, 175 | error: "Failed to delete chat, message not found" 176 | }; 177 | } 178 | 179 | await this.sock?.chatModify({ 180 | delete: true, 181 | lastMessages: [ 182 | lastMessage 183 | ] 184 | }, 185 | remoteJid 186 | ); 187 | 188 | return { 189 | success: true, 190 | error: "Chat has deleted successfully" 191 | }; 192 | 193 | }catch(err){ 194 | 195 | return { 196 | success: false, 197 | error: "Failed to delete chat" 198 | }; 199 | 200 | } 201 | 202 | } 203 | 204 | async pinChat(remoteJid: string, pin: boolean){ 205 | 206 | try{ 207 | 208 | await this.sock?.chatModify({pin},remoteJid); 209 | 210 | return { 211 | success: true, 212 | error: "Pin changed successfully" 213 | }; 214 | 215 | }catch(err){ 216 | 217 | return { 218 | success: false, 219 | error: "Failed to change pin" 220 | }; 221 | 222 | } 223 | 224 | } 225 | 226 | } -------------------------------------------------------------------------------- /src/infra/http/controllers/profile.ts: -------------------------------------------------------------------------------- 1 | import { instances } from "../../../shared/constants"; 2 | import { WAMessage, WASocket } from "@whiskeysockets/baileys"; 3 | import { StatusPresence } from "../../../shared/types"; 4 | import PrismaConnection from "../../../core/connection/prisma"; 5 | 6 | export default class ProfileController { 7 | 8 | private sock: WASocket | undefined; 9 | private instance: string | undefined; 10 | 11 | constructor(owner: string, instanceName: string){ 12 | const key = `${owner}_${instanceName}`; 13 | this.instance = key; 14 | this.sock = instances[key]?.getSock(); 15 | } 16 | 17 | async onWhatsapp(remoteJid: string){ 18 | 19 | try{ 20 | 21 | const contact = await PrismaConnection.getContactById(this.instance!, remoteJid); 22 | 23 | if(contact){ 24 | 25 | return { 26 | success: true, 27 | message: "Contact exists on Whatsapp", 28 | data: contact 29 | }; 30 | 31 | }else{ 32 | 33 | const results = await this.sock?.onWhatsApp(remoteJid); 34 | 35 | if (results && results.length > 0 && results[0]?.exists) { 36 | 37 | const result = results[0]; 38 | 39 | return { 40 | success: true, 41 | message: "Contact exists on Whatsapp", 42 | data: { 43 | id: result?.jid 44 | } 45 | }; 46 | 47 | }else{ 48 | return { 49 | success: false, 50 | error: "Contact dont exists on Whatsapp" 51 | }; 52 | } 53 | 54 | } 55 | 56 | }catch(err){ 57 | 58 | return { 59 | success: false, 60 | error: "Error checking number exists on Whatsapp" 61 | }; 62 | 63 | } 64 | 65 | } 66 | 67 | async fetchStatus(remoteJid: string){ 68 | 69 | try{ 70 | 71 | const status = await this.sock?.fetchStatus(remoteJid); 72 | 73 | return { 74 | success: true, 75 | message: "Status fetched", 76 | data: { 77 | status 78 | } 79 | }; 80 | 81 | }catch(err){ 82 | 83 | return { 84 | success: false, 85 | error: "Error fetching status" 86 | }; 87 | 88 | } 89 | 90 | } 91 | 92 | async fetchProfilePicture(remoteJid: string){ 93 | 94 | try{ 95 | 96 | const status = await this.sock?.profilePictureUrl(remoteJid, 'image'); 97 | 98 | return { 99 | success: true, 100 | message: "Successfully", 101 | data: { 102 | status 103 | } 104 | }; 105 | 106 | }catch(err){ 107 | 108 | return { 109 | success: false, 110 | error: "Error fetching profile picture" 111 | }; 112 | 113 | } 114 | 115 | } 116 | 117 | async fetchBusinessProfile(remoteJid: string){ 118 | 119 | try{ 120 | 121 | const profile = await this.sock?.getBusinessProfile(remoteJid); 122 | 123 | return { 124 | success: true, 125 | message: "Successfully", 126 | data: { 127 | profile 128 | } 129 | }; 130 | 131 | }catch(err){ 132 | 133 | return { 134 | success: false, 135 | error: "Error fetching profile Business" 136 | }; 137 | 138 | } 139 | 140 | } 141 | 142 | async presenceSubscribe(remoteJid: string){ 143 | 144 | try{ 145 | 146 | await this.sock?.presenceSubscribe(remoteJid); 147 | 148 | return { 149 | success: true, 150 | message: "Successfully" 151 | }; 152 | 153 | }catch(err){ 154 | 155 | return { 156 | success: false, 157 | error: "Error subscribe presence" 158 | }; 159 | 160 | } 161 | 162 | } 163 | 164 | async profileName(name: string){ 165 | 166 | try{ 167 | 168 | await this.sock?.updateProfileName(name); 169 | 170 | return { 171 | success: true, 172 | message: `Successfully, your new name is '${name}'` 173 | }; 174 | 175 | }catch(err){ 176 | 177 | return { 178 | success: false, 179 | error: "Error changing name" 180 | }; 181 | 182 | } 183 | 184 | } 185 | 186 | async profileStatus(status: string){ 187 | 188 | try{ 189 | 190 | await this.sock?.updateProfileStatus(status); 191 | 192 | return { 193 | success: true, 194 | message: `Successfully, your new status is '${status}'` 195 | }; 196 | 197 | }catch(err){ 198 | 199 | return { 200 | success: false, 201 | error: "Error changing status" 202 | }; 203 | 204 | } 205 | 206 | } 207 | 208 | async updateProfilePicture(remoteJid: string, url: string){ 209 | 210 | try{ 211 | 212 | await this.sock?.updateProfilePicture(remoteJid, {url}); 213 | 214 | return { 215 | success: true, 216 | message: `Successfully` 217 | }; 218 | 219 | }catch(err){ 220 | 221 | return { 222 | success: false, 223 | error: "Error changing profile picture" 224 | }; 225 | 226 | } 227 | 228 | } 229 | 230 | async removeProfilePicture(remoteJid: string){ 231 | 232 | try{ 233 | 234 | await this.sock?.removeProfilePicture(remoteJid); 235 | 236 | return { 237 | success: true, 238 | message: `Successfully` 239 | }; 240 | 241 | }catch(err){ 242 | 243 | return { 244 | success: false, 245 | error: "Error removing profile picture" 246 | }; 247 | 248 | } 249 | 250 | } 251 | 252 | } -------------------------------------------------------------------------------- /src/shared/utils.ts: -------------------------------------------------------------------------------- 1 | import * as fs from "fs"; 2 | import { HttpsProxyAgent } from 'https-proxy-agent'; 3 | import { SocksProxyAgent } from 'socks-proxy-agent'; 4 | import { ProxyAgent as UndiciProxyAgent } from 'undici'; 5 | import { ConnectionStatus, InstanceData, ProxyAgent, WebhookPayload } from './types'; 6 | import path from "path"; 7 | import UserConfig from "../infra/config/env" 8 | import { Worker } from 'worker_threads'; 9 | import { jidNormalizedUser } from "@whiskeysockets/baileys"; 10 | 11 | 12 | export async function removeInstancePath(instancePath: string){ 13 | 14 | fs.rmSync(instancePath, { recursive: true, force: true }); 15 | 16 | } 17 | 18 | export async function genProxy(wppProxy?: string): Promise{ 19 | 20 | const proxys: ProxyAgent = {}; 21 | 22 | if(!wppProxy){ 23 | return proxys; 24 | } 25 | 26 | const isProtocol = (url: string) => url.split(":")[0]?.toLowerCase(); 27 | 28 | const protocol = isProtocol(wppProxy); 29 | 30 | switch(protocol){ 31 | case 'http': 32 | case 'https':{ 33 | proxys.wsAgent = new HttpsProxyAgent(wppProxy); 34 | break; 35 | } 36 | case 'socks': 37 | case 'socks4': 38 | case 'socks5':{ 39 | proxys.wsAgent = new SocksProxyAgent(wppProxy); 40 | break; 41 | } 42 | default:{ 43 | console.warn(`Unknown Protocol in Proxy: ${wppProxy}`); 44 | } 45 | } 46 | 47 | proxys.fetchAgent = new UndiciProxyAgent(wppProxy); 48 | 49 | return proxys; 50 | } 51 | 52 | function serializeData(data: any): any { 53 | try { 54 | return JSON.parse(JSON.stringify(data)); 55 | } catch (err) { 56 | console.warn('Failed to serialize data, returning empty object:', err); 57 | return {}; 58 | } 59 | } 60 | 61 | export async function trySendWebhook(event: string, instance: InstanceData, data: any) { 62 | 63 | const payload: WebhookPayload = serializeData({ 64 | event, 65 | instance: { 66 | instanceName: instance.instanceName, 67 | owner: instance.owner, 68 | connectionStatus: instance.connectionStatus, 69 | profilePictureUrl: instance.profilePictureUrl, 70 | instanceJid: jidNormalizedUser(instance.socket?.user?.id) || null 71 | }, 72 | data, 73 | targetUrl: UserConfig.webhookUrl 74 | }); 75 | 76 | const worker = new Worker(path.join(__dirname, 'webhookWorker.js')); 77 | 78 | worker.postMessage(payload); 79 | 80 | worker.on('message', async (result) => { 81 | if (!result.success) { 82 | if(UserConfig.useWebhookQueue){ 83 | console.warn(`[${instance.owner}/${instance.instanceName}] Fail to send webhook ${event}, saving locally...`); 84 | await saveWebhookEvent(payload); 85 | } 86 | } 87 | worker.terminate(); 88 | }); 89 | 90 | worker.on('error', async (err) => { 91 | if(UserConfig.useWebhookQueue){ 92 | console.warn(`[${instance.owner}/${instance.instanceName}] Fail in webhook worker ${event}:`, err); 93 | await saveWebhookEvent(payload); 94 | } 95 | worker.terminate(); 96 | }); 97 | } 98 | 99 | async function ensureDir() { 100 | try { 101 | await fs.promises.mkdir(UserConfig.webhook_queue_dir, { recursive: true }); 102 | } catch (err) { 103 | console.error("Error creating webhook directory:", err); 104 | } 105 | } 106 | 107 | export async function saveWebhookEvent(payload: WebhookPayload) { 108 | try { 109 | await ensureDir(); 110 | const filename = `${payload.instance.instanceName}-${Date.now()}.json`; 111 | const filePath = path.join(UserConfig.webhook_queue_dir, filename); 112 | await fs.promises.writeFile(filePath, JSON.stringify(payload, null, 2), "utf8"); 113 | } catch (err) { 114 | console.error("Error saving webhook:", err); 115 | } 116 | } 117 | 118 | export async function processWebhookQueue(getInstanceStatus: (name: string) => ConnectionStatus) { 119 | try { 120 | await ensureDir(); 121 | const files = await fs.promises.readdir(UserConfig.webhook_queue_dir); 122 | 123 | for (const file of files) { 124 | const filePath = path.join(UserConfig.webhook_queue_dir, file); 125 | const raw = await fs.promises.readFile(filePath, "utf8"); 126 | const payload: WebhookPayload = JSON.parse(raw); 127 | 128 | const status = getInstanceStatus(payload.instance.instanceName); 129 | 130 | if(status !== "ONLINE"){ 131 | if(status === "REMOVED"){ 132 | await fs.promises.unlink(filePath); 133 | } 134 | continue; 135 | } 136 | 137 | // Usa worker para reenviar webhook 138 | const worker = new Worker(path.join(__dirname, 'webhookWorker.js')); 139 | 140 | worker.postMessage(payload); 141 | 142 | worker.on('message', async (result) => { 143 | if (result.success) { 144 | await fs.promises.unlink(filePath); 145 | } else { 146 | console.warn(`Fail to resend webhook ${file}: ${result.error}`); 147 | } 148 | worker.terminate(); 149 | }); 150 | 151 | worker.on('error', async (err) => { 152 | console.warn(`Error trying to resend webhook ${file}:`, err.message); 153 | worker.terminate(); 154 | }); 155 | } 156 | } catch (err) { 157 | console.error("Error processing webhook queue:", err); 158 | } 159 | } 160 | 161 | export async function clearInstanceWebhooks(instanceName: string) { 162 | try { 163 | 164 | await ensureDir(); 165 | const files = await fs.promises.readdir(UserConfig.webhook_queue_dir); 166 | const related = files.filter(f => f.startsWith(`${instanceName}-`)); 167 | 168 | for (const file of related) { 169 | await fs.promises.unlink(path.join(UserConfig.webhook_queue_dir, file)); 170 | } 171 | } catch (err) { 172 | console.error(`Error removing webhooks for instance ${instanceName}:`, err); 173 | } 174 | } 175 | 176 | export function startWebhookRetryLoop(getInstanceStatus: (name: string) => "ONLINE" | "OFFLINE" | "REMOVED") { 177 | if(UserConfig.useWebhookQueue){ 178 | setInterval(() => { 179 | processWebhookQueue(getInstanceStatus); 180 | }, UserConfig.webhook_interval); 181 | } 182 | } 183 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 |
3 | 4 | # ZapToBox Whatsapp Api 5 | 6 | [![jeankassio - ZapToBox-Whatsapp-Api](https://img.shields.io/static/v1?label=jeankassio&message=ZapToBox-Whatsapp-Api&color=darkgreen&logo=github)](https://github.com/jeankassio/ZapToBox-Whatsapp-Api "Go to GitHub repo") 7 | [![stars - ZapToBox-Whatsapp-Api](https://img.shields.io/github/stars/jeankassio/ZapToBox-Whatsapp-Api?style=social)](https://github.com/jeankassio/ZapToBox-Whatsapp-Api) 8 | [![forks - ZapToBox-Whatsapp-Api](https://img.shields.io/github/forks/jeankassio/ZapToBox-Whatsapp-Api?style=social)](https://github.com/jeankassio/ZapToBox-Whatsapp-Api) 9 | 10 | 11 | [![Support](https://img.shields.io/badge/-Grupo%20Whatsapp-darkgreen?style=for-the-badge&logo=whatsapp)](https://chat.whatsapp.com/Deus9QmrfZlJaZIf46F129) 12 | 13 | 14 | [![Support](https://img.shields.io/badge/Buy%20me%20coffe-PayPal-blue?style=for-the-badge)](https://paypal.me/JAlmeidaCheib) 15 | [![Support](https://img.shields.io/badge/Buy%20me%20coffe-Pix-darkturquoise?style=for-the-badge)](#pix) 16 |
17 | 18 |

19 | 20 | 21 | 22 | 23 | 24 | background github 25 | 26 |

27 | 28 | REST API platform for integrating systems with WhatsApp with stability, multi-instance management, message sending, and full webhook event streaming. 29 | Designed for enterprise automation, bots, SaaS platforms, CRMs, ERPs, and large-scale integrations. 30 | 31 | # Overview 32 | 33 | ZapToBox WhatsApp API is an advanced REST platform built on top of Baileys, enabling fast and stable integration between applications and WhatsApp. 34 | 35 | #### The project supports: 36 | 37 | - Multi-instance session management 38 | 39 | - File-system based authentication (/sessions/{{owner}}/{{instanceName}}) 40 | 41 | - Full event webhook streaming 42 | 43 | - High-performance message persistence 44 | 45 | - All Baileys methods exposed through endpoints 46 | 47 | - Scalable architecture using DDD (Domain Driven Design) 48 | 49 | - Full TypeScript backend with Prisma ORM 50 | 51 | - Automatic reconnection and failure handling 52 | 53 | - Connection with QrCode and Pairing Code 54 | 55 | 56 | # Features 57 | 58 | ### Multi-instance Session Architecture 59 | 60 | Each instance is fully isolated using the structure: 61 | 62 | ```bash 63 | /sessions/{owner}/{instanceName} 64 | ``` 65 | 66 | This enables: 67 | 68 | - Multiple authenticated devices per system 69 | 70 | - Multi-user/multi-tenant architecture 71 | 72 | - Stateless REST integration across environments 73 | 74 | ### All Baileys Functionalities Exposed via API 75 | 76 | #### The platform exposes every usable operation from Baileys, including: 77 | 78 | - Messages 79 | 80 | - Media 81 | 82 | - Chat 83 | 84 | - Group 85 | 86 | - Profile 87 | 88 | - Privacy 89 | 90 | 91 | ### Webhook Event Streaming 92 | 93 | #### All supported Baileys events are forwarded to your system in real-time, they are: 94 | 95 | - messaging-history.set 96 | - chats.upsert 97 | - chats.update 98 | - chats.delete 99 | - lid-mapping.update 100 | - presence.update 101 | - contacts.upsert 102 | - contacts.update 103 | - messages.upsert 104 | - messages.update 105 | - messages.delete 106 | - messages.media-update 107 | - messages.reaction 108 | - message-receipt.update 109 | - groups.upsert 110 | - groups.update 111 | - group-participants.update 112 | - group.join-request 113 | - blocklist.set 114 | - blocklist.update 115 | - call 116 | - labels.edit 117 | - labels.association 118 | - newsletter.reaction 119 | - newsletter.view 120 | - newsletter-participants.update 121 | - newsletter-settings.update 122 | 123 | #### And as additional webhook events: 124 | 125 | - contacts.set 126 | - chats.set 127 | - messages.set 128 | - qrcode.updated 129 | - qrcode.limit 130 | - pairingcode.updated 131 | - pairingcode.limit 132 | - connection.connecting 133 | - connection.open 134 | - connection.close 135 | - connection.removed 136 | 137 | 138 | ## Status de Mensagem do WhatsApp (Baileys) 139 | 140 | The message status returned by Baileys is an integer (`status`). For quick reference when integrating it into your project, see below how to map it: 141 | 142 | | Número | String 143 | |--------|--------------- 144 | | 0 | ERROR | 145 | | 1 | PENDING | 146 | | 2 | SENT | 147 | | 3 | DELIVERED | 148 | | 4 | READ | 149 | | 5 | PLAYED | 150 | 151 | 152 | # Project Structure 153 | 154 | - `/prisma` 155 | - `/migrations` 156 | - `...` 157 | - `schema.prisma` 158 | - `/src` 159 | - `/core` 160 | - `/connection` 161 | - `prisma.ts` 162 | - `/repositories` 163 | - `instances.ts` 164 | - `/infra` 165 | - `/baileys` 166 | - `services.ts` 167 | - `/config` 168 | - `env.ts` 169 | - `/http` 170 | - `/controllers` 171 | - `chat.ts` 172 | - `group.ts` 173 | - `instances.ts` 174 | - `media.ts` 175 | - `messages.ts` 176 | - `privacy.ts` 177 | - `profile.ts` 178 | - `/routes` 179 | - `chat.ts` 180 | - `group.ts` 181 | - `instances.ts` 182 | - `media.ts` 183 | - `messages.ts` 184 | - `privacy.ts` 185 | - `profile.ts` 186 | - `/mappers` 187 | - `contactMapper.ts` 188 | - `messageMapper.ts` 189 | - `/state` 190 | - `auth.ts` 191 | - `sessions.ts` 192 | - `/webhook` 193 | - `queue.ts` 194 | - `/shared` 195 | - `constants.ts` 196 | - `types.ts` 197 | - `utils.ts` 198 | - `main.ts` 199 | - `/docs` 200 | - `.env.example` 201 | - `.gitignore` 202 | - `docker-compose.yml` 203 | - `Dockerfile` 204 | - `LICENSE` 205 | - `package.json` 206 | - `prisma.config.ts` 207 | - `README.md` 208 | - `tsconfig.json` 209 | 210 | 211 | # Installation 212 | 213 | ### Clone the repository 214 | 215 | ```bash 216 | git clone https://github.com/jeankassio/ZapToBox-Whatsapp-Api.git 217 | cd ZapToBox-Whatsapp-Api 218 | ``` 219 | 220 | ### Install dependencies 221 | 222 | ```bash 223 | npm install 224 | ``` 225 | 226 | ### Environment configuration 227 | 228 | #### Rename .env.example to .env 229 | 230 | ```bash 231 | cp .env.example .env 232 | ``` 233 | ##### Please fill in all the information correctly within the .env file before proceeding, especially the PostgreSQL connection URL. 234 | 235 | ### Deploy Prisma 236 | 237 | ```bash 238 | npx prisma migrate deploy 239 | ``` 240 | ##### Confirm and proceed 241 | 242 | 243 | # Running 244 | 245 | ## Development 246 | 247 | ```bash 248 | npm run dev 249 | ``` 250 | 251 | ## Production (auto build) 252 | 253 | ```bash 254 | npm run start 255 | ``` 256 | 257 | ## Production (PM2) 258 | 259 | ### build 260 | 261 | ```bash 262 | npm run build 263 | ``` 264 | ### run 265 | 266 | ```bash 267 | npm run start:pm2 268 | 269 | //optionals: 270 | pm2 save 271 | pm2 startup 272 | ``` 273 | 274 | ## Docker Deploy 275 | 276 | ### Build the image 277 | 278 | ```bash 279 | docker build -t zaptobox-whatsapp-api 280 | ``` 281 | ### Run 282 | 283 | ```bash 284 | docker run -d --name zaptobox -p 3000:3000 --env-file .env zaptobox-whatsapp-api 285 | ``` 286 | 287 | # API Docs 288 | 289 | ##### You can find complete documentation for the endpoints at: 290 | 291 | - [Postman](https://www.postman.com/jeankassio12/zaptobox-api) 292 | 293 | # Donate 294 | 295 |
296 | 297 | ### Pix 298 | 299 | image 300 | 301 | #### 6dcc2052-0b7c-4947-831d-46d67235416e 302 | 303 |
304 | -------------------------------------------------------------------------------- /src/infra/http/routes/chat.ts: -------------------------------------------------------------------------------- 1 | import express, { Request, Response } from "express"; 2 | import ChatController from "../controllers/chat"; 3 | 4 | export default class ChatRoutes{ 5 | 6 | private router = express.Router(); 7 | 8 | get(){ 9 | 10 | this.router 11 | .patch("/rejectCall/:owner/:instanceName", async (req: Request, res: Response) => { 12 | 13 | const owner = req.params.owner; 14 | const instanceName = req.params.instanceName; 15 | 16 | if(!owner || !instanceName){ 17 | return res.status(400).json({ 18 | error: "Owner and instanceName are required" 19 | }); 20 | } 21 | 22 | const { callId, callFrom} = req.body; 23 | 24 | if(!callId || !callFrom){ 25 | return res.status(400).json({ 26 | error: "callId and callFrom are required" 27 | }); 28 | } 29 | 30 | const chatController = new ChatController(owner, instanceName); 31 | const result = await chatController.rejectCall(callId, callFrom); 32 | 33 | if(result?.error){ 34 | return res.status(500).json(result); 35 | }else{ 36 | return res.status(200).json(result); 37 | } 38 | 39 | }) 40 | .post("/sendPresence/:owner/:instanceName", async (req: Request, res: Response) => { 41 | 42 | const owner = req.params.owner; 43 | const instanceName = req.params.instanceName; 44 | 45 | if(!owner || !instanceName){ 46 | return res.status(400).json({ 47 | error: "Owner and instanceName are required" 48 | }); 49 | } 50 | 51 | const { presence, remoteJid} = req.body; 52 | 53 | if(!presence){ 54 | return res.status(400).json({ 55 | error: "presence is required" 56 | }); 57 | } 58 | 59 | const chatController = new ChatController(owner, instanceName); 60 | const result = await chatController.sendPresence(presence, remoteJid); 61 | 62 | if(result?.error){ 63 | return res.status(500).json(result); 64 | }else{ 65 | return res.status(200).json(result); 66 | } 67 | }) 68 | .patch("/archiveChat/:owner/:instanceName", async (req: Request, res: Response) => { 69 | 70 | const owner = req.params.owner; 71 | const instanceName = req.params.instanceName; 72 | 73 | if(!owner || !instanceName){ 74 | return res.status(400).json({ 75 | error: "Owner and instanceName are required" 76 | }); 77 | } 78 | 79 | const { remoteJid, archive } = req.body; 80 | 81 | if(!remoteJid || typeof archive === 'undefined'){ 82 | return res.status(400).json({ 83 | error: "jid and archive are required" 84 | }); 85 | } 86 | 87 | const chatController = new ChatController(owner, instanceName); 88 | const result = await chatController.arquiveChat(remoteJid, archive); 89 | 90 | if(result?.error){ 91 | return res.status(500).json(result); 92 | }else{ 93 | return res.status(200).json(result); 94 | } 95 | 96 | }) 97 | .patch("/mute/:owner/:instanceName", async (req: Request, res: Response) => { 98 | 99 | const owner = req.params.owner; 100 | const instanceName = req.params.instanceName 101 | 102 | if(!owner || !instanceName){ 103 | return res.status(400).json({ 104 | error: "Owner and instanceName are required" 105 | }); 106 | } 107 | 108 | const { remoteJid, mute } = req.body; 109 | 110 | if(!remoteJid || typeof mute === 'undefined'){ 111 | return res.status(400).json({ 112 | error: "Parameters jid and mute are required" 113 | }); 114 | } 115 | 116 | const chatController = new ChatController(owner, instanceName); 117 | const result = await chatController.muteChat(remoteJid, mute); 118 | 119 | if(result?.error){ 120 | return res.status(500).json(result); 121 | }else{ 122 | return res.status(200).json(result); 123 | } 124 | 125 | }) 126 | .patch("/markChatAsRead/:owner/:instanceName", async (req: Request, res: Response) => { 127 | 128 | const owner = req.params.owner; 129 | const instanceName = req.params.instanceName; 130 | 131 | if(!owner || !instanceName){ 132 | return res.status(400).json({ 133 | error: "Owner and instanceName are required" 134 | }); 135 | } 136 | 137 | const { remoteJid, markAsRead } = req.body; 138 | 139 | if(!remoteJid || typeof markAsRead === 'undefined'){ 140 | return res.status(400).json({error: "remoteJid is required"}); 141 | } 142 | 143 | const chatController = new ChatController(owner, instanceName); 144 | const result = await chatController.markChatAsRead(remoteJid, markAsRead); 145 | 146 | if(result?.error){ 147 | return res.status(500).json(result); 148 | }else{ 149 | return res.status(200).json(result); 150 | } 151 | 152 | }) 153 | .delete("/deleteChat/:owner/:instanceName", async(req: Request, res: Response) => { 154 | 155 | const owner = req.params.owner; 156 | const instanceName = req.params.instanceName; 157 | 158 | if(!owner || !instanceName){ 159 | return res.status(400).json({ 160 | error: "Owner and instanceName are required" 161 | }); 162 | } 163 | 164 | const {remoteJid} = req.body; 165 | 166 | if(!remoteJid){ 167 | return res.status(400).json({ 168 | error: "messageId is required" 169 | }); 170 | } 171 | 172 | const chatController = new ChatController(owner, instanceName); 173 | const result = await chatController.deleteChat(remoteJid); 174 | 175 | if(result?.error){ 176 | return res.status(500).json(result); 177 | }else{ 178 | return res.status(200).json(result); 179 | } 180 | 181 | }) 182 | .patch("/unpin/:owner/:instanceName", async(req: Request, res: Response) => { 183 | 184 | const owner = req.params.owner; 185 | const instanceName = req.params.instanceName; 186 | 187 | if(!owner || !instanceName){ 188 | return res.status(400).json({ 189 | error: "Owner and instanceName are required" 190 | }); 191 | } 192 | 193 | const {remoteJid, pin} = req.body; 194 | 195 | if(!remoteJid || typeof pin === 'undefined'){ 196 | return res.status(400).json({ 197 | error: "messageId and pin is required" 198 | }); 199 | } 200 | 201 | const chatController = new ChatController(owner, instanceName); 202 | const result = await chatController.pinChat(remoteJid, pin); 203 | 204 | if(result?.error){ 205 | return res.status(500).json(result); 206 | }else{ 207 | return res.status(200).json(result); 208 | } 209 | 210 | }) 211 | return this.router; 212 | 213 | } 214 | 215 | } -------------------------------------------------------------------------------- /src/infra/http/routes/profile.ts: -------------------------------------------------------------------------------- 1 | import express, { Request, Response } from "express"; 2 | import ProfileController from "../controllers/profile"; 3 | 4 | export default class ProfileRoutes{ 5 | 6 | private router = express.Router(); 7 | 8 | get(){ 9 | 10 | this.router 11 | .post("/onWhatsapp/:owner/:instanceName", async (req: Request, res: Response) => { 12 | 13 | const owner = req.params.owner; 14 | const instanceName = req.params.instanceName; 15 | 16 | if(!owner || !instanceName){ 17 | return res.status(400).json({ error: "Owner and instanceName are required." }); 18 | } 19 | 20 | const { id } = req.body; 21 | 22 | if(!id){ 23 | return res.status(400).json({ error: "Field 'id' is required." }); 24 | } 25 | 26 | const profileController = new ProfileController(owner, instanceName); 27 | const result = await profileController.onWhatsapp(id); 28 | 29 | if(result?.error){ 30 | return res.status(500).json(result); 31 | }else{ 32 | return res.status(200).json(result); 33 | } 34 | 35 | }) 36 | .post("/fetchStatus/:owner/:instanceName", async(req: Request, res: Response) => { 37 | 38 | const owner = req.params.owner; 39 | const instanceName = req.params.instanceName; 40 | 41 | if(!owner || !instanceName){ 42 | return res.status(400).json({ error: "Owner and instanceName are required." }); 43 | } 44 | 45 | const { remoteJid } = req.body; 46 | 47 | if(!remoteJid){ 48 | return res.status(400).json({ error: "Field 'remoteJid' is required." }); 49 | } 50 | 51 | const profileController = new ProfileController(owner, instanceName); 52 | const result = await profileController.fetchStatus(remoteJid); 53 | 54 | if(result?.error){ 55 | return res.status(500).json(result); 56 | }else{ 57 | return res.status(200).json(result); 58 | } 59 | 60 | }) 61 | .post("/fetchProfilePicture/:owner/:instanceName", async(req: Request, res: Response) => { 62 | 63 | const owner = req.params.owner; 64 | const instanceName = req.params.instanceName; 65 | 66 | if(!owner || !instanceName){ 67 | return res.status(400).json({ error: "Owner and instanceName are required." }); 68 | } 69 | 70 | const { remoteJid } = req.body; 71 | 72 | if(!remoteJid){ 73 | return res.status(400).json({ error: "Field 'remoteJid' is required." }); 74 | } 75 | 76 | const profileController = new ProfileController(owner, instanceName); 77 | const result = await profileController.fetchProfilePicture(remoteJid); 78 | 79 | if(result?.error){ 80 | return res.status(500).json(result); 81 | }else{ 82 | return res.status(200).json(result); 83 | } 84 | 85 | }) 86 | .post("/fetchBusinessProfile/:owner/:instanceName", async(req: Request, res: Response) => { 87 | 88 | const owner = req.params.owner; 89 | const instanceName = req.params.instanceName; 90 | 91 | if(!owner || !instanceName){ 92 | return res.status(400).json({ error: "Owner and instanceName are required." }); 93 | } 94 | 95 | const { remoteJid } = req.body; 96 | 97 | if(!remoteJid){ 98 | return res.status(400).json({ error: "Field 'remoteJid' is required." }); 99 | } 100 | 101 | const profileController = new ProfileController(owner, instanceName); 102 | const result = await profileController.fetchBusinessProfile(remoteJid); 103 | 104 | if(result?.error){ 105 | return res.status(500).json(result); 106 | }else{ 107 | return res.status(200).json(result); 108 | } 109 | 110 | }) 111 | .post("/presenceSubscribe/:owner/:instanceName", async(req: Request, res: Response) => { 112 | 113 | const owner = req.params.owner; 114 | const instanceName = req.params.instanceName; 115 | 116 | if(!owner || !instanceName){ 117 | return res.status(400).json({ error: "Owner and instanceName are required." }); 118 | } 119 | 120 | const { remoteJid } = req.body; 121 | 122 | if(!remoteJid){ 123 | return res.status(400).json({ error: "Field 'remoteJid' is required." }); 124 | } 125 | 126 | const profileController = new ProfileController(owner, instanceName); 127 | const result = await profileController.presenceSubscribe(remoteJid); 128 | 129 | if(result?.error){ 130 | return res.status(500).json(result); 131 | }else{ 132 | return res.status(200).json(result); 133 | } 134 | 135 | }) 136 | .patch("/profileName/:owner/:instanceName", async(req: Request, res: Response) => { 137 | 138 | const owner = req.params.owner; 139 | const instanceName = req.params.instanceName; 140 | 141 | if(!owner || !instanceName){ 142 | return res.status(400).json({ error: "Owner and instanceName are required." }); 143 | } 144 | 145 | const { name } = req.body; 146 | 147 | if(!name){ 148 | return res.status(400).json({ error: "Field 'name' is required." }); 149 | } 150 | 151 | const profileController = new ProfileController(owner, instanceName); 152 | const result = await profileController.profileName(name); 153 | 154 | if(result?.error){ 155 | return res.status(500).json(result); 156 | }else{ 157 | return res.status(200).json(result); 158 | } 159 | 160 | }) 161 | .patch("/profileStatus/:owner/:instanceName", async(req: Request, res: Response) => { 162 | 163 | const owner = req.params.owner; 164 | const instanceName = req.params.instanceName; 165 | 166 | if(!owner || !instanceName){ 167 | return res.status(400).json({ error: "Owner and instanceName are required." }); 168 | } 169 | 170 | const { status } = req.body; 171 | 172 | if(!status){ 173 | return res.status(400).json({ error: "Field 'status' is required." }); 174 | } 175 | 176 | const profileController = new ProfileController(owner, instanceName); 177 | const result = await profileController.profileStatus(status); 178 | 179 | if(result?.error){ 180 | return res.status(500).json(result); 181 | }else{ 182 | return res.status(200).json(result); 183 | } 184 | 185 | }) 186 | .put("/profilePicture/:owner/:instanceName", async(req: Request, res: Response) => { 187 | 188 | const owner = req.params.owner; 189 | const instanceName = req.params.instanceName; 190 | 191 | if(!owner || !instanceName){ 192 | return res.status(400).json({ error: "Owner and instanceName are required." }); 193 | } 194 | 195 | const { jid, url } = req.body; 196 | 197 | if(!jid){ 198 | return res.status(400).json({ error: "Fields 'jid', 'url' and 'active' is required." }); 199 | } 200 | 201 | const profileController = new ProfileController(owner, instanceName); 202 | const result = await (url ? profileController.updateProfilePicture(jid, url) : profileController.removeProfilePicture(jid)); 203 | 204 | if(result?.error){ 205 | return res.status(500).json(result); 206 | }else{ 207 | return res.status(200).json(result); 208 | } 209 | 210 | }) 211 | return this.router; 212 | 213 | } 214 | 215 | } -------------------------------------------------------------------------------- /src/infra/http/controllers/messages.ts: -------------------------------------------------------------------------------- 1 | import { instances } from "../../../shared/constants"; 2 | import { AudioMessage, ContactMessage, DocumentMessage, GifMessage, ImageMessage, LocationMessage, PollMessage, ReactionMessage, StatusPresence, StickerMessage, TextMessage, VideoMessage } from "../../../shared/types"; 3 | import { AnyMessageContent, delay, MessageContentGenerationOptions, WAMessage, WAMessageKey, WASocket } from "@whiskeysockets/baileys"; 4 | import PrismaConnection from "../../../core/connection/prisma"; 5 | import { JsonObject } from "@prisma/client/runtime/library"; 6 | 7 | export default class MessagesController { 8 | 9 | private sock: WASocket | undefined; 10 | private jid: string; 11 | private delay: number | string; 12 | 13 | constructor(owner: string, instanceName: string, jid: string, delay: (number | string)){ 14 | const key = `${owner}_${instanceName}`; 15 | this.sock = instances[key]?.getSock(); 16 | this.jid = this.formatJid(jid); 17 | this.delay = delay; 18 | } 19 | 20 | async filterOptions(rawOptions: JsonObject): Promise{ 21 | 22 | const options: any = {}; 23 | 24 | if(rawOptions?.quoted && typeof rawOptions.quoted == 'string'){ 25 | 26 | const quoted = await PrismaConnection.getMessageById(rawOptions.quoted); 27 | 28 | if(quoted){ 29 | options.quoted = quoted; 30 | } 31 | 32 | } 33 | 34 | return options; 35 | 36 | } 37 | 38 | async sendMessageText(message: TextMessage, rawOptions: JsonObject | undefined){ 39 | 40 | const options: MessageContentGenerationOptions | undefined = (rawOptions ? await this.filterOptions(rawOptions) : undefined); 41 | 42 | return this.sendMessage(message, options); 43 | 44 | } 45 | 46 | 47 | async sendMessageLocation(message: LocationMessage, rawOptions: JsonObject | undefined){ 48 | 49 | const options: MessageContentGenerationOptions | undefined = (rawOptions ? await this.filterOptions(rawOptions) : undefined); 50 | 51 | return this.sendMessage(message, options); 52 | 53 | } 54 | 55 | 56 | async sendMessageContact(message: ContactMessage, rawOptions: JsonObject | undefined){ 57 | 58 | const options: MessageContentGenerationOptions | undefined = (rawOptions ? await this.filterOptions(rawOptions) : undefined); 59 | 60 | const vcard = 'BEGIN:VCARD\n' 61 | + 'VERSION:3.0\n' 62 | + `FN:${message.displayName}\n` 63 | + 'ORG:ZapToBox Whatsapp Api;\n' 64 | + `TEL;type=CELL;type=VOICE;waid=${message.waid}:${message.phoneNumber}\n` 65 | + 'END:VCARD'; 66 | 67 | const contact: AnyMessageContent = { 68 | contacts:{ 69 | displayName: message.displayName, 70 | contacts: [{vcard}] 71 | } 72 | }; 73 | 74 | return this.sendMessage(contact, options); 75 | 76 | } 77 | 78 | async sendMessageReaction(reaction: ReactionMessage){ 79 | 80 | const options: undefined = undefined; 81 | 82 | const messageReact = await PrismaConnection.getMessageById(reaction.messageId); 83 | 84 | if(!messageReact){ 85 | return { 86 | success: false, 87 | error: "Failed to send message.", 88 | }; 89 | } 90 | 91 | const message:AnyMessageContent = { 92 | react:{ 93 | text: reaction.emoji, 94 | key: messageReact.key 95 | } 96 | }; 97 | 98 | return this.sendMessage(message, options); 99 | 100 | } 101 | 102 | async sendMessagePoll(message: PollMessage, rawOptions: JsonObject | undefined){ 103 | 104 | const options: undefined = undefined; 105 | 106 | return this.sendMessage(message, options); 107 | 108 | } 109 | 110 | async sendMessageImage(message: ImageMessage, rawOptions: JsonObject | undefined){ 111 | 112 | const options: MessageContentGenerationOptions | undefined = (rawOptions ? await this.filterOptions(rawOptions) : undefined); 113 | 114 | return this.sendMessage(message, options); 115 | 116 | } 117 | 118 | async sendMessageVideo(message: VideoMessage, rawOptions: JsonObject | undefined){ 119 | 120 | const options: MessageContentGenerationOptions | undefined = (rawOptions ? await this.filterOptions(rawOptions) : undefined); 121 | 122 | return this.sendMessage(message, options); 123 | 124 | } 125 | 126 | async sendMessageGif(message: GifMessage, rawOptions: JsonObject | undefined){ 127 | 128 | const options: MessageContentGenerationOptions | undefined = (rawOptions ? await this.filterOptions(rawOptions) : undefined); 129 | 130 | return this.sendMessage(message, options); 131 | 132 | } 133 | 134 | async sendMessageAudio(message: AudioMessage, rawOptions: JsonObject | undefined){ 135 | 136 | const options: MessageContentGenerationOptions | undefined = (rawOptions ? await this.filterOptions(rawOptions) : undefined); 137 | 138 | return this.sendMessage(message, options); 139 | 140 | } 141 | 142 | async sendMessageDocument(message: DocumentMessage, rawOptions: JsonObject | undefined){ 143 | 144 | const options: MessageContentGenerationOptions | undefined = (rawOptions ? await this.filterOptions(rawOptions) : undefined); 145 | 146 | return this.sendMessage(message, options); 147 | 148 | } 149 | 150 | async sendMessageSticker(message: StickerMessage, rawOptions: JsonObject | undefined){ 151 | 152 | const options: MessageContentGenerationOptions | undefined = (rawOptions ? await this.filterOptions(rawOptions) : undefined); 153 | 154 | return this.sendMessage(message, options); 155 | 156 | } 157 | 158 | async sendMessage(message: AnyMessageContent, options: MessageContentGenerationOptions | undefined): Promise{ 159 | 160 | try{ 161 | 162 | const text = ("text" in message) ? message.text : ("caption" in message) ? message.caption : ""; 163 | 164 | const presence: StatusPresence = ("audio" in message ? "recording" : "composing"); 165 | 166 | await this.simulateTyping(presence, text); 167 | 168 | const sentMessage: WAMessage | undefined = await this.sock?.sendMessage(this.jid, message, options); 169 | 170 | if(!sentMessage || !sentMessage.key || !sentMessage.key.id){ 171 | return { 172 | success: false, 173 | message: "Failed to send message.", 174 | }; 175 | } 176 | 177 | return { 178 | success: true, 179 | message: "Message sent successfully.", 180 | }; 181 | 182 | }catch(err){ 183 | 184 | return { 185 | success: false, 186 | error: "Failed to send message: " + (err as Error).message, 187 | }; 188 | 189 | } 190 | 191 | } 192 | 193 | async deleteMessage(key: WAMessageKey, forEveryone: boolean): Promise{ 194 | 195 | try{ 196 | 197 | if(forEveryone){ 198 | await this.sock?.sendMessage(this.jid, { delete: key }); 199 | }else{ 200 | await this.sock?.chatModify( 201 | { 202 | deleteForMe: { 203 | deleteMedia: true, 204 | key: key, 205 | timestamp: Date.now() / 1000 206 | } 207 | }, 208 | this.jid 209 | ) 210 | } 211 | 212 | return { 213 | success: true, 214 | message: "Message deleted successfully.", 215 | }; 216 | 217 | }catch(err){ 218 | 219 | return { 220 | success: false, 221 | error: "Failed to delete message.", 222 | }; 223 | } 224 | 225 | } 226 | async readMessage(messageId: WAMessageKey): Promise{ 227 | 228 | try{ 229 | 230 | await this.sock?.readMessages([messageId]); 231 | 232 | return { 233 | success: true, 234 | message: "Message marked as read successfully.", 235 | }; 236 | 237 | }catch(err){ 238 | 239 | return { 240 | success: false, 241 | error: "Failed to mark message as read.", 242 | }; 243 | 244 | } 245 | 246 | } 247 | 248 | async unStar(messageId: string, remoteJid: string, star: boolean): Promise{ 249 | 250 | try{ 251 | 252 | const message: WAMessage | undefined = await PrismaConnection.getMessageById(messageId); 253 | 254 | if(!message){ 255 | return { 256 | success: false, 257 | error: "Failed to change star status in message, message not found.", 258 | }; 259 | } 260 | 261 | await this.sock?.chatModify({ 262 | star: { 263 | messages: [{ 264 | id: message?.key.id!, 265 | fromMe: message?.key?.fromMe! 266 | }], 267 | star 268 | } 269 | }, remoteJid); 270 | 271 | return { 272 | success: true, 273 | message: "Message marked star successfully.", 274 | }; 275 | 276 | }catch(err){ 277 | 278 | return { 279 | success: false, 280 | error: "Failed to change star status in message.", 281 | }; 282 | 283 | } 284 | 285 | } 286 | 287 | formatJid(jid: string){ 288 | if(jid.endsWith("@s.whatsapp.net") || jid.endsWith("@g.us") || jid.endsWith("@lid")){ 289 | return jid; 290 | } 291 | return `${jid}@s.whatsapp.net`; 292 | } 293 | 294 | calculateDelay(text: string){ 295 | const words = text.trim().split(/\s+/).length; 296 | const wpm = 40; // words per minute 297 | const delayInMinutes = words / wpm; 298 | return delayInMinutes * 60 * 1000; // convert to milliseconds 299 | } 300 | 301 | async simulateTyping(compose: StatusPresence, text: string): Promise{ 302 | 303 | await this.sock?.presenceSubscribe(this.jid); 304 | 305 | await delay(800); 306 | 307 | await this.sock?.sendPresenceUpdate(compose, this.jid); 308 | 309 | if(typeof this.delay === "string" && this.delay == "auto" && text.length > 0){ 310 | await delay(this.calculateDelay(text)); 311 | }else if(typeof this.delay === "number" && this.delay > 0){ 312 | await delay(this.delay); 313 | } 314 | 315 | await this.sock?.sendPresenceUpdate("paused", this.jid); 316 | 317 | } 318 | 319 | } -------------------------------------------------------------------------------- /src/infra/http/controllers/group.ts: -------------------------------------------------------------------------------- 1 | import { instances } from "../../../shared/constants"; 2 | import { ParticipantAction, proto, WASocket } from "@whiskeysockets/baileys"; 3 | import PrismaConnection from "../../../core/connection/prisma"; 4 | 5 | export default class GroupController { 6 | 7 | private sock: WASocket | undefined; 8 | private instance: string | undefined; 9 | 10 | constructor(owner: string, instanceName: string){ 11 | const key = `${owner}_${instanceName}`; 12 | this.instance = key; 13 | this.sock = instances[key]?.getSock(); 14 | } 15 | 16 | async create(groupName: string, participants: string[]){ 17 | 18 | try{ 19 | 20 | const group = await this.sock?.groupCreate(groupName, participants); 21 | 22 | return { 23 | success: true, 24 | message: "Group created success", 25 | data: group 26 | }; 27 | 28 | }catch(err){ 29 | 30 | return { 31 | success: false, 32 | error: "Error creating group" 33 | }; 34 | 35 | } 36 | 37 | } 38 | 39 | async participantsUpdate(remoteJid: string, participants: string[], method: ParticipantAction){ 40 | 41 | try{ 42 | 43 | await this.sock?.groupParticipantsUpdate(remoteJid, participants, method); 44 | 45 | return { 46 | success: true, 47 | message: "Participants status in group changed with success" 48 | }; 49 | 50 | }catch(err){ 51 | 52 | return { 53 | success: false, 54 | error: "Error on change status group" 55 | }; 56 | 57 | } 58 | 59 | } 60 | 61 | async updateSubject(remoteJid: string, subject: string){ 62 | 63 | try{ 64 | 65 | await this.sock?.groupUpdateSubject(remoteJid, subject); 66 | 67 | return { 68 | success: true, 69 | message: "Group subject changed with success" 70 | }; 71 | 72 | }catch(err){ 73 | 74 | return { 75 | success: false, 76 | error: "Error on change group subject" 77 | }; 78 | 79 | } 80 | 81 | } 82 | 83 | async updateDescription(remoteJid: string, description: string){ 84 | 85 | try{ 86 | 87 | await this.sock?.groupUpdateDescription(remoteJid, description); 88 | 89 | return { 90 | success: true, 91 | message: "Group description changed with success" 92 | }; 93 | 94 | }catch(err){ 95 | 96 | return { 97 | success: false, 98 | error: "Error on change group description" 99 | }; 100 | 101 | } 102 | 103 | } 104 | 105 | async updateSetting(remoteJid: string, setting: "announcement" | "not_announcement" | "locked" | "unlocked"){ 106 | 107 | try{ 108 | 109 | await this.sock?.groupSettingUpdate(remoteJid, setting); 110 | 111 | return { 112 | success: true, 113 | message: "Group subject changed with success" 114 | }; 115 | 116 | }catch(err){ 117 | 118 | return { 119 | success: false, 120 | error: "Error on change subject group" 121 | }; 122 | 123 | } 124 | 125 | } 126 | 127 | async leave(remoteJid: string){ 128 | 129 | try{ 130 | 131 | await this.sock?.groupLeave(remoteJid); 132 | 133 | return { 134 | success: true, 135 | message: "Leave group with success" 136 | }; 137 | 138 | }catch(err){ 139 | 140 | return { 141 | success: false, 142 | error: "Error on leave group" 143 | }; 144 | 145 | } 146 | 147 | } 148 | 149 | async getInviteCode(remoteJid: string){ 150 | 151 | try{ 152 | 153 | const code = await this.sock?.groupInviteCode(remoteJid); 154 | 155 | return { 156 | success: true, 157 | message: "Get invite code with success", 158 | data: { 159 | code, 160 | link: "https://chat.whatsapp.com/" + code 161 | } 162 | }; 163 | 164 | }catch(err){ 165 | 166 | return { 167 | success: false, 168 | error: "Error in get invite code" 169 | }; 170 | 171 | } 172 | 173 | } 174 | 175 | async revokeInviteCode(remoteJid: string){ 176 | 177 | try{ 178 | 179 | const code = await this.sock?.groupRevokeInvite(remoteJid); 180 | 181 | return { 182 | success: true, 183 | message: "Revoke invite code with success", 184 | data: { 185 | code, 186 | link: "https://chat.whatsapp.com/" + code 187 | } 188 | }; 189 | 190 | }catch(err){ 191 | 192 | return { 193 | success: false, 194 | error: "Error in revoke invite code" 195 | }; 196 | 197 | } 198 | 199 | } 200 | 201 | async join(code: string){ 202 | 203 | try{ 204 | 205 | const response = await this.sock?.groupAcceptInvite(code.replace("https://chat.whatsapp.com/", "")); 206 | 207 | return { 208 | success: true, 209 | message: "Join with success", 210 | data: { 211 | response 212 | } 213 | }; 214 | 215 | }catch(err){ 216 | 217 | return { 218 | success: false, 219 | error: "Error in join group" 220 | }; 221 | 222 | } 223 | 224 | } 225 | 226 | async joinByInviteMessage(groupJid: string, messageId: string){ 227 | 228 | try{ 229 | 230 | const messageInvite = await PrismaConnection.getMessageById(messageId) as proto.Message.IGroupInviteMessage; 231 | 232 | if(!messageInvite){ 233 | return { 234 | success: false, 235 | error: "Invite message not found" 236 | }; 237 | } 238 | 239 | const response = await this.sock?.groupAcceptInviteV4(groupJid, messageInvite); 240 | 241 | return { 242 | success: true, 243 | message: "Join with success", 244 | data: { 245 | response 246 | } 247 | }; 248 | 249 | }catch(err){ 250 | 251 | return { 252 | success: false, 253 | error: "Error in join group" 254 | }; 255 | 256 | } 257 | 258 | } 259 | 260 | async getInfoByCode(code: string){ 261 | 262 | try{ 263 | 264 | const response = await this.sock?.groupGetInviteInfo(code.replace("https://chat.whatsapp.com/", "")); 265 | 266 | return { 267 | success: true, 268 | message: "Get infos with success", 269 | data: { 270 | response 271 | } 272 | }; 273 | 274 | }catch(err){ 275 | 276 | return { 277 | success: false, 278 | error: "Error in get group infos" 279 | }; 280 | 281 | } 282 | 283 | } 284 | 285 | async queryMetadata(groupJid: string){ 286 | 287 | try{ 288 | 289 | const response = await this.sock?.groupMetadata(groupJid); 290 | 291 | return { 292 | success: true, 293 | message: "Get metadata with success", 294 | data: { 295 | response 296 | } 297 | }; 298 | 299 | }catch(err){ 300 | 301 | return { 302 | success: false, 303 | error: "Error in get group metadata" 304 | }; 305 | 306 | } 307 | 308 | } 309 | 310 | async participantsList(groupJid: string){ 311 | 312 | try{ 313 | 314 | const response = await this.sock?.groupRequestParticipantsList(groupJid); 315 | 316 | return { 317 | success: true, 318 | message: "Get participants list with success", 319 | data: { 320 | response 321 | } 322 | }; 323 | 324 | }catch(err){ 325 | 326 | return { 327 | success: false, 328 | error: "Error in get group participants list" 329 | }; 330 | 331 | } 332 | 333 | } 334 | 335 | async requestParticipants(groupJid: string, participants: string[], action: "approve" | "reject"){ 336 | 337 | try{ 338 | 339 | const response = await this.sock?.groupRequestParticipantsUpdate(groupJid, participants, action); 340 | 341 | return { 342 | success: true, 343 | message: "Update participants requests with success", 344 | data: { 345 | response 346 | } 347 | }; 348 | 349 | }catch(err){ 350 | 351 | return { 352 | success: false, 353 | error: "Error in update participants requests" 354 | }; 355 | 356 | } 357 | 358 | } 359 | 360 | async fetchAllParticipants(){ 361 | 362 | try{ 363 | 364 | const response = await this.sock?.groupFetchAllParticipating(); 365 | 366 | return { 367 | success: true, 368 | message: "Get all participants with success", 369 | data: { 370 | response 371 | } 372 | }; 373 | 374 | }catch(err){ 375 | 376 | return { 377 | success: false, 378 | error: "Error in get groups participants" 379 | }; 380 | 381 | } 382 | 383 | } 384 | 385 | async ephemeralMessages(groupJid: string, time: number){ 386 | 387 | try{ 388 | 389 | await this.sock?.groupToggleEphemeral(groupJid, time); 390 | 391 | return { 392 | success: true, 393 | message: "Message expiration defined" 394 | }; 395 | 396 | }catch(err){ 397 | 398 | return { 399 | success: false, 400 | error: "Error in set message expiration" 401 | }; 402 | 403 | } 404 | 405 | } 406 | 407 | async addMode(groupJid: string, onlyAdmin: boolean){ 408 | 409 | try{ 410 | 411 | const addMode = (onlyAdmin ? 'admin_add' : 'all_member_add'); 412 | await this.sock?.groupMemberAddMode(groupJid, addMode); 413 | 414 | return { 415 | success: true, 416 | message: "Group add Mode defined" 417 | }; 418 | 419 | }catch(err){ 420 | 421 | return { 422 | success: false, 423 | error: "Error in set add mode in group" 424 | }; 425 | 426 | } 427 | 428 | } 429 | 430 | } -------------------------------------------------------------------------------- /src/infra/http/routes/privacy.ts: -------------------------------------------------------------------------------- 1 | import express, { Request, Response } from "express"; 2 | import PrivacyController from "../controllers/privacy"; 3 | 4 | export default class PrivacyRoutes{ 5 | 6 | private router = express.Router(); 7 | 8 | get(){ 9 | 10 | this.router 11 | .patch("/unblock/:owner/:instanceName", async (req: Request, res: Response) => { 12 | 13 | const owner = req.params.owner; 14 | const instanceName = req.params.instanceName; 15 | 16 | if(!owner || !instanceName){ 17 | return res.status(400).json({ error: "Owner and instanceName are required." }); 18 | } 19 | 20 | const { remoteJid, block } = req.body; 21 | 22 | if(!remoteJid || typeof block === 'undefined'){ 23 | return res.status(400).json({ error: "Fields 'remoteJid' and 'block' is required." }); 24 | } 25 | 26 | const privController = new PrivacyController(owner, instanceName); 27 | const result = await privController.unBlockUser(remoteJid, block); 28 | 29 | if(result?.error){ 30 | return res.status(500).json(result); 31 | }else{ 32 | return res.status(200).json(result); 33 | } 34 | 35 | }) 36 | .get("/privacySettings/:owner/:instanceName", async (req: Request, res: Response) => { 37 | 38 | const owner = req.params.owner; 39 | const instanceName = req.params.instanceName; 40 | 41 | if(!owner || !instanceName){ 42 | return res.status(400).json({ error: "Owner and instanceName are required." }); 43 | } 44 | 45 | const privController = new PrivacyController(owner, instanceName); 46 | const result = await privController.getPrivacySettings(); 47 | 48 | if(result?.error){ 49 | return res.status(500).json(result); 50 | }else{ 51 | return res.status(200).json(result); 52 | } 53 | 54 | }) 55 | .get("/blockList/:owner/:instanceName", async (req: Request, res: Response) => { 56 | 57 | const owner = req.params.owner; 58 | const instanceName = req.params.instanceName; 59 | 60 | if(!owner || !instanceName){ 61 | return res.status(400).json({ error: "Owner and instanceName are required." }); 62 | } 63 | 64 | const privController = new PrivacyController(owner, instanceName); 65 | const result = await privController.getBlockList(); 66 | 67 | if(result?.error){ 68 | return res.status(500).json(result); 69 | }else{ 70 | return res.status(200).json(result); 71 | } 72 | 73 | }) 74 | .patch("/lastSeen/:owner/:instanceName", async (req: Request, res: Response) => { 75 | 76 | const owner = req.params.owner; 77 | const instanceName = req.params.instanceName; 78 | 79 | if(!owner || !instanceName){ 80 | return res.status(400).json({ error: "Owner and instanceName are required." }); 81 | } 82 | 83 | const { privacy } = req.body; 84 | 85 | if(!privacy){ 86 | return res.status(400).json({ error: "Field 'privacy' is required." }); 87 | }else if(!['all', 'contacts', 'contact_blacklist', 'none'].includes(privacy)){ 88 | return res.status(400).json({ error: "Field 'privacy' is invalid." }); 89 | } 90 | 91 | const privController = new PrivacyController(owner, instanceName); 92 | const result = await privController.updateLastSeen(privacy); 93 | 94 | if(result?.error){ 95 | return res.status(500).json(result); 96 | }else{ 97 | return res.status(200).json(result); 98 | } 99 | 100 | }) 101 | .patch("/online/:owner/:instanceName", async (req: Request, res: Response) => { 102 | 103 | const owner = req.params.owner; 104 | const instanceName = req.params.instanceName; 105 | 106 | if(!owner || !instanceName){ 107 | return res.status(400).json({ error: "Owner and instanceName are required." }); 108 | } 109 | 110 | const { privacy } = req.body; 111 | 112 | if(!privacy){ 113 | return res.status(400).json({ error: "Field 'privacy' is required." }); 114 | }else if(!['all', 'match_last_seen'].includes(privacy)){ 115 | return res.status(400).json({ error: "Field 'privacy' is invalid." }); 116 | } 117 | 118 | const privController = new PrivacyController(owner, instanceName); 119 | const result = await privController.updateOnline(privacy); 120 | 121 | if(result?.error){ 122 | return res.status(500).json(result); 123 | }else{ 124 | return res.status(200).json(result); 125 | } 126 | 127 | }) 128 | .patch("/picture/:owner/:instanceName", async (req: Request, res: Response) => { 129 | 130 | const owner = req.params.owner; 131 | const instanceName = req.params.instanceName; 132 | 133 | if(!owner || !instanceName){ 134 | return res.status(400).json({ error: "Owner and instanceName are required." }); 135 | } 136 | 137 | const { privacy } = req.body; 138 | 139 | if(!privacy){ 140 | return res.status(400).json({ error: "Field 'privacy' is required." }); 141 | }else if(!['all', 'contacts', 'contact_blacklist', 'none'].includes(privacy)){ 142 | return res.status(400).json({ error: "Field 'privacy' is invalid." }); 143 | } 144 | 145 | const privController = new PrivacyController(owner, instanceName); 146 | const result = await privController.profilePicture(privacy); 147 | 148 | if(result?.error){ 149 | return res.status(500).json(result); 150 | }else{ 151 | return res.status(200).json(result); 152 | } 153 | 154 | }) 155 | .patch("/status/:owner/:instanceName", async (req: Request, res: Response) => { 156 | 157 | const owner = req.params.owner; 158 | const instanceName = req.params.instanceName; 159 | 160 | if(!owner || !instanceName){ 161 | return res.status(400).json({ error: "Owner and instanceName are required." }); 162 | } 163 | 164 | const { privacy } = req.body; 165 | 166 | if(!privacy){ 167 | return res.status(400).json({ error: "Field 'privacy' is required." }); 168 | }else if(!['all', 'contacts', 'contact_blacklist', 'none'].includes(privacy)){ 169 | return res.status(400).json({ error: "Field 'privacy' is invalid." }); 170 | } 171 | 172 | const privController = new PrivacyController(owner, instanceName); 173 | const result = await privController.status(privacy); 174 | 175 | if(result?.error){ 176 | return res.status(500).json(result); 177 | }else{ 178 | return res.status(200).json(result); 179 | } 180 | 181 | }) 182 | .patch("/read/:owner/:instanceName", async (req: Request, res: Response) => { 183 | 184 | const owner = req.params.owner; 185 | const instanceName = req.params.instanceName; 186 | 187 | if(!owner || !instanceName){ 188 | return res.status(400).json({ error: "Owner and instanceName are required." }); 189 | } 190 | 191 | const { privacy } = req.body; 192 | 193 | if(!privacy){ 194 | return res.status(400).json({ error: "Field 'privacy' is required." }); 195 | }else if(!['all', 'none'].includes(privacy)){ 196 | return res.status(400).json({ error: "Field 'privacy' is invalid." }); 197 | } 198 | 199 | const privController = new PrivacyController(owner, instanceName); 200 | const result = await privController.markRead(privacy); 201 | 202 | if(result?.error){ 203 | return res.status(500).json(result); 204 | }else{ 205 | return res.status(200).json(result); 206 | } 207 | 208 | }) 209 | .patch("/addGroups/:owner/:instanceName", async (req: Request, res: Response) => { 210 | 211 | const owner = req.params.owner; 212 | const instanceName = req.params.instanceName; 213 | 214 | if(!owner || !instanceName){ 215 | return res.status(400).json({ error: "Owner and instanceName are required." }); 216 | } 217 | 218 | const { privacy } = req.body; 219 | 220 | if(!privacy){ 221 | return res.status(400).json({ error: "Field 'privacy' is required." }); 222 | }else if(!['all', 'contacts', 'contact_blacklist'].includes(privacy)){ 223 | return res.status(400).json({ error: "Field 'privacy' is invalid." }); 224 | } 225 | 226 | const privController = new PrivacyController(owner, instanceName); 227 | const result = await privController.addGroups(privacy); 228 | 229 | if(result?.error){ 230 | return res.status(500).json(result); 231 | }else{ 232 | return res.status(200).json(result); 233 | } 234 | 235 | }) 236 | .patch("/expirationMessage/:owner/:instanceName", async (req: Request, res: Response) => { 237 | 238 | const owner = req.params.owner; 239 | const instanceName = req.params.instanceName; 240 | 241 | if(!owner || !instanceName){ 242 | return res.status(400).json({ error: "Owner and instanceName are required." }); 243 | } 244 | 245 | const { ephemeral } = req.body; 246 | 247 | if(!ephemeral){ 248 | return res.status(400).json({ error: "Field 'privacy' is required." }); 249 | }else if(!['0', '24h', '7d', '90d'].includes(ephemeral)){ 250 | return res.status(400).json({ error: "Field 'privacy' is invalid." }); 251 | } 252 | 253 | let time: number = 0; 254 | 255 | switch(ephemeral){ 256 | case '24h':{ 257 | time = 86400; 258 | break; 259 | } 260 | case '7d':{ 261 | time = 604800; 262 | break; 263 | } 264 | case '90d':{ 265 | time = 7776000; 266 | break; 267 | } 268 | 269 | } 270 | 271 | const privController = new PrivacyController(owner, instanceName); 272 | const result = await privController.ephemeral(time); 273 | 274 | if(result?.error){ 275 | return res.status(500).json(result); 276 | }else{ 277 | return res.status(200).json(result); 278 | } 279 | 280 | }) 281 | return this.router; 282 | 283 | } 284 | 285 | } -------------------------------------------------------------------------------- /src/infra/http/routes/messages.ts: -------------------------------------------------------------------------------- 1 | import express, { Request, Response } from "express"; 2 | import MessagesController from "../controllers/messages"; 3 | import { WAMessageKey } from "@whiskeysockets/baileys"; 4 | import { isAudioMessage, isContactMessage, isDocumentMessage, isGifMessage, isImageMessage, isLocationMessage, isPollMessage, isReactionMessage, isStickerMessage, isTextMessage, isVideoMessage } from "../../../shared/guards"; 5 | 6 | export default class MessageRoutes{ 7 | 8 | private router = express.Router(); 9 | 10 | get(){ 11 | 12 | this.router 13 | .post("/sendText/:owner/:instanceName", async (req: Request, res: Response) => { 14 | 15 | const owner = req.params.owner; 16 | const instanceName = req.params.instanceName; 17 | 18 | if(!owner || !instanceName){ 19 | return res.status(400).json({ error: "Owner and instanceName are required." }); 20 | } 21 | 22 | const { jid, delay, message, options } = req.body; 23 | 24 | if(!owner || !instanceName || !jid || !message){ 25 | return res.status(400).json({ error: "Fields 'owner', 'instanceName', 'jid' and 'message' is required." }); 26 | }else if(!isTextMessage(message)){ 27 | return res.status(400).json({ error: "Invalid message format." }); 28 | } 29 | 30 | const messagesController = new MessagesController(owner, instanceName, jid, delay || 0); 31 | const result = await messagesController.sendMessageText(message, options); 32 | 33 | if(result?.error){ 34 | return res.status(500).json(result); 35 | }else{ 36 | return res.status(200).json(result); 37 | } 38 | 39 | }) 40 | .post("/sendLocation/:owner/:instanceName", async (req: Request, res: Response) => { 41 | 42 | const owner = req.params.owner; 43 | const instanceName = req.params.instanceName; 44 | 45 | if(!owner || !instanceName){ 46 | return res.status(400).json({ error: "Owner and instanceName are required." }); 47 | } 48 | 49 | const { jid, delay, message, options } = req.body; 50 | 51 | if(!owner || !instanceName || !jid || !message){ 52 | return res.status(400).json({ error: "Fields 'owner', 'instanceName', 'jid' and 'message' is required." }); 53 | }else if(!isLocationMessage(message)){ 54 | return res.status(400).json({ error: "Invalid message format." }); 55 | } 56 | 57 | const messagesController = new MessagesController(owner, instanceName, jid, delay || 0); 58 | const result = await messagesController.sendMessageLocation(message, options); 59 | 60 | if(result?.error){ 61 | return res.status(500).json(result); 62 | }else{ 63 | return res.status(200).json(result); 64 | } 65 | 66 | }) 67 | .post("/sendContact/:owner/:instanceName", async (req: Request, res: Response) => { 68 | 69 | const owner = req.params.owner; 70 | const instanceName = req.params.instanceName; 71 | 72 | if(!owner || !instanceName){ 73 | return res.status(400).json({ error: "Owner and instanceName are required." }); 74 | } 75 | 76 | const { jid, delay, message, options } = req.body; 77 | 78 | if(!owner || !instanceName || !jid || !message){ 79 | return res.status(400).json({ error: "Fields 'owner', 'instanceName', 'jid' and 'message' is required." }); 80 | }else if(!isContactMessage(message)){ 81 | return res.status(400).json({ error: "Invalid message format." }); 82 | } 83 | 84 | const messagesController = new MessagesController(owner, instanceName, jid, delay || 0); 85 | const result = await messagesController.sendMessageContact(message, options); 86 | 87 | if(result?.error){ 88 | return res.status(500).json(result); 89 | }else{ 90 | return res.status(200).json(result); 91 | } 92 | 93 | }) 94 | .post("/sendReaction/:owner/:instanceName", async (req: Request, res: Response) => { 95 | 96 | const owner = req.params.owner; 97 | const instanceName = req.params.instanceName; 98 | 99 | if(!owner || !instanceName){ 100 | return res.status(400).json({ error: "Owner and instanceName are required." }); 101 | } 102 | 103 | const { jid, delay, message } = req.body; 104 | 105 | if(!owner || !instanceName || !jid || !message){ 106 | return res.status(400).json({ error: "Fields 'owner', 'instanceName', 'jid' and 'message' is required." }); 107 | }else if(!isReactionMessage(message)){ 108 | return res.status(400).json({ error: "Invalid message format." }); 109 | } 110 | 111 | const messagesController = new MessagesController(owner, instanceName, jid, delay || 0); 112 | const result = await messagesController.sendMessageReaction(message); 113 | 114 | if(result?.error){ 115 | return res.status(500).json(result); 116 | }else{ 117 | return res.status(200).json(result); 118 | } 119 | 120 | }) 121 | .post("/sendPoll/:owner/:instanceName", async (req: Request, res: Response) => { 122 | 123 | const owner = req.params.owner; 124 | const instanceName = req.params.instanceName; 125 | 126 | if(!owner || !instanceName){ 127 | return res.status(400).json({ error: "Owner and instanceName are required." }); 128 | } 129 | 130 | const { jid, delay, message, options } = req.body; 131 | 132 | if(!owner || !instanceName || !jid || !message){ 133 | return res.status(400).json({ error: "Fields 'owner', 'instanceName', 'jid' and 'message' is required." }); 134 | }else if(!isPollMessage(message)){ 135 | return res.status(400).json({ error: "Invalid message format." }); 136 | } 137 | 138 | const messagesController = new MessagesController(owner, instanceName, jid, delay || 0); 139 | const result = await messagesController.sendMessagePoll(message, options); 140 | 141 | if(result?.error){ 142 | return res.status(500).json(result); 143 | }else{ 144 | return res.status(200).json(result); 145 | } 146 | 147 | }) 148 | .post("/sendImage/:owner/:instanceName", async (req: Request, res: Response) => { 149 | 150 | const owner = req.params.owner; 151 | const instanceName = req.params.instanceName; 152 | 153 | if(!owner || !instanceName){ 154 | return res.status(400).json({ error: "Owner and instanceName are required." }); 155 | } 156 | 157 | const { jid, delay, message, options } = req.body; 158 | 159 | if(!owner || !instanceName || !jid || !message){ 160 | return res.status(400).json({ error: "Fields 'owner', 'instanceName', 'jid' and 'message' is required." }); 161 | }else if(!isImageMessage(message)){ 162 | return res.status(400).json({ error: "Invalid message format." }); 163 | } 164 | 165 | const messagesController = new MessagesController(owner, instanceName, jid, delay || 0); 166 | const result = await messagesController.sendMessageImage(message, options); 167 | 168 | if(result?.error){ 169 | return res.status(500).json(result); 170 | }else{ 171 | return res.status(200).json(result); 172 | } 173 | 174 | }) 175 | .post("/sendVideo/:owner/:instanceName", async (req: Request, res: Response) => { 176 | 177 | const owner = req.params.owner; 178 | const instanceName = req.params.instanceName; 179 | 180 | if(!owner || !instanceName){ 181 | return res.status(400).json({ error: "Owner and instanceName are required." }); 182 | } 183 | 184 | const { jid, delay, message, options } = req.body; 185 | 186 | if(!owner || !instanceName || !jid || !message){ 187 | return res.status(400).json({ error: "Fields 'owner', 'instanceName', 'jid' and 'message' is required." }); 188 | }else if(!isVideoMessage(message)){ 189 | return res.status(400).json({ error: "Invalid message format." }); 190 | } 191 | 192 | const messagesController = new MessagesController(owner, instanceName, jid, delay || 0); 193 | const result = await messagesController.sendMessageVideo(message, options); 194 | 195 | if(result?.error){ 196 | return res.status(500).json(result); 197 | }else{ 198 | return res.status(200).json(result); 199 | } 200 | 201 | }) 202 | .post("/sendGif/:owner/:instanceName", async (req: Request, res: Response) => { 203 | 204 | const owner = req.params.owner; 205 | const instanceName = req.params.instanceName; 206 | 207 | if(!owner || !instanceName){ 208 | return res.status(400).json({ error: "Owner and instanceName are required." }); 209 | } 210 | 211 | const { jid, delay, message, options } = req.body; 212 | 213 | if(!owner || !instanceName || !jid || !message){ 214 | return res.status(400).json({ error: "Fields 'owner', 'instanceName', 'jid' and 'message' is required." }); 215 | }else if(!isGifMessage(message)){ 216 | return res.status(400).json({ error: "Invalid message format." }); 217 | } 218 | 219 | const messagesController = new MessagesController(owner, instanceName, jid, delay || 0); 220 | const result = await messagesController.sendMessageGif(message, options); 221 | 222 | if(result?.error){ 223 | return res.status(500).json(result); 224 | }else{ 225 | return res.status(200).json(result); 226 | } 227 | 228 | }) 229 | .post("/sendAudio/:owner/:instanceName", async (req: Request, res: Response) => { 230 | 231 | const owner = req.params.owner; 232 | const instanceName = req.params.instanceName; 233 | 234 | if(!owner || !instanceName){ 235 | return res.status(400).json({ error: "Owner and instanceName are required." }); 236 | } 237 | 238 | const { jid, delay, message, options } = req.body; 239 | 240 | if(!owner || !instanceName || !jid || !message){ 241 | return res.status(400).json({ error: "Fields 'owner', 'instanceName', 'jid' and 'message' is required." }); 242 | }else if(!isAudioMessage(message)){ 243 | return res.status(400).json({ error: "Invalid message format." }); 244 | } 245 | 246 | const messagesController = new MessagesController(owner, instanceName, jid, delay || 0); 247 | const result = await messagesController.sendMessageAudio(message, options); 248 | 249 | if(result?.error){ 250 | return res.status(500).json(result); 251 | }else{ 252 | return res.status(200).json(result); 253 | } 254 | 255 | }) 256 | .post("/sendDocument/:owner/:instanceName", async (req: Request, res: Response) => { 257 | 258 | const owner = req.params.owner; 259 | const instanceName = req.params.instanceName; 260 | 261 | if(!owner || !instanceName){ 262 | return res.status(400).json({ error: "Owner and instanceName are required." }); 263 | } 264 | 265 | const { jid, delay, message, options } = req.body; 266 | 267 | if(!owner || !instanceName || !jid || !message){ 268 | return res.status(400).json({ error: "Fields 'owner', 'instanceName', 'jid' and 'message' is required." }); 269 | }else if(!isDocumentMessage(message)){ 270 | return res.status(400).json({ error: "Invalid message format." }); 271 | } 272 | 273 | const messagesController = new MessagesController(owner, instanceName, jid, delay || 0); 274 | const result = await messagesController.sendMessageDocument(message, options); 275 | 276 | if(result?.error){ 277 | return res.status(500).json(result); 278 | }else{ 279 | return res.status(200).json(result); 280 | } 281 | 282 | }) 283 | 284 | .post("/sendSticker/:owner/:instanceName", async (req: Request, res: Response) => { 285 | 286 | const owner = req.params.owner; 287 | const instanceName = req.params.instanceName; 288 | 289 | if(!owner || !instanceName){ 290 | return res.status(400).json({ error: "Owner and instanceName are required." }); 291 | } 292 | 293 | const { jid, delay, message, options } = req.body; 294 | 295 | if(!owner || !instanceName || !jid || !message){ 296 | return res.status(400).json({ error: "Fields 'owner', 'instanceName', 'jid' and 'message' is required." }); 297 | }else if(!isStickerMessage(message)){ 298 | return res.status(400).json({ error: "Invalid message format." }); 299 | } 300 | 301 | const messagesController = new MessagesController(owner, instanceName, jid, delay || 0); 302 | const result = await messagesController.sendMessageSticker(message, options); 303 | 304 | if(result?.error){ 305 | return res.status(500).json(result); 306 | }else{ 307 | return res.status(200).json(result); 308 | } 309 | 310 | }) 311 | .patch("/readMessage/:owner/:instanceName", async (req: Request, res: Response) => { 312 | 313 | const owner = req.params.owner; 314 | const instanceName = req.params.instanceName; 315 | 316 | if(!owner || !instanceName){ 317 | return res.status(400).json({ error: "Owner and instanceName are required." }); 318 | } 319 | 320 | const { messageId, remoteJid, participant, isViewOnce } = req.body; 321 | 322 | const key: WAMessageKey = { 323 | id: messageId, 324 | remoteJid: remoteJid, 325 | participant: participant, 326 | isViewOnce: isViewOnce || false 327 | }; 328 | 329 | const messagesController = new MessagesController(owner, instanceName, remoteJid, 0); 330 | 331 | const result = await messagesController.readMessage(key); 332 | 333 | if(result?.error){ 334 | return res.status(500).json(result); 335 | }else{ 336 | return res.status(200).json(result); 337 | } 338 | 339 | }) 340 | .delete("/deleteMessage/:owner/:instanceName", async (req: Request, res: Response) => { 341 | 342 | const owner = req.params.owner; 343 | const instanceName = req.params.instanceName 344 | 345 | if(!owner || !instanceName){ 346 | return res.status(400).json({ error: "Owner and instanceName are required." }); 347 | } 348 | 349 | const { messageId, remoteJid, forEveryone } = req.body; 350 | 351 | if(!messageId || !remoteJid || !forEveryone){ 352 | return res.status(400).json({ error: "messageId, remoteJid and forEveryone are required." }); 353 | } 354 | 355 | const key: WAMessageKey = { 356 | id: messageId, 357 | remoteJid: remoteJid 358 | }; 359 | 360 | const messagesController = new MessagesController(owner, instanceName, remoteJid, 0); 361 | const result = await messagesController.deleteMessage(key, forEveryone || false); 362 | 363 | if(result?.error){ 364 | return res.status(500).json(result); 365 | }else{ 366 | return res.status(200).json(result); 367 | } 368 | 369 | }) 370 | .patch("/unstar/:owner/:instanceName", async (req: Request, res: Response) => { 371 | 372 | const owner = req.params.owner; 373 | const instanceName = req.params.instanceName 374 | 375 | if(!owner || !instanceName){ 376 | return res.status(400).json({ error: "Owner and instanceName are required." }); 377 | } 378 | 379 | const { messageId, remoteJid, star } = req.body; 380 | 381 | if(!messageId || !remoteJid || !star){ 382 | return res.status(400).json({ error: "messageId, remoteJid and star are required." }); 383 | } 384 | 385 | const messagesController = new MessagesController(owner, instanceName, remoteJid, 0); 386 | const result = await messagesController.unStar(messageId, remoteJid, star); 387 | 388 | if(result?.error){ 389 | return res.status(500).json(result); 390 | }else{ 391 | return res.status(200).json(result); 392 | } 393 | 394 | }) 395 | return this.router; 396 | 397 | } 398 | 399 | } -------------------------------------------------------------------------------- /src/infra/http/routes/group.ts: -------------------------------------------------------------------------------- 1 | import express, { Request, Response } from "express"; 2 | import GroupController from "../controllers/group"; 3 | 4 | 5 | export default class GroupRoutes{ 6 | 7 | private router = express.Router(); 8 | 9 | get(){ 10 | 11 | this.router 12 | .post("/create/:owner/:instanceName", async (req: Request, res: Response) => { 13 | 14 | const owner = req.params.owner; 15 | const instanceName = req.params.instanceName; 16 | 17 | if(!owner || !instanceName){ 18 | return res.status(400).json({ error: "Owner and instanceName are required." }); 19 | } 20 | 21 | const { groupName, participants } = req.body; 22 | 23 | if(!groupName || !participants){ 24 | return res.status(400).json({ error: "Field 'id' is required." }); 25 | }else if(!Array.isArray(participants) || participants.length < 2){ 26 | return res.status(400).json({ error: "Minimum of 2 participants required." }); 27 | } 28 | 29 | const groupController = new GroupController(owner, instanceName); 30 | const result = await groupController.create(groupName, participants); 31 | 32 | if(result?.error){ 33 | return res.status(500).json(result); 34 | }else{ 35 | return res.status(200).json(result); 36 | } 37 | 38 | }) 39 | .patch("/participantsUpdate/:owner/:instanceName", async (req: Request, res: Response) => { 40 | 41 | const owner = req.params.owner; 42 | const instanceName = req.params.instanceName; 43 | 44 | if(!owner || !instanceName){ 45 | return res.status(400).json({ error: "Owner and instanceName are required." }); 46 | } 47 | 48 | const { groupJid, participants, method } = req.body; 49 | 50 | if(!groupJid || !method || !participants){ 51 | return res.status(400).json({ error: "Fields 'groupJid', 'method', 'participants' is required." }); 52 | }else if(!Array.isArray(participants) || participants.length < 1){ 53 | return res.status(400).json({ error: "Minimum of 1 participant required." }); 54 | }else if(!['add', 'remove', 'demote', 'promote'].includes(method)){ 55 | return res.status(400).json({ error: "Invalid participant action method." }); 56 | } 57 | 58 | const groupController = new GroupController(owner, instanceName); 59 | const result = await groupController.participantsUpdate(groupJid, participants, method); 60 | 61 | if(result?.error){ 62 | return res.status(500).json(result); 63 | }else{ 64 | return res.status(200).json(result); 65 | } 66 | 67 | }) 68 | .patch("/subject/:owner/:instanceName", async (req: Request, res: Response) => { 69 | 70 | const owner = req.params.owner; 71 | const instanceName = req.params.instanceName; 72 | 73 | if(!owner || !instanceName){ 74 | return res.status(400).json({ error: "Owner and instanceName are required." }); 75 | } 76 | 77 | const { groupJid, subject } = req.body; 78 | 79 | if(!groupJid || !subject){ 80 | return res.status(400).json({ error: "Fields 'groupJid' and 'subject' is required." }); 81 | } 82 | 83 | const groupController = new GroupController(owner, instanceName); 84 | const result = await groupController.updateSubject(groupJid, subject); 85 | 86 | if(result?.error){ 87 | return res.status(500).json(result); 88 | }else{ 89 | return res.status(200).json(result); 90 | } 91 | 92 | }) 93 | .patch("/description/:owner/:instanceName", async (req: Request, res: Response) => { 94 | 95 | const owner = req.params.owner; 96 | const instanceName = req.params.instanceName; 97 | 98 | if(!owner || !instanceName){ 99 | return res.status(400).json({ error: "Owner and instanceName are required." }); 100 | } 101 | 102 | const { groupJid, description } = req.body; 103 | 104 | if(!groupJid || !description){ 105 | return res.status(400).json({ error: "Fields 'groupJid' and 'description' is required." }); 106 | } 107 | 108 | const groupController = new GroupController(owner, instanceName); 109 | const result = await groupController.updateDescription(groupJid, description); 110 | 111 | if(result?.error){ 112 | return res.status(500).json(result); 113 | }else{ 114 | return res.status(200).json(result); 115 | } 116 | 117 | }) 118 | .patch("/setting/:owner/:instanceName", async (req: Request, res: Response) => { 119 | 120 | const owner = req.params.owner; 121 | const instanceName = req.params.instanceName; 122 | 123 | if(!owner || !instanceName){ 124 | return res.status(400).json({ error: "Owner and instanceName are required." }); 125 | } 126 | 127 | const { groupJid, setting } = req.body; 128 | 129 | if(!groupJid || !setting){ 130 | return res.status(400).json({ error: "Fields 'groupJid' and 'setting' is required." }); 131 | }else if(!["announcement", "not_announcement", "locked", "unlocked"].includes(setting)){ 132 | return res.status(400).json({ error: "Invalid setting option" }); 133 | } 134 | 135 | const groupController = new GroupController(owner, instanceName); 136 | const result = await groupController.updateSetting(groupJid, setting); 137 | 138 | if(result?.error){ 139 | return res.status(500).json(result); 140 | }else{ 141 | return res.status(200).json(result); 142 | } 143 | 144 | }) 145 | .post("/leave/:owner/:instanceName", async (req: Request, res: Response) => { 146 | 147 | const owner = req.params.owner; 148 | const instanceName = req.params.instanceName; 149 | 150 | if(!owner || !instanceName){ 151 | return res.status(400).json({ error: "Owner and instanceName are required." }); 152 | } 153 | 154 | const { groupJid } = req.body; 155 | 156 | if(!groupJid){ 157 | return res.status(400).json({ error: "Field 'groupJid' is required." }); 158 | } 159 | 160 | const groupController = new GroupController(owner, instanceName); 161 | const result = await groupController.leave(groupJid); 162 | 163 | if(result?.error){ 164 | return res.status(500).json(result); 165 | }else{ 166 | return res.status(200).json(result); 167 | } 168 | 169 | }) 170 | .post("/getInviteCode/:owner/:instanceName", async (req: Request, res: Response) => { 171 | 172 | const owner = req.params.owner; 173 | const instanceName = req.params.instanceName; 174 | 175 | if(!owner || !instanceName){ 176 | return res.status(400).json({ error: "Owner and instanceName are required." }); 177 | } 178 | 179 | const { groupJid } = req.body; 180 | 181 | if(!groupJid){ 182 | return res.status(400).json({ error: "Field 'groupJid' is required." }); 183 | } 184 | 185 | const groupController = new GroupController(owner, instanceName); 186 | const result = await groupController.getInviteCode(groupJid); 187 | 188 | if(result?.error){ 189 | return res.status(500).json(result); 190 | }else{ 191 | return res.status(200).json(result); 192 | } 193 | 194 | }) 195 | .post("/revokeInviteCode/:owner/:instanceName", async (req: Request, res: Response) => { 196 | 197 | const owner = req.params.owner; 198 | const instanceName = req.params.instanceName; 199 | 200 | if(!owner || !instanceName){ 201 | return res.status(400).json({ error: "Owner and instanceName are required." }); 202 | } 203 | 204 | const { groupJid } = req.body; 205 | 206 | if(!groupJid){ 207 | return res.status(400).json({ error: "Field 'groupJid' is required." }); 208 | } 209 | 210 | const groupController = new GroupController(owner, instanceName); 211 | const result = await groupController.revokeInviteCode(groupJid); 212 | 213 | if(result?.error){ 214 | return res.status(500).json(result); 215 | }else{ 216 | return res.status(200).json(result); 217 | } 218 | 219 | }) 220 | .post("/join/:owner/:instanceName", async (req: Request, res: Response) => { 221 | 222 | const owner = req.params.owner; 223 | const instanceName = req.params.instanceName; 224 | 225 | if(!owner || !instanceName){ 226 | return res.status(400).json({ error: "Owner and instanceName are required." }); 227 | } 228 | 229 | const { code } = req.body; 230 | 231 | if(!code){ 232 | return res.status(400).json({ error: "Field 'code' is required." }); 233 | } 234 | 235 | const groupController = new GroupController(owner, instanceName); 236 | const result = await groupController.join(code); 237 | 238 | if(result?.error){ 239 | return res.status(500).json(result); 240 | }else{ 241 | return res.status(200).json(result); 242 | } 243 | 244 | }) 245 | .post("/joinByInviteMessage/:owner/:instanceName", async (req: Request, res: Response) => { 246 | 247 | const owner = req.params.owner; 248 | const instanceName = req.params.instanceName; 249 | 250 | if(!owner || !instanceName){ 251 | return res.status(400).json({ error: "Owner and instanceName are required." }); 252 | } 253 | 254 | const { groupJid, messageId } = req.body; 255 | 256 | if(!messageId || !groupJid){ 257 | return res.status(400).json({ error: "Fields 'messageId' and 'groupJid' is required." }); 258 | } 259 | 260 | const groupController = new GroupController(owner, instanceName); 261 | const result = await groupController.joinByInviteMessage(groupJid, messageId); 262 | 263 | if(result?.error){ 264 | return res.status(500).json(result); 265 | }else{ 266 | return res.status(200).json(result); 267 | } 268 | 269 | }) 270 | .post("/infoByCode/:owner/:instanceName", async (req: Request, res: Response) => { 271 | 272 | const owner = req.params.owner; 273 | const instanceName = req.params.instanceName; 274 | 275 | if(!owner || !instanceName){ 276 | return res.status(400).json({ error: "Owner and instanceName are required." }); 277 | } 278 | 279 | const { code } = req.body; 280 | 281 | if(!code){ 282 | return res.status(400).json({ error: "Field 'code' is required." }); 283 | } 284 | 285 | const groupController = new GroupController(owner, instanceName); 286 | const result = await groupController.getInfoByCode(code); 287 | 288 | if(result?.error){ 289 | return res.status(500).json(result); 290 | }else{ 291 | return res.status(200).json(result); 292 | } 293 | 294 | }) 295 | .post("/metadata/:owner/:instanceName", async (req: Request, res: Response) => { 296 | 297 | const owner = req.params.owner; 298 | const instanceName = req.params.instanceName; 299 | 300 | if(!owner || !instanceName){ 301 | return res.status(400).json({ error: "Owner and instanceName are required." }); 302 | } 303 | 304 | const { groupJid } = req.body; 305 | 306 | if(!groupJid){ 307 | return res.status(400).json({ error: "Field 'groupJid' is required." }); 308 | } 309 | 310 | const groupController = new GroupController(owner, instanceName); 311 | const result = await groupController.queryMetadata(groupJid); 312 | 313 | if(result?.error){ 314 | return res.status(500).json(result); 315 | }else{ 316 | return res.status(200).json(result); 317 | } 318 | 319 | }) 320 | .post("/participantsList/:owner/:instanceName", async (req: Request, res: Response) => { 321 | 322 | const owner = req.params.owner; 323 | const instanceName = req.params.instanceName; 324 | 325 | if(!owner || !instanceName){ 326 | return res.status(400).json({ error: "Owner and instanceName are required." }); 327 | } 328 | 329 | const { groupJid } = req.body; 330 | 331 | if(!groupJid){ 332 | return res.status(400).json({ error: "Field 'groupJid' is required." }); 333 | } 334 | 335 | const groupController = new GroupController(owner, instanceName); 336 | const result = await groupController.participantsList(groupJid); 337 | 338 | if(result?.error){ 339 | return res.status(500).json(result); 340 | }else{ 341 | return res.status(200).json(result); 342 | } 343 | 344 | }) 345 | .get("/allParticipantsGroups/:owner/:instanceName", async (req: Request, res: Response) => { 346 | 347 | const owner = req.params.owner; 348 | const instanceName = req.params.instanceName; 349 | 350 | if(!owner || !instanceName){ 351 | return res.status(400).json({ error: "Owner and instanceName are required." }); 352 | } 353 | 354 | const groupController = new GroupController(owner, instanceName); 355 | const result = await groupController.fetchAllParticipants(); 356 | 357 | if(result?.error){ 358 | return res.status(500).json(result); 359 | }else{ 360 | return res.status(200).json(result); 361 | } 362 | 363 | }) 364 | .patch("/requestParticipants/:owner/:instanceName", async (req: Request, res: Response) => { 365 | 366 | const owner = req.params.owner; 367 | const instanceName = req.params.instanceName; 368 | 369 | if(!owner || !instanceName){ 370 | return res.status(400).json({ error: "Owner and instanceName are required." }); 371 | } 372 | 373 | const { groupJid, participants, action } = req.body; 374 | 375 | if(!groupJid || !participants || !action){ 376 | return res.status(400).json({ error: "Fields 'groupJid', 'action' and 'participants' is required." }); 377 | }else if(!['approve', 'reject'].includes(action)){ 378 | return res.status(400).json({ error: "Invalid action value" }); 379 | } 380 | 381 | const groupController = new GroupController(owner, instanceName); 382 | const result = await groupController.requestParticipants(groupJid, participants, action); 383 | 384 | if(result?.error){ 385 | return res.status(500).json(result); 386 | }else{ 387 | return res.status(200).json(result); 388 | } 389 | 390 | }) 391 | .patch("/expirationMessage/:owner/:instanceName", async (req: Request, res: Response) => { 392 | 393 | const owner = req.params.owner; 394 | const instanceName = req.params.instanceName; 395 | 396 | if(!owner || !instanceName){ 397 | return res.status(400).json({ error: "Owner and instanceName are required." }); 398 | } 399 | 400 | const { groupJid, time } = req.body; 401 | 402 | if(!groupJid || typeof time === 'undefined'){ 403 | return res.status(400).json({ error: "Fields 'groupJid' and 'time' is required." }); 404 | }else if(!['0', '24h', '7d', '90d'].includes(time)){ 405 | return res.status(400).json({ error: "Invalid action value" }); 406 | } 407 | 408 | let ephemeralTime; 409 | 410 | switch(time){ 411 | case '24h':{ 412 | ephemeralTime = 86400; 413 | break; 414 | } 415 | case '7d':{ 416 | ephemeralTime = 604800; 417 | break; 418 | } 419 | case '90d':{ 420 | ephemeralTime = 7776000; 421 | break; 422 | } 423 | default:{ 424 | ephemeralTime = 0; 425 | } 426 | } 427 | 428 | const groupController = new GroupController(owner, instanceName); 429 | const result = await groupController.ephemeralMessages(groupJid, ephemeralTime); 430 | 431 | if(result?.error){ 432 | return res.status(500).json(result); 433 | }else{ 434 | return res.status(200).json(result); 435 | } 436 | 437 | }) 438 | .patch("/addMode/:owner/:instanceName", async (req: Request, res: Response) => { 439 | 440 | const owner = req.params.owner; 441 | const instanceName = req.params.instanceName; 442 | 443 | if(!owner || !instanceName){ 444 | return res.status(400).json({ error: "Owner and instanceName are required." }); 445 | } 446 | 447 | const { groupJid, onlyAdmin } = req.body; 448 | 449 | if(!groupJid || typeof onlyAdmin === 'undefined'){ 450 | return res.status(400).json({ error: "Fields 'groupJid' and 'onlyAdmin' is required." }); 451 | } 452 | 453 | const groupController = new GroupController(owner, instanceName); 454 | const result = await groupController.addMode(groupJid, onlyAdmin); 455 | 456 | if(result?.error){ 457 | return res.status(500).json(result); 458 | }else{ 459 | return res.status(200).json(result); 460 | } 461 | 462 | }) 463 | return this.router; 464 | 465 | } 466 | 467 | } -------------------------------------------------------------------------------- /src/infra/baileys/services.ts: -------------------------------------------------------------------------------- 1 | import makeWASocket, { 2 | DisconnectReason, 3 | useMultiFileAuthState, 4 | WASocket, 5 | fetchLatestBaileysVersion, 6 | BaileysEventMap, 7 | WABrowserDescription, 8 | CacheStore, 9 | getAggregateVotesInPollMessage, 10 | WAMessage, 11 | proto, 12 | delay, 13 | getContentType, 14 | } from "@whiskeysockets/baileys"; 15 | import * as fs from "fs"; 16 | import * as path from "path"; 17 | import QRCode from "qrcode"; 18 | import { release } from "os"; 19 | import NodeCache from "node-cache" 20 | import P from "pino"; 21 | import { baileysEvents, instanceConnection, instances, instanceStatus, sessionsPath } from "../../shared/constants"; 22 | import { clearInstanceWebhooks, genProxy, removeInstancePath, trySendWebhook } from "../../shared/utils"; 23 | import { ConnectionStatus, InstanceData, MessageWebhook } from "../../shared/types"; 24 | import UserConfig from "../config/env"; 25 | import PrismaConnection from "../../core/connection/prisma"; 26 | 27 | const msgRetryCounterCache: CacheStore = new NodeCache(); 28 | const userDevicesCache: CacheStore = new NodeCache(); 29 | const groupCache = new NodeCache({stdTTL: 5 * 60, useClones: false}); 30 | 31 | export default class Instance{ 32 | 33 | private sock!: WASocket; 34 | private instance!: InstanceData; 35 | private owner!: string; 36 | private instanceName!: string; 37 | private key!: string; 38 | private instancePath!: string; 39 | private qrCodeCount!: number; 40 | private qrCodeResolver?: (qrBase64: string) => void; 41 | private qrCodePromise?: Promise; 42 | private phoneNumber?: string | undefined; 43 | private reconnectAttempts = 0; 44 | private maxReconnectAttempts = 5; 45 | 46 | getSock(): (WASocket | undefined){ 47 | return this?.sock; 48 | } 49 | 50 | async create(data: { owner: string; instanceName: string , phoneNumber: string | undefined}) { 51 | this.owner = String(data.owner); 52 | this.instanceName = String(data.instanceName); 53 | this.phoneNumber = data.phoneNumber?.replace(/\D/g, ""); 54 | 55 | this.instancePath = path.join(sessionsPath, this.owner, this.instanceName); 56 | if (!fs.existsSync(path.join(sessionsPath, this.owner))){ 57 | fs.mkdirSync(path.join(sessionsPath, this.owner)); 58 | } 59 | if (!fs.existsSync(this.instancePath)) { 60 | fs.mkdirSync(this.instancePath); 61 | } 62 | 63 | const { state, saveCreds } = await useMultiFileAuthState(this.instancePath); 64 | const { version } = await fetchLatestBaileysVersion(); 65 | 66 | const browser: WABrowserDescription = [UserConfig.sessionClient, UserConfig.sessionName, release()]; 67 | const agents = await genProxy(UserConfig.proxyUrl); 68 | 69 | this.qrCodePromise = new Promise((resolve) => { 70 | this.qrCodeResolver = resolve; 71 | }); 72 | 73 | let sock: WASocket | undefined; 74 | try{ 75 | sock = makeWASocket({ 76 | auth: state, 77 | version, 78 | browser, 79 | emitOwnEvents: true, 80 | generateHighQualityLinkPreview: true, 81 | syncFullHistory: true, 82 | msgRetryCounterCache: msgRetryCounterCache, 83 | userDevicesCache: userDevicesCache, 84 | enableAutoSessionRecreation: true, 85 | agent: agents.wsAgent, 86 | fetchAgent: agents.fetchAgent, 87 | retryRequestDelayMs: 3 * 1000, 88 | maxMsgRetryCount: 1000, 89 | logger: P({level: 'fatal'}), 90 | cachedGroupMetadata: async (jid) => groupCache.get(jid), 91 | getMessage: async (key) => await this.getMessage(key.id!) as proto.IMessage, 92 | qrTimeout: UserConfig.qrCodeTimeout * 1000 93 | }); 94 | }catch(err){ 95 | console.error(`[${this.owner}/${this.instanceName}] Error creating socket`, err); 96 | await this.reconnectWithBackoff(); 97 | throw err; 98 | } 99 | 100 | this.sock = sock; 101 | this.attachSocketErrorHandlers(); 102 | 103 | this.key = `${this.owner}_${this.instanceName}`; 104 | 105 | this.instance = { 106 | owner: this.owner, 107 | instanceName: this.instanceName, 108 | socket: this.sock, 109 | connectionStatus: "OFFLINE", 110 | }; 111 | 112 | instanceConnection[this.key] = this.instance; 113 | 114 | this.setStatus("OFFLINE"); 115 | 116 | this.instanceEvents(saveCreds); 117 | 118 | this.qrCodeCount = 0; 119 | 120 | let qrCodeReturn: string | undefined; 121 | let pairingCodeReturn: string | undefined; 122 | 123 | if(this.phoneNumber){ 124 | 125 | if(!this.sock.authState.creds.registered){ 126 | 127 | try { 128 | 129 | const pNumber = this.phoneNumber; 130 | 131 | await delay(500); 132 | 133 | pairingCodeReturn = await this.sock.requestPairingCode(pNumber); 134 | 135 | console.log(pairingCodeReturn); 136 | 137 | } catch(err) { 138 | console.log("Error requesting pairing code:", err); 139 | pairingCodeReturn = undefined; 140 | } 141 | 142 | } 143 | 144 | }else{ 145 | 146 | try { 147 | const qrCode = await Promise.race([ 148 | this.qrCodePromise, 149 | new Promise((_, reject) => 150 | setTimeout(() => reject(new Error('QR code timeout')), UserConfig.qrCodeTimeout * 1000) 151 | ) 152 | ]); 153 | qrCodeReturn = qrCode; 154 | } catch { 155 | qrCodeReturn = undefined; 156 | } 157 | 158 | } 159 | 160 | return { 161 | instance: this.instance, 162 | qrCode: qrCodeReturn, 163 | pairingCode: pairingCodeReturn 164 | }; 165 | 166 | } 167 | 168 | private attachSocketErrorHandlers(){ 169 | try{ 170 | this.sock?.ws?.on?.('error', (err: any) => this.handleSocketError(err)); 171 | this.sock?.ws?.on?.('close', () => { 172 | if(this.instance?.connectionStatus === 'ONLINE'){ 173 | console.warn(`[${this.owner}/${this.instanceName}] ws closed unexpectedly`); 174 | this.handleSocketError(new Error('ws closed')); 175 | } 176 | }); 177 | }catch(e){ 178 | console.warn(`[${this.owner}/${this.instanceName}] Failed to register ws handlers`, e); 179 | } 180 | 181 | try{ 182 | const anySock: any = this.sock; 183 | anySock?.options?.agent?.on?.('error', (err: any) => this.handleSocketError(err)); 184 | anySock?.options?.fetchAgent?.on?.('error', (err: any) => this.handleSocketError(err)); 185 | }catch(e){ 186 | console.warn(`[${this.owner}/${this.instanceName}] Failed to register agents handlers`, e); 187 | } 188 | } 189 | 190 | private handleSocketError(err: any){ 191 | if(!err) return; 192 | const msg = String(err?.message || ''); 193 | const code = err?.code; 194 | const isUndici = code === 'UND_ERR_SOCKET' || /terminated/i.test(msg) || /other side closed/i.test(msg); 195 | console.error(`[${this.owner}/${this.instanceName}] Socket/Fetch error captured`, { code, msg }); 196 | if(isUndici){ 197 | this.reconnectWithBackoff(); 198 | } 199 | } 200 | 201 | private async reconnectWithBackoff(){ 202 | if(this.instance?.connectionStatus === 'REMOVED') return; 203 | if(this.reconnectAttempts >= this.maxReconnectAttempts){ 204 | console.error(`[${this.owner}/${this.instanceName}] Reconnection limit reached`); 205 | return; 206 | } 207 | const wait = Math.min(30000, 1000 * 2 ** this.reconnectAttempts); 208 | this.reconnectAttempts++; 209 | console.log(`[${this.owner}/${this.instanceName}] Trying to reconnect in ${wait}ms (attempt ${this.reconnectAttempts}/${this.maxReconnectAttempts})`); 210 | await delay(wait); 211 | try{ 212 | await this.create({ owner: this.owner, instanceName: this.instanceName, phoneNumber: this.phoneNumber }); 213 | }catch(e){ 214 | console.error(`[${this.owner}/${this.instanceName}] Reconnection failed`, e); 215 | } 216 | } 217 | 218 | private registerGlobalHandlers(){ 219 | if(!(global as any).__zap_global_error_wrapped){ 220 | (global as any).__zap_global_error_wrapped = true; 221 | 222 | process.on('uncaughtException', (err) => { 223 | if(/terminated/i.test(String(err?.message))){ 224 | console.error('UncaughtException (terminated) captured. Process preserved.'); 225 | }else{ 226 | console.error('UncaughtException', err); 227 | } 228 | }); 229 | 230 | process.on('unhandledRejection', (reason: any) => { 231 | if(/terminated/i.test(String(reason?.message))){ 232 | console.error('UnhandledRejection (terminated) captured. Process preserved.'); 233 | }else{ 234 | console.error('UnhandledRejection', reason); 235 | } 236 | }); 237 | } 238 | } 239 | 240 | async instanceEvents(saveCreds: () => Promise){ 241 | 242 | this.sock.ev.on("creds.update", saveCreds as (data: BaileysEventMap["creds.update"]) => void); 243 | 244 | this.sock.ev.on("connection.update", async (update: BaileysEventMap['connection.update']) => { 245 | const { connection, lastDisconnect, qr } = update; 246 | 247 | if(this.phoneNumber && qr){ 248 | 249 | await delay(1500); 250 | const pairingCode = await this.sock.requestPairingCode(this.phoneNumber); 251 | 252 | if(this.qrCodeCount > UserConfig.qrCodeLimit){ 253 | 254 | console.log(`[${this.owner}/${this.instanceName}] PAIRING CODE LIMIT REACHED`); 255 | await trySendWebhook("pairingcode.limit", this.instance, { pairingCodeLimit: UserConfig.qrCodeLimit }); 256 | 257 | await this.clearInstance(); 258 | 259 | }else{ 260 | 261 | this.qrCodeCount++; 262 | console.log(`Pairing Code: ${pairingCode}`); 263 | 264 | await trySendWebhook("pairingcode.updated", this.instance, { pairingCode }); 265 | 266 | } 267 | 268 | }else if(qr){ 269 | 270 | this.qrCodeCount++; 271 | 272 | if(this.qrCodeCount > UserConfig.qrCodeLimit){ 273 | 274 | console.log(`[${this.owner}/${this.instanceName}] QRCODE LIMIT REACHED`); 275 | 276 | await trySendWebhook("qrcode.limit", this.instance, { qrCodeLimit: UserConfig.qrCodeLimit }); 277 | 278 | await this.clearInstance(); 279 | 280 | }else{ 281 | 282 | const qrBase64 = await QRCode.toDataURL(qr); 283 | 284 | QRCode.toString(qr, { type: "utf8" }, (err, qrTerminal) => { 285 | if (!err){ 286 | console.log(qrTerminal); 287 | } 288 | }); 289 | 290 | if (this.qrCodeResolver) { 291 | this.qrCodeResolver(qrBase64); 292 | delete this.qrCodeResolver; 293 | } 294 | 295 | await trySendWebhook("qrcode.updated", this.instance, { qrCode: qrBase64 }); 296 | 297 | } 298 | 299 | }else if(connection === "connecting"){ 300 | 301 | this.setStatus("OFFLINE"); 302 | 303 | await trySendWebhook("connection.connecting", this.instance, update); 304 | 305 | }else if (connection === "open"){ 306 | 307 | this.setStatus("ONLINE"); 308 | 309 | const ppUrl = await this.getProfilePicture(); 310 | this.instance.profilePictureUrl = ppUrl; 311 | 312 | console.log(`[${this.owner}/${this.instanceName}] Connected to Whatsapp`); 313 | 314 | this.sock.sendPresenceUpdate('unavailable'); 315 | 316 | await delay(2); 317 | 318 | await trySendWebhook("connection.open", this.instance, update); 319 | 320 | if (this.qrCodeResolver) { 321 | this.qrCodeResolver(''); 322 | delete this.qrCodeResolver; 323 | } 324 | 325 | }else if(connection === "close"){ 326 | 327 | this.setStatus("OFFLINE"); 328 | 329 | const reason = (lastDisconnect?.error as any)?.output?.statusCode; 330 | 331 | const shouldReconnect = reason !== DisconnectReason.loggedOut; 332 | 333 | if (shouldReconnect) { 334 | 335 | await trySendWebhook("connection.close", this.instance, reason); 336 | await this.create({ owner:this.owner, instanceName:this.instanceName, phoneNumber: this.phoneNumber}); 337 | 338 | } else { 339 | 340 | console.log(`[${this.owner}/${this.instanceName}] REMOVED`); 341 | console.log(`Reason: ${DisconnectReason[reason!]}`); 342 | 343 | await trySendWebhook("connection.removed", this.instance, reason); 344 | 345 | await this.clearInstance(); 346 | 347 | } 348 | } 349 | }); 350 | 351 | this.sock.ev.on("messaging-history.set", async({messages, chats, contacts}: BaileysEventMap['messaging-history.set']) => { 352 | 353 | if(contacts && contacts.length > 0){ 354 | await PrismaConnection.saveManyContacts(`${this.instance.owner}_${this.instance.instanceName}`, contacts); 355 | await trySendWebhook("contacts.set", this.instance, contacts); 356 | } 357 | 358 | if(chats && chats.length > 0){ 359 | await trySendWebhook("chats.set", this.instance, chats); 360 | } 361 | 362 | if(messages && messages.length > 0){ 363 | 364 | const rawMessages: MessageWebhook[] = []; 365 | 366 | for(const msg of messages){ 367 | 368 | if(msg.message?.protocolMessage || msg.message?.senderKeyDistributionMessage || !msg.message){ 369 | continue; 370 | } 371 | 372 | const contentType = getContentType(msg?.message); 373 | 374 | if(!contentType){ 375 | continue; 376 | } 377 | 378 | let timestamp = msg?.messageTimestamp; 379 | 380 | if(timestamp && typeof timestamp === 'object' && typeof timestamp.toNumber === 'function'){ 381 | timestamp = timestamp.toNumber(); 382 | }else if(timestamp && typeof timestamp === 'object' && 'low' in timestamp){ 383 | timestamp = Number((timestamp as any).low) || 0; 384 | }else if(typeof timestamp !== 'number'){ 385 | timestamp = 0; 386 | } 387 | 388 | msg.messageTimestamp = timestamp; 389 | 390 | rawMessages.push({ 391 | ...msg, 392 | messageType: contentType 393 | }); 394 | } 395 | 396 | const sanitized = messages.map(m => this.sanitizeWAMessage(m)); 397 | await PrismaConnection.saveManyMessages(`${this.instance.owner}_${this.instance.instanceName}`, sanitized); 398 | trySendWebhook("messages.set", this.instance, rawMessages); 399 | } 400 | 401 | }); 402 | 403 | this.sock.ev.on("chats.upsert", async (chats: BaileysEventMap['chats.upsert']) => { 404 | await trySendWebhook("chats.upsert", this.instance, chats); 405 | }); 406 | 407 | this.sock.ev.on("chats.update", async (chats: BaileysEventMap['chats.update']) => { 408 | await trySendWebhook("chats.update", this.instance, chats); 409 | }); 410 | 411 | this.sock.ev.on("chats.delete", async (ids: BaileysEventMap['chats.delete']) => { 412 | await trySendWebhook("chats.delete", this.instance, ids); 413 | }); 414 | 415 | this.sock.ev.on("lid-mapping.update", async (mapping: BaileysEventMap['lid-mapping.update']) => { 416 | await trySendWebhook("lid-mapping.update", this.instance, mapping); 417 | }); 418 | 419 | this.sock.ev.on("presence.update", async (presence: BaileysEventMap['presence.update']) => { 420 | await trySendWebhook("presence.update", this.instance, presence); 421 | }); 422 | 423 | this.sock.ev.on("contacts.upsert", async (contacts: BaileysEventMap['contacts.upsert']) => { 424 | await PrismaConnection.saveManyContacts(`${this.instance.owner}_${this.instance.instanceName}`, contacts); 425 | await trySendWebhook("contacts.upsert", this.instance, contacts); 426 | }); 427 | 428 | this.sock.ev.on("contacts.update", async (contacts: BaileysEventMap['contacts.update']) => { 429 | await PrismaConnection.saveManyContacts(`${this.instance.owner}_${this.instance.instanceName}`, contacts); 430 | await trySendWebhook("contacts.update", this.instance, contacts); 431 | }); 432 | 433 | this.sock.ev.on("messages.upsert", async (messages: BaileysEventMap['messages.upsert']) => { 434 | this.sock.sendPresenceUpdate('unavailable'); 435 | 436 | const rawMessages: MessageWebhook[] = []; 437 | 438 | for(const msg of messages.messages){ 439 | 440 | if(!msg?.message){ 441 | await this.sock.waitForMessage(msg.key.id!); 442 | continue; 443 | } 444 | 445 | const contentType = getContentType(msg?.message); 446 | 447 | if(!contentType){ 448 | continue; 449 | } 450 | 451 | let timestamp = msg?.messageTimestamp; 452 | 453 | if(timestamp && typeof timestamp === 'object' && typeof timestamp.toNumber === 'function'){ 454 | timestamp = timestamp.toNumber(); 455 | }else if(timestamp && typeof timestamp === 'object' && 'low' in timestamp){ 456 | timestamp = Number((timestamp as any).low) || 0; 457 | }else if(typeof timestamp !== 'number'){ 458 | timestamp = 0; 459 | } 460 | 461 | msg.messageTimestamp = timestamp; 462 | 463 | rawMessages.push({ 464 | ...msg, 465 | messageType: contentType 466 | }); 467 | } 468 | 469 | const sanitized = messages.messages.map(m => this.sanitizeWAMessage(m)); 470 | await PrismaConnection.saveManyMessages(`${this.instance.owner}_${this.instance.instanceName}`, sanitized); 471 | await trySendWebhook("messages.upsert", this.instance, rawMessages); 472 | 473 | }); 474 | 475 | this.sock.ev.on("messages.update", async (updates: BaileysEventMap['messages.update']) => { 476 | 477 | const nupdates = await Promise.all( 478 | updates.map(async (message) => { 479 | const { key, update } = message; 480 | if(update.pollUpdates){ 481 | const pollCreation = await PrismaConnection.getMessageById(key.id!) as any; 482 | if(pollCreation?.message){ 483 | const pollVotes = getAggregateVotesInPollMessage({ 484 | message: pollCreation.message, 485 | pollUpdates: update.pollUpdates 486 | }); 487 | 488 | const newUpdate = { 489 | ...update, 490 | pollVotes 491 | } as any; 492 | 493 | return { ...message, update: newUpdate } as Partial; 494 | 495 | } 496 | } 497 | return message; 498 | }) 499 | ); 500 | 501 | await trySendWebhook("messages.update", this.instance, nupdates); 502 | }); 503 | 504 | this.sock.ev.on("messages.delete", async (deletes: BaileysEventMap['messages.delete']) => { 505 | await trySendWebhook("messages.delete", this.instance, deletes); 506 | }); 507 | 508 | this.sock.ev.on("messages.media-update", async (mediaUpdates: BaileysEventMap['messages.media-update']) => { 509 | await trySendWebhook("messages.media-update", this.instance, mediaUpdates); 510 | }); 511 | 512 | this.sock.ev.on("messages.reaction", async (reactions: BaileysEventMap['messages.reaction']) => { 513 | await trySendWebhook("messages.reaction", this.instance, reactions); 514 | }); 515 | 516 | this.sock.ev.on("message-receipt.update", async (receipts: BaileysEventMap['message-receipt.update']) => { 517 | await trySendWebhook("message-receipt.update", this.instance, receipts); 518 | }); 519 | 520 | this.sock.ev.on("groups.upsert", async (groups: BaileysEventMap['groups.upsert']) => { 521 | await trySendWebhook("groups.upsert", this.instance, groups); 522 | }); 523 | 524 | this.sock.ev.on("groups.update", async (groups: BaileysEventMap['groups.update']) => { 525 | const [event] = groups; 526 | const metadata = await this.sock.groupMetadata(event?.id!); 527 | groupCache.set(event?.id!, metadata); 528 | await trySendWebhook("groups.update", this.instance, groups); 529 | }); 530 | 531 | this.sock.ev.on("group-participants.update", async (update: BaileysEventMap['group-participants.update']) => { 532 | const metadata = await this.sock.groupMetadata(update.id); 533 | groupCache.set(update.id, metadata); 534 | await trySendWebhook("group-participants.update", this.instance, update); 535 | }); 536 | 537 | this.sock.ev.on("group.join-request", async (request: BaileysEventMap['group.join-request']) => { 538 | await trySendWebhook("group.join-request", this.instance, request); 539 | }); 540 | 541 | this.sock.ev.on("blocklist.set", async (blocklist: BaileysEventMap['blocklist.set']) => { 542 | await trySendWebhook("blocklist.set", this.instance, blocklist); 543 | }); 544 | 545 | this.sock.ev.on("blocklist.update", async (update: BaileysEventMap['blocklist.update']) => { 546 | await trySendWebhook("blocklist.update", this.instance, update); 547 | }); 548 | 549 | this.sock.ev.on("call", async (calls: BaileysEventMap['call']) => { 550 | await trySendWebhook("call", this.instance, calls); 551 | }); 552 | 553 | this.sock.ev.on("labels.edit", async (label: BaileysEventMap['labels.edit']) => { 554 | await trySendWebhook("labels.edit", this.instance, label); 555 | }); 556 | 557 | this.sock.ev.on("labels.association", async (assoc: BaileysEventMap['labels.association']) => { 558 | await trySendWebhook("labels.association", this.instance, assoc); 559 | }); 560 | 561 | this.sock.ev.on("newsletter.reaction", async (reaction: BaileysEventMap['newsletter.reaction']) => { 562 | await trySendWebhook("newsletter.reaction", this.instance, reaction); 563 | }); 564 | 565 | this.sock.ev.on("newsletter.view", async (view: BaileysEventMap['newsletter.view']) => { 566 | await trySendWebhook("newsletter.view", this.instance, view); 567 | }); 568 | 569 | this.sock.ev.on("newsletter-participants.update", async (update: BaileysEventMap['newsletter-participants.update']) => { 570 | await trySendWebhook("newsletter-participants.update", this.instance, update); 571 | }); 572 | 573 | this.sock.ev.on("newsletter-settings.update", async (update: BaileysEventMap['newsletter-settings.update']) => { 574 | await trySendWebhook("newsletter-settings.update", this.instance, update); 575 | }); 576 | 577 | 578 | } 579 | 580 | setStatus(status: ConnectionStatus): void{ 581 | 582 | this.instance.connectionStatus = status; 583 | instanceStatus.set(this.key, status); 584 | 585 | } 586 | 587 | async clearInstance(){ 588 | 589 | try{ 590 | 591 | await this.sock?.ws?.close?.(); 592 | 593 | this.setStatus("REMOVED"); 594 | 595 | await clearInstanceWebhooks(`${this.owner}_${this.instanceName}`); 596 | await removeInstancePath(this.instancePath); 597 | 598 | PrismaConnection.deleteByInstance(`${this.owner}_${this.instanceName}`); 599 | 600 | for(const event of baileysEvents){ 601 | this.sock?.ev.removeAllListeners(event); 602 | } 603 | 604 | delete instanceConnection[this.key]; 605 | delete instances[this.key]; 606 | 607 | }catch{ 608 | console.error("Error removing instance"); 609 | } 610 | 611 | } 612 | 613 | async getProfilePicture(): Promise { 614 | try { 615 | 616 | const jid = this.sock?.user?.id; 617 | 618 | if (!jid){ 619 | return undefined; 620 | } 621 | 622 | return await this.sock?.profilePictureUrl(jid, "image"); 623 | 624 | } catch { 625 | return undefined; 626 | } 627 | } 628 | 629 | async getMessage(key: string): Promise { 630 | 631 | await delay(2); 632 | 633 | const message: WAMessage | undefined = await PrismaConnection.getMessageById(key); 634 | 635 | if(message?.message){ 636 | return proto.Message.fromObject(message.message); 637 | } 638 | 639 | return proto.Message.fromObject({}); 640 | 641 | } 642 | 643 | private deepSanitize(value: any): any { 644 | if (value === null || value === undefined) return value; 645 | if (typeof value === 'bigint') return Number(value); 646 | if (typeof value === 'function') return undefined; 647 | if (value instanceof Uint8Array) return Buffer.from(value).toString('base64'); 648 | if (Array.isArray(value)) { 649 | return value.map(v => this.deepSanitize(v)).filter(v => v !== undefined); 650 | } 651 | if (typeof value === 'object') { 652 | if ('low' in value && 'high' in value && 653 | typeof (value as any).low === 'number' && 654 | typeof (value as any).high === 'number') { 655 | const low = (value as any).low >>> 0; 656 | const high = (value as any).high >>> 0; 657 | return high * 2 ** 32 + low; 658 | } 659 | const out: any = {}; 660 | for (const [k, v] of Object.entries(value)) { 661 | const sv = this.deepSanitize(v); 662 | if (sv !== undefined) out[k] = sv; 663 | } 664 | return out; 665 | } 666 | return value; 667 | } 668 | 669 | private sanitizeWAMessage(msg: any): any { 670 | return this.deepSanitize(msg); 671 | } 672 | 673 | } 674 | 675 | 676 | --------------------------------------------------------------------------------